This article describes a fast Game of Life implementation with MFC, hence the strange name Game of MFC Live. It uses lambdas, a C++ standard random number generator, contains a tiny JSON reader and shows how to store user settings as binary. Some of the obstacles I stumbled upon using the MFC Direct2D classes are explained.
To compile the code, you need Visual Studio 2019 or newer. To run the binaries, you must have the "Visual Studio 2015-2019 Runtime" 32 bit or 64bit installed.
Introduction
The application has the following features:
- Initial cell population is created by random, with three possible densities
- Shows trails of dead cells (they fade out!)
- Play field size is user-configurable
- Cells are drawn with user-defined size and optionally anti-aliased
- Cells can be drawn as bitmap, allowing really big play fields
- Single-step and endless mode
- Store/load games
- Live diagram of population count
- Switch between normal and fullscreen mode (press ESC key)
Background
The Wikipedia article about Conway's Game of Life describes the rules for cell generation.
As the game is stored as JSON file, which is basically text, you can create your own cell formation with Notepad and test how long it will survive. Simply edit a saved game and then reload and run it.
Direct2D is Microsoft's new 2D API for high-performance 2Dgraphics. The rendering is done by the GPU with 3D primitives, while GDI is in Vista and above CPU only.
Using the Code
The basic idea for fast calculation of a new generation is to have different lambdas for the inner and outer cell range. Therefore the lambda for outer cell range which has the additional burden of range checking is only applied to a small percentage of cells.
The fastest approach would be to widen the cell area with an "invisible" border. Then no range checking would be needed, but the functions for initializing and drawing would be more complicated and I think the code would be less clear.
Below, I explain the functions and how they work together.
The generate
lambda for applying the Conway's Rules, returns the new state of the cell: New state can be Living
(=0) or one of the dead levels FadeStart
... RealDead
.
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; }
}
else {
if (neighbours < 2 || neighbours > 3)
return FadeStart; }
return Living;
};
The alive
lambda for checking if a cell is alive is used for the inner cells, returns true
if cell is living:
auto alive= [&](int x, int y) -> bool {
return cells[y][x] == Living;
};
The aliveClamped
lambda for checking if a cell is alive is used for the potentially border cells, outside cells are assumed dead:
auto aliveClamped= [&](int x, int y) -> bool {
if (x >= 0 && y >= 0 && x < cx && y < cy)
return cells[y][x] == Living;
return false;
};
The CountNeighbours
template applies the given AliveFunc
to all the neighbours of a cell, returns the count of living neighbours:
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);
};
Here, the lambdas work together to generate the new generation from cells
array in cells2
array:
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));
}
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));
}
}
Another aspect is using row-pointers still has the best speed for the two-dimensional array access. I do not use a vector<vector<BYTE>>
for the cell arrays. Neither do I use a vector cellData<BYTE>
of size cx
*cy
and then use index math cellData[y*cx+x]
. I allocate cellData<BYTE>
and use a row-pointer vector cells<BYTE*>
to access the data:
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;
}
If you are adventurous enough, you can play with the #defines
in the code, you have the following possibilities:
- Comment in
#define TIMING
in ChildView.cpp to measure the time for creating a new generation and for draw, shows them in status bar - Comment out
#define USED2D
in ChildView.h to draw with GDI instead of Direct2D (not well tested, no population chart, and slow!) - Comment out
#define GDI_IMMEDIATE_DRAW
in ChildView.cpp to draw with GDI only in the OnPaint
routine (even slower than above!)
Points of Interest
What I've learned is there are nice MFC wrapper classes for Direct2D but some edges on Direct2D are not well documented. Below, you find my suggestions:
Draw the scene if you receive the (by MFC) registered message AFX_WM_DRAW2D
.
When the Direct2D render target is lost, you receive the (by MFC) registered message AFX_WM_RECREATED2DRESOURCES
. Direct2D render target is lost if you swap graphic cards ;-), but more important if you lock your computer and unlock it afterward. This means MFC has recreated all Direct2D objects you use, as they are associated with the render target. But after that a redraw operation must be triggered, and simply redrawing in the message handler does not work. You must post a user message and in handling this message, redraw the play field.
Look into the code for the AFX_WM_RECREATED2DRESOURCES
handler to see this working.
Moreover, if your render target EndDraw()
returns D2DERR_RECREATE_TARGET
, you also need to recreate the Direct2D object you use.
What I did not find in documentation, there is an easy way to recreate the Direct2D objects: The MFC wrapper classes store the parameters for the creation of the corresponding Direct2D objects and the render target holds a list of objects. Therefore, you simply can call CRenderTarget::Recreate(*this)
and MFC recreates the resources for you.
After a long period of time, I got a laptop with high-DPI display. The application initially scaled wrong, as Direct2D works with DIPs (Device independent pixels). Therefore I had to scale at my own. Also, I made the font of the graph a little bit bigger on high-DPI display.
After another period of time, I got a historic flashback and tried to get the app working on Windows XP. To do that, I added the Visual Studio 2017 for XP compiler and the appropriate MFC libraries in Visual Studio 2019. Unfortunately, I had to recreate a new MFC Project with the Wizard to get the project to compile. But finally, it works.
The 32bit version is now the version which runs on Windows XP and above. It draws using GDI (slower) and will not have anti-aliased drawing. The graph window is not draw by alpha bending but covers the playfield.
I also fixed some small bugs again.
History
- 12th July, 2014: Initial release
- 22nd July, 2014: Better explained cell generation approach, fullscreen feature described
- 19th April, 2020: Small fixes and High-DPI Monitor enhancements
- 21st July 2020: 32bit Version now runs on Windows XP, small fixes,
AFX_WM_RECREATED2DRESOURCES
handling improved