Click here to Skip to main content
11,581,982 members (74,961 online)
Click here to Skip to main content

A Game of Life Implementation using Direct2D

, 23 Jul 2014 CPOL 10.3K 1.2K 25
Rate this:
Please Sign up or sign in to vote.
Just for fun, a Conway's Game of Life application with many features using MFC Direct2D classes

To compile the code you need Visual Studio 2012 or newer.

Introduction

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.

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

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:

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

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'v 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 Wink | ;-) , but more important if you lock your computer and unlock it afterward. This means you must recreate all Direct2D objects you use, as they are associated with the render target.

What I did not found 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.

Another small caveat is that after recreating the resources a redraw operation must be triggered, and simply redrawing in the OnRecreate2DResources() 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.

History

2014-07-12 initial release

2014-07-22 better explain cell generation approach, fullscreen feature described

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Hans
Germany Germany
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Mihai MOGA13-Aug-14 1:40
professionalMihai MOGA13-Aug-14 1:40 
QuestionThanks and a question about Direct2D performance Pin
Colin Humphries15-Jul-14 23:09
memberColin Humphries15-Jul-14 23:09 
AnswerRe: Thanks and a question about Direct2D performance Pin
Hans16-Jul-14 4:13
memberHans16-Jul-14 4:13 
GeneralRe: Thanks and a question about Direct2D performance Pin
Colin Humphries16-Jul-14 4:46
memberColin Humphries16-Jul-14 4:46 
GeneralMy vote of 5 Pin
deshjibon14-Jul-14 17:31
memberdeshjibon14-Jul-14 17:31 
GeneralRe: My vote of 5 Pin
Hans15-Jul-14 4:22
memberHans15-Jul-14 4:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150603.1 | Last Updated 23 Jul 2014
Article Copyright 2014 by Hans
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid