探索生命游戏的编程实现

生命游戏,又称为康威生命游戏,是一个零玩家游戏,由英国数学家约翰·康威在1970年发明。这个游戏虽然简单,却能够展示出复杂而有趣的行为模式。本文将介绍如何使用Visual Studio进行生命游戏的编程实现,包括Direct2D的使用,以及在不同Windows版本上的兼容性处理。

编译与运行环境

要编译这个游戏,需要安装Visual Studio2019或更新的版本。运行编译后的二进制文件,则需要安装“Visual Studio 2015-2019 Runtime”的32位或64位版本。

应用特性

这个应用程序具有以下特性:

  • 初始细胞群通过随机方式创建,具有三种可能的密度。
  • 展示死亡细胞的轨迹(它们会逐渐消失)。
  • 游戏场地大小可由用户配置。
  • 细胞的绘制大小由用户定义,并且可以选择是否进行抗锯齿处理。
  • 细胞可以以位图形式绘制,允许非常大的游戏场地。
  • 单步和无尽模式。
  • 保存/加载游戏。
  • 实时显示人口计数图。
  • 在普通和全屏模式之间切换(按下ESC键)。

维基百科上关于Conway's Game of Life的文章描述了细胞生成的规则。游戏以JSON文件形式存储,基本上是文本,可以使用记事本创建自己的细胞形态,并测试它能存活多久。简单地编辑一个保存的游戏,然后重新加载并运行它。

Direct2D的使用

Direct2D是微软的新型2D API,用于高性能的2D图形渲染。渲染由GPU使用3D原语完成,而GDI在Vista及以上版本中仅使用CPU。

代码使用

快速计算新一代的基本思想是为内部和外部细胞范围使用不同的lambda表达式。因此,用于外部细胞范围的lambda表达式,由于需要额外的范围检查,只应用于一小部分细胞。

最快的方法可能是通过一个“不可见”的边界来扩大细胞区域。这样就不需要范围检查了,但是初始化和绘制的函数会更复杂,认为代码也会更不清晰。

函数解释

下面,将解释函数及其工作原理。

auto generate = [&](int x, int y, int neighbours) -> BYTE { BYTE cs = cells[y][x]; if (cs != Living) { if (neighbours != 3) { 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; };

这个generate lambda用于应用康威规则,返回细胞的新状态:新状态可以是Living(=0)或其中一个死亡级别FadeStart...RealDead

性能优化

使用行指针仍然具有二维数组访问的最佳速度。没有使用vector>来存储细胞数组。也没有使用大小为cx*cyvector cellData,然后使用索引数学cellData[y*cx+x]。分配了cellData并使用行指针向量cells来访问数据:

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; }

兼容性处理

在代码中,可以尝试不同的#define定义,以实现以下可能性:

  • ChildView.cpp中注释掉#define TIMING,以测量创建新一代和绘制的时间,并将它们显示在状态栏上。
  • ChildView.h中注释掉#define USED2D,以使用GDI而不是Direct2D进行绘制(未经充分测试,没有人口图表,且较慢!)。
  • ChildView.cpp中注释掉#define GDI_IMMEDIATE_DRAW,仅在OnPaint例程中使用GDI进行绘制(比上述方法更慢!)。

开发经验分享

  • 当接收到(由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就会为重新创建资源。
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485