Click here to Skip to main content
14,874,239 members
Articles / Desktop Programming / WPF
Posted 31 Aug 2010

Tagged as


32 bookmarked

WPF Game of Life

Rate me:
Please Sign up or sign in to vote.
4.92/5 (18 votes)
31 Aug 2010CPOL7 min read
WPF XBAP that implements Conways Game of Life


This article describes an XBAP (XAML Browser Application) implementation of Conway's Game of Life. This is my first article and in fact my first serious project in WPF. The reason I've done it is more as a learning exercise to see what WPF is capable of (a lot!) and what the limitations of XBAPs were. Life was a good candidate for testing a simple XBAP because it follows simple rules and will happily play in the security sandbox that XBAPs operate in - interestingly for me, Life was also one of the first applications I ever wrote when I first started programming (a long time ago).

As well as exploring XBAPs, this application also allowed to me to learn about writing FrameworkElement derived classes. I think XAML is great glue code, but if you want real performance you do sometimes need to deal with Visuals and drawing code.



There are many web sites that deal with Conway's Game of Life - perhaps the best introduction to the topic is the Wikipedia article, In essence, the game consists of an infinite two dimensional array of cells containing cellular automata that are in one of two states: alive or dead, and follow simple rules as follows (extract from Wikipedia):

Every cell interacts with its eight neighbours, which are the cells that are directly horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:

  1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
  2. Any live cell with more than three live neighbours dies, as if by overcrowding.
  3. Any live cell with two or three live neighbours lives on to the next generation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

The game starts with the user setting which cells are alive, then the game advances one generation at a time. Typically a copy is made of the grid for the next generation and cells are filled in based on the previous grid and the rules listed above.

It's difficult to implement an 'infinite' grid, so what most programmers do, is implement a 'torus' which is essentially a rectangular area in which the left and right edges are joined and the top and bottom edges are joined, e.g. if the grid is N x N then the cell at (0, 0) is diagonally adjacent to the cell at (N - 1, N - 1).

In this particular implement of Life, I have added the following:

  • Clear button - clears all cells
  • Next - advances the 'game' by one generation
  • Animate button - automatically advances the game at a rate of 20 generations per second. This is a toggle button, so clicking it again will halt automation. Automation will also stop if the game reaches a static, i.e. unchanging state. In actual fact, the game will often enter into a repeating two state sequence (e.g. blinkers) but we don't detect those.
  • Dimensions - sets the dimensions of the grid.
  • Zoom combo box - allows zooming out in 10% steps from 100% to 10%.
  • Generation - displays the generation number. This is cleared to 0 by the clear button. The generation is useful as many patterns are characterized by their lifetime, i.e. how many generations before they become stable.
  • Hovering the mouse over a cell displays a pink cell.
  • Click in the grid to set or clear a cell.
  • Click and drag in the grid to set a sequence of cells.

Using the Code

The code is essentially broken up into the following classes:

  • Page1.xaml - This is the main XBAP page
  • LifeView - This is a FrameworkElement derived class that provides the user interface for the game.
  • LifeModel - Implements the rules of the game. In particular, it allows setting of cells and a Next() method to advance the game one generation.
  • LifeTorus - This implements a boolean two dimensional array. The indexer getter implements the torus function thus simplifying the LifeModel code.
  • The Dimensions struct provides a functions for integer height and width being the number of cells in the Location.

Starting with Page1.xaml, most the XAML is pretty standard. The interesting bit is where the LifeView FrameworkElement is as shown below:

	<ComboBox ItemStringFormat="p0"  Width="60" Name="comboBox2"  
	SelectedValue="{Binding ElementName=viewScale, Path=ScaleX}">
	<Separator Margin="5,0,5,0"/>
	<TextBlock Name="label1" VerticalAlignment="Center" Width="100" 
	Text="{Binding ElementName=lifeView1, Path=Generation, 
	StringFormat=Generation \{0\} }"/>

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
	<local:LifeView x:Name="lifeView1" Background="Ivory" 
		Foreground="{StaticResource cellBrush}" Padding="20"  >
			<ScaleTransform x:Name="viewScale" 
			ScaleY="{Binding ElementName=comboBox2, 
			Path=SelectedValue, Mode=OneWay}" />

Dependency properties were added for the background, foreground and padding of LifeView to allow them to be set in XAML. The foreground is set to a LinearGradientBrush stored in the page's resources (I thought it looked more like an automoton than a flat brush would).

To allow scrolling, the LifeView instance is wrapped in a ScrollViewer - be sure to set HorizontalScrollBarVisibility=true and VerticalScrollBarVisibility=true or the scrollbars won't appear/disappear as expected.

To implement zooming, the LayoutTransform is set using a ScaleTransform which has its ScaleY property bound to comboBox2's SelectedValue and the comboBox2's SelectedValue is in a two way binding with the ScaleX property. Note both ScaleX and
ScaleY have to be set to maintain the aspect ration as its scrolled. Note also if you set the RenderTransform instead of the LayoutTransform you will still get zooming but the actual width and height of LifeView won't be correctly interpreted by the ScrollViewer.

One last item on the XAML - note that the number of generations is displayed in the toolbar by the following binding:

Text="{Binding ElementName=lifeView1, Path=Generation, StringFormat=Generation \{0\} }" 

which uses the StringFormat attribute of the binding.

Looking now at LifeView, the snippet below shows some of the dependency properties that were defined.

#region Dependency properties

public int CellSize
    get { return (int)GetValue(CellSizeProperty); }
    set { SetValue(CellSizeProperty, value); }

public static readonly DependencyProperty CellSizeProperty =
    DependencyProperty.Register("CellSize", typeof(int), typeof(LifeView), 
    new FrameworkPropertyMetadata(12, FrameworkPropertyMetadataOptions.AffectsMeasure));

public Dimensions Dimensions
    get { return (Dimensions)GetValue(DimensionsProperty); }
    set { SetValue(DimensionsProperty, value); }

public static readonly DependencyProperty DimensionsProperty =
    DependencyProperty.Register("Dimensions", typeof(Dimensions), typeof(LifeView),
    new FrameworkPropertyMetadata(new Dimensions(64, 64), 
    FrameworkPropertyMetadataOptions.AffectsMeasure, DimensionsChangedCallback));

static void DimensionsChangedCallback
	(DependencyObject d, DependencyPropertyChangedEventArgs e)

The CellSize property is defined as AffectsMeasure to force the MeasureOverride and ArrangeOverride()s to be called and the default value is set to 12. The Dimensions property is also marked as AffectsMeasure but also has a property changed callback defined to force redrawing of the grid. The callback must be static but it simply cats the DependencyObject to LifeView before calling the instance method.

When deriving from FrameworkElement, etc., the main two methods you need to implement are the MeasureOverride and OnRender(). In MeasureOverride, we can return our preferred size based on the available size. In our case, we don't care what is available and simply return our preferred size as being the CellSize times the Dimensions and allowing for the Padding - see snippet below.

protected override Size MeasureOverride(Size availableSize)
    // Ignore availableSize - just return our 'desired' size
    Dimensions dim = this.Dimensions;
    int cellSize = this.CellSize;
    Thickness padding = this.Padding;
    return new Size(dim.Width * cellSize + padding.Left + padding.Right,
    	dim.Height * cellSize + padding.Top + padding.Bottom);

The other main method I mentioned is OnRender(). This is our opportunity to draw into the DrawingContext of the underlying Visual. In actual fact, we choose not to do so but actually do our drawing into three separate child Visuals each representing a separate and independent drawing layer. For convenience, I have juxtaposed three separate parts of the code below.

#region Fields
List<Visual> _visuals = new List<Visual>();
DrawingVisual _gridVisual = new DrawingVisual();
DrawingVisual _cellsVisual = new DrawingVisual();
DrawingVisual _adornerVisual = new DrawingVisual();

public LifeView()
    AddVisual(_adornerVisual);// top of Z layer

void AddVisual(Visual child)

protected override Visual GetVisualChild(int index)
    return _visuals[index];

protected override int VisualChildrenCount
        return _visuals.Count;

The code shows that we maintain a list of visuals which can be accessed via the GetVisualChild() and VisualChildren overrides. Note that if you do not implement these methods, then even though you have added the Visuals using AddLogicalChild() and AddVisulaChild() they will not be visible. The order in which the child Visuals is also important since it governs the Z-Order or display order.

The three layers we implement are:

  • Grid layer - This is the unchanging grid (except if Dimensions or CellSize are altered) on which the automotons are drawn. Drawing this once reduces the calculation overheads of advancing to the next generation.
  • Cells layer - This where the cells are drawn using data stored in the LifeModel.
  • Adorner layer - Not the real adorner but it serves the same purpose. We use it here to highlight the cell underneath the mouse.

Drawing into a Visual is straightforward as the code for DarwModel() below shows.

public void DrawModel()
    using (DrawingContext dc = _cellsVisual.RenderOpen())
        Dimensions dim = this.Dimensions;
        Thickness padding = this.Padding;
        int cellSize = this.CellSize;
        Brush cellBrush = this.Foreground;
        for (int x = 0; x < dim.Width; x++)
        for (int y = 0; y < dim.Height; y++)
            if (_model[x, y])
                Rect rect = new Rect(padding.Left + x * cellSize + 1, 
                padding.Top + y * cellSize + 1, cellSize - 1, cellSize - 1);
                dc.DrawRectangle(cellBrush, null, rect);

The main points here that Visual.RenderOpen() opens the DrawingContext, in the process clearing any previous drawing. Drawings are lowest level code in WPF and also the fastest. The RenderOpen is wrapped in a 'using' to ensure it is closed (it implements IDisposable). The code then fetches the Dimensions Padding, CellSize and Foreground dependency properties once - dependency properties are much slower than regular properties.

Points of Interest

A minor point of interest is the Dimensions struct. I had to define a TypeConverter and reference that in the appropriate attribute on the class. The TypeConverter itself has to implement CanConvertFrom() and ConvertFrom() to allow setting of the Dimension from XAML - which we do with the comboBox1. As the code snippet below shows, we allow for the XAML string format to be two numbers separated by either spaces, a comma or an 'x' character.

[TypeConverter(typeof(DimensionsTypeConverter))]	// provided to allow 
						// setting of Dimension in XAML.
public struct Dimensions
	public int Width { get; set; }

	public int Height { get; set; }

	public Dimensions(int width, int height)
			: this()
		Width = width;
		Height = height;

	public override string ToString()
		return string.Format("({0}, {1})", Width, Height);

	public override bool Equals(object obj)
		if (!(obj is Dimensions))
			return false;
		Dimensions dim = (Dimensions)obj;
		return this.Width == dim.Width && this.Height == dim.Height;

	public override int GetHashCode()
		return Width ^ Height;

	public static bool operator !=(Dimensions lhs, Dimensions rhs)
		return lhs.Width != rhs.Width || lhs.Height != rhs.Height;

	public static bool operator ==(Dimensions lhs, Dimensions rhs)
		return lhs.Width == rhs.Width && lhs.Height == rhs.Height;

public class DimensionsTypeConverter : TypeConverter
	public override bool CanConvertFrom
		(ITypeDescriptorContext context, Type sourceType)
		if (sourceType == typeof(string))
			return true;
		return base.CanConvertFrom(context, sourceType);

	public override object ConvertFrom
		(ITypeDescriptorContext context, CultureInfo culture, object value)
		if (value is string)
			string text = (string)value;
			char separator = ' ';// allow either space, 
					// comma or x as a separator, e.g. 32 x 20
			if (text.Contains(','))
				separator = ',';
			else if (text.Contains('x'))
				separator = 'x';
			string[] args = text.Split(separator);
			if (args.Length != 2)
				throw new ArgumentException
				("Must have two comma separated numbers.");
				int width = 0;
				int height = 0;
				if (!int.TryParse(args[0].Trim(), out width) ||
					!int.TryParse(args[1].Trim(), out height))
					throw new ArgumentException
					("Either width or height 
						is not an integer.");
				return new Dimensions(width, height);

		return base.ConvertFrom(context, culture, value);

	public override bool CanConvertTo(ITypeDescriptorContext context, 
					Type destinationType)
		if (destinationType == typeof(Dimensions))
			return true;

		return base.CanConvertTo(context, destinationType);

	public override object ConvertTo(ITypeDescriptorContext context, 
			CultureInfo culture, object value, Type destinationType)
		if (value is Dimensions)
			Dimensions dim = (Dimensions)value;
			return string.Format("{0} x {1}", dim.Width, dim.Height);
		return base.ConvertTo(context, culture, value, destinationType);


This is the first iteration of the application. I did think it would be neat to come up with a three dimensional version of the game (with altered rules) to test out the 3D features of WPF - but I will have to leave that for some later date.


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


About the Author

Ron de Jong
Product Manager
Australia Australia
No Biography provided

Comments and Discussions

GeneralMy vote of 5 Pin
NetDave6-Dec-10 18:49
MemberNetDave6-Dec-10 18:49 
Questionmouse down event not reaching in my code Pin
umeshrameshsatavase13-Oct-10 23:50
Memberumeshrameshsatavase13-Oct-10 23:50 
AnswerRe: mouse down event not reaching in my code Pin
Ron de Jong8-Jan-11 12:38
MemberRon de Jong8-Jan-11 12:38 
GeneralMy vote of 5 Pin
dbrenth8-Sep-10 10:51
Memberdbrenth8-Sep-10 10:51 
GeneralMy vote of 5 Pin
Marcelo Ricardo de Oliveira6-Sep-10 1:42
MemberMarcelo Ricardo de Oliveira6-Sep-10 1:42 
GeneralGood article, nice app Pin
Gregory Gadow3-Sep-10 11:47
MemberGregory Gadow3-Sep-10 11:47 
GeneralRe: Good article, nice app Pin
Ron de Jong4-Sep-10 12:30
MemberRon de Jong4-Sep-10 12:30 
GeneralI saw something similar a while back Pin
Sacha Barber2-Sep-10 21:39
mvaSacha Barber2-Sep-10 21:39 
GeneralRe: I saw something similar a while back Pin
Ron de Jong2-Sep-10 22:46
MemberRon de Jong2-Sep-10 22:46 
GeneralRe: I saw something similar a while back Pin
Jaime Olivares7-Sep-10 11:59
MemberJaime Olivares7-Sep-10 11:59 
GeneralRe: I saw something similar a while back Pin
dojohansen7-Sep-10 22:22
Memberdojohansen7-Sep-10 22:22 
GeneralRe: I saw something similar a while back Pin
Ron de Jong8-Sep-10 15:05
MemberRon de Jong8-Sep-10 15:05 
QuestionAny particular version dependencies? Pin
torial1-Sep-10 16:46
Membertorial1-Sep-10 16:46 
AnswerRe: Any particular version dependencies? Pin
Ibrahim Yusuf2-Sep-10 5:58
MemberIbrahim Yusuf2-Sep-10 5:58 

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

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