Click here to Skip to main content
Click here to Skip to main content

WPF Game of Life

, 31 Aug 2010
Rate this:
Please Sign up or sign in to vote.
WPF XBAP that implements Conways Game of Life

Introduction

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.

Life XBAP

Background

There are many web sites that deal with Conway's Game of Life - perhaps the best introduction to the topic is the Wikipedia article, http://en.wikipedia.org/wiki/Conways_Game_of_Life. 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}">
		<s:Double>1.0</s:Double>
		<s:Double>0.9</s:Double>
		<s:Double>0.8</s:Double>
		<s:Double>0.7</s:Double>
		<s:Double>0.6</s:Double>
		<s:Double>0.5</s:Double>
		<s:Double>0.4</s:Double>
		<s:Double>0.3</s:Double>
		<s:Double>0.2</s:Double>
		<s:Double>0.1</s:Double>
	</ComboBox>
	<Separator Margin="5,0,5,0"/>
	<TextBlock Name="label1" VerticalAlignment="Center" Width="100" 
	Text="{Binding ElementName=lifeView1, Path=Generation, 
	StringFormat=Generation \{0\} }"/>

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

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)
{
    ((LifeView)d).UpdateDimensions();
}

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();
#endregion

public LifeView()
{
    AddVisual(_gridVisual);
    AddVisual(_cellsVisual);
    AddVisual(_adornerVisual);// top of Z layer
}

void AddVisual(Visual child)
{
    this.AddLogicalChild(child);
    this.AddVisualChild(child);
    _visuals.Add(child);
}

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

protected override int VisualChildrenCount
{
    get
    {
        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.");
			else
			{
				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);
	}
}

History

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.

License

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

Share

About the Author

Ron de Jong
Product Manager
Australia Australia
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinmemberNetDave6-Dec-10 18:49 
Questionmouse down event not reaching in my code Pinmemberumeshrameshsatavase13-Oct-10 23:50 
AnswerRe: mouse down event not reaching in my code PinmemberRon de Jong8-Jan-11 12:38 
GeneralMy vote of 5 Pinmemberdbrenth8-Sep-10 10:51 
GeneralMy vote of 5 PinmemberMarcelo Ricardo de Oliveira6-Sep-10 1:42 
GeneralGood article, nice app PinmemberGregory.Gadow3-Sep-10 11:47 
GeneralRe: Good article, nice app PinmemberRon de Jong4-Sep-10 12:30 
GeneralI saw something similar a while back PinmvpSacha Barber2-Sep-10 21:39 
GeneralRe: I saw something similar a while back PinmemberRon de Jong2-Sep-10 22:46 
GeneralRe: I saw something similar a while back PinmemberJaime Olivares7-Sep-10 11:59 
GeneralRe: I saw something similar a while back Pinmemberdojohansen7-Sep-10 22:22 
GeneralRe: I saw something similar a while back PinmemberRon de Jong8-Sep-10 15:05 
QuestionAny particular version dependencies? Pinmembertorial1-Sep-10 16:46 
AnswerRe: Any particular version dependencies? PinmemberIbrahim Yusuf2-Sep-10 5:58 

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 | Mobile
Web04 | 2.8.140827.1 | Last Updated 31 Aug 2010
Article Copyright 2010 by Ron de Jong
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid