使用 Direct2D 实现的生命游戏
纯粹为了好玩,一个具有许多功能的 Conway 生命游戏应用程序,使用了 MFC Direct2D 类
要编译代码,您需要 Visual Studio 2019 或更高版本。要运行二进制文件,您必须安装“Visual Studio 2015-2019 Runtime”32 位或 64 位。
引言
该应用程序具有以下功能:
- 初始细胞种群是随机生成的,具有三种可能的密度
- 显示死亡细胞的轨迹(它们会逐渐消失!)
- 游戏区域大小可由用户配置
- 细胞可以定义大小进行绘制,并可选抗锯齿
- 细胞可以绘制为位图,允许非常大的游戏区域
- 单步和无尽模式
- 保存/加载游戏
- 人口计数实时图表
- 在普通模式和全屏模式之间切换(按 ESC 键)
背景
Conway's Game of Life 的维基百科文章描述了细胞生成的规则。
由于游戏以 JSON 文件(本质上是文本)存储,您可以使用记事本创建自己的细胞布局,并测试它能存活多久。只需编辑已保存的游戏,然后重新加载并运行它。
Direct2D 是微软新的高性能 2D 图形 API。渲染由 GPU 使用 3D 原始对象完成,而 GDI 在 Vista 及以上版本中仅使用 CPU。
Using the Code
快速计算新一代的基本思想是为内部和外部细胞范围具有不同的 lambda。因此,具有额外范围检查负担的外部细胞范围的 lambda 只应用于一小部分细胞。
最快的方法是使用“不可见”边框来扩展细胞区域。这样就不需要进行范围检查,但初始化和绘制函数会更复杂,我认为代码会不那么清晰。
下面我将解释这些函数以及它们如何协同工作。
用于应用 Conway 规则的 generate
lambda 返回细胞的新状态:新状态可以是 Living
(=0) 或死亡级别之一 FadeStart
... RealDead
。
auto generate= [&](int x, int y, int neighbours) -> BYTE {
BYTE cs= cells[y][x];
if (cs != Living) {
if (neighbours != 3) {// if three neighbours --> new borne cell
if (cs == RealDead)
return RealDead;
static const BYTE LifeChangeFades= CChildView::FadeSteps / 2;
static_assert(((int)FadeLast - LifeChangeFades) > Living,
"Last fade count must be greater than Living");
lifechange= lifechange || cs >= (FadeLast - LifeChangeFades);
return cs+1; // next fade level
}
}
else {
if (neighbours < 2 || neighbours > 3)
return FadeStart; // new dead ones
}
return Living;
};
用于检查细胞是否存活的 alive
lambda 用于内部细胞,如果细胞存活则返回 true
。
auto alive= [&](int x, int y) -> bool {
return cells[y][x] == Living;
};
用于检查细胞是否存活的 aliveClamped
lambda 用于潜在的边界细胞,外部细胞假定为死亡。
auto aliveClamped= [&](int x, int y) -> bool {
if (x >= 0 && y >= 0 && x < cx && y < cy)
return cells[y][x] == Living;
return false;
};
CountNeighbours
模板将给定的 AliveFunc
应用于细胞的所有邻居,返回存活邻居的数量。
template <typename AliveFunc>
int CountNeighbours(int x, int y, AliveFunc f) {
return f(x-1,y-1) + f(x,y-1) + f(x+1,y-1) +
f(x-1,y) + f(x+1,y) +
f(x-1,y+1) + f(x,y+1) + f(x+1,y+1);
};
这里,lambda 协同工作,从 cells
数组生成到 cells2
数组的新一代。
// border cells generation using clamped neighbour counting
for (int x= 0; x < cx; ++x) {
cells2[0][x]= generate(x,0, CountNeighbours(x,0,aliveClamped));
cells2[cy-1][x]= generate(x,cy-1, CountNeighbours(x,cy-1,aliveClamped));
}
for (int y= 0; y < cy; ++y) {
cells2[y][0]= generate(0,y, CountNeighbours(0,y,aliveClamped));
cells2[y][cx-1]= generate(cx-1,y, CountNeighbours(cx-1,y,aliveClamped));
}
// inner cells generation using unclamped neighbour counting
for (int y= 1; y < cy-1; ++y) {
for (int x= 1; x < cx-1; ++x) {
cells2[y][x]= generate(x,y, CountNeighbours(x,y,alive));
}
}
另一个方面是,使用行指针对于二维数组访问仍然具有最佳速度。我没有为细胞数组使用 vector<vector<BYTE>>
。我也没有使用大小为 cx
*cy
的 cellData<BYTE>
向量,然后使用索引计算 cellData[y*cx+x]
。我分配了 cellData<BYTE>
,并使用行指针向量 cells<BYTE*>
来访问数据。
cellData.resize(size, RealDead);
cellData2.resize(size, RealDead);
cells.resize(cy);
cells2.resize(cy);
for (int y= 0; y < cy; ++y) {
cells[y]= cellData.data() + y*cx;
cells2[y]= cellData2.data() + y*cx;
}
如果您足够有冒险精神,可以玩玩代码中的 #defines
,您有以下几种可能性:
- 在 ChildView.cpp 中注释掉
#define TIMING
以测量创建新一代和绘制的时间,并在状态栏显示它们。 - 在 ChildView.h 中注释掉
#define USED2D
以使用 GDI 而不是 Direct2D 进行绘制(测试不充分,没有人口图,而且很慢!)。 - 在 ChildView.cpp 中注释掉
#define GDI_IMMEDIATE_DRAW
以仅在OnPaint
例程中使用 GDI 进行绘制(比上面更慢!)。
关注点
我学到的是,有很好的 MFC 封装类用于 Direct2D,但 Direct2D 的一些边缘之处文档并不完善。下面是我的建议:
当您收到(由 MFC)注册的消息 AFX_WM_DRAW2D
时,绘制场景。
当 Direct2D 渲染目标丢失时,您会收到(由 MFC)注册的消息 AFX_WM_RECREATED2DRESOURCES
。Direct2D 渲染目标会在您切换显卡时丢失;但更重要的是,当您锁定计算机然后解锁它之后。这意味着 MFC 已经重新创建了所有您使用的 Direct2D 对象,因为它们与渲染目标相关联。但之后必须触发重绘操作,仅仅在消息处理程序中重绘是行不通的。您必须发布一个用户消息,并在处理该消息时重绘游戏区域。
查看 AFX_WM_RECREATED2DRESOURCES
处理程序的代码,看看它是如何工作的。
此外,如果您的渲染目标 EndDraw()
返回 D2DERR_RECREATE_TARGET
,您还需要重新创建您使用的 Direct2D 对象。
我在文档中没有找到的是,重新创建 Direct2D 对象有一个简单的方法:MFC 封装类存储了创建相应 Direct2D 对象所需的参数,并且渲染目标维护了一个对象列表。因此,您只需调用 CRenderTarget::Recreate(*this)
,MFC 就会为您重新创建资源。
长时间后,我得到了一个带有高 DPI 显示器的笔记本电脑。应用程序最初缩放错误,因为 Direct2D 使用 DIP(设备无关像素)。因此,我不得不自己进行缩放。此外,在高 DPI 显示器上,我将图表的字体稍微调大了一些。
又过了一段时间,我得到了一个历史性的闪回,并尝试让应用程序在 Windows XP 上运行。为此,我在 Visual Studio 2019 中添加了 Visual Studio 2017 for XP 编译器和相应的 MFC 库。不幸的是,我不得不使用向导重新创建一个新的 MFC 项目才能使项目编译。但最终,它成功了。
32 位版本是现在可以在 Windows XP 及以上版本上运行的版本。它使用 GDI 进行绘制(较慢),并且不会进行抗锯齿绘制。图表窗口不是通过 alpha 混合绘制,而是覆盖了游戏区域。
我还再次修复了一些小错误。
历史
- 2014 年 7 月 12 日:初次发布
- 2014 年 7 月 22 日:更详细地解释了细胞生成方法,描述了全屏功能
- 2020 年 4 月 19 日:小修正和高 DPI 显示器增强
- 2020 年 7 月 21 日:32 位版本现在可以在 Windows XP 上运行,小修正,改进了
AFX_WM_RECREATED2DRESOURCES
处理