Game of Life in Uno Platform

This article is part of the fourth annual C# Advent by Matthew D. Groves and Calvin A. Allen. Thank you for organizing this!

The Game of Life is an interesting yet simulation of the evolution of living cells, that was devised by British mathematician John Conway in 1970. A few months ago I saw a blog post by Khalid Abuhakmeh with a C# console implementation, which was then followed up by a Blazor version by Matthew Jones.

And today we will build the Game of Life in C# and XAML with Uno Platform, and make it run everywhere – on Windows (10, 7), Android, iOS, macOS, web, Linux and Tizen!

Rules of Game of Life

The Game of Life consists of a 2D grid of squares, each representing a “cell” which is either alive or dead.

Example configuration of Game of Life
Example configuration of Game of Life

After setting an initial configuration, the game unfolds through steps called generations. In each generation, the cells shift between the alive and dead states based on four simple rules:

  1. Any alive cell with less than two alive neighbors dies of underpopulation
  2. Any alive cell with two or three alive neighbors remains alive
  3. Any alive cell with more than three alive neighbors dies of overpopulation
  4. Any dead cell with exactly three alive neighbors becomes alive by reproduction

For example see the following example:

Initial configuration
Initial configuration

The top-left cell is dead but has exactly three alive neighbors. Applying rule 4 it will become alive in the next generation. The second cell on the top row is alive, but has four alive neighbors – this means it will die of overpopulation as per rule 3. The third cell on the first row is alive and will stay alive, as it has three alive neighbors, rule 2. Finally, the last cell on the last row has no alive neighbors, hence it will die of underpopulation by rule 1.  Once we apply the rules to all cells, the next configuration will look like this:

After one generational step
After one generational step

Now that we know how it works, let’s implement it!

Creating the solution

I have installed the Uno Platform Solution Templates for Visual Studio, which gives me the Cross-Platform App template.

Creating new Uno Platform project
Creating new Uno Platform project

This creates a new solution with all the platform targets that Uno Platform supports. As I am using the latest and greatest version of Uno Platform templates (3.3), it has Android, iOS, macOS, GTK, Tizen, WPF, UWP and WebAssembly support.

That's a lotta platforms!
That’s a lotta platforms!

All our code will go in the Shared project, as there will be nothing platform-specific.

Game logic

Let’s first implement the game logic. As described, each cell has two possible states – dead and alive. We could use simple binary numbers, but for better readability, let’s use an enum:

public enum CellState
{
Dead,
Alive
}

view raw
CellState.cs
hosted with ❤ by GitHub

Now we need a class to represent the game state. The 2D grid is best represented by a 2D array:

public class GameState
{
public GameState(int size)
{
Cells = new CellState[size, size];
Size = size;
}
public int Size { get; }
public CellState[,] Cells { get; set; }
}

view raw
GameState.cs
hosted with ❤ by GitHub

To make generating an initial configuration easy, let’s add a Randomize method:

private readonly Random _random = new Random();
public void Randomize()
{
for (int row = 0; row < Size; row++)
{
for (int column = 0; column < Size; column++)
{
Cells[row, column] = (CellState)_random.Next(0, 2);
}
}
}

This simply generates a random cell state for each cell of our grid.

Let’s implement the transition from one generation to another:

private CellState[,] _nextGeneration = null;
public void Tick()
{
_nextGeneration = _nextGeneration ?? new CellState[Size, Size];
for (int row = 0; row < Size; row++)
{
for (int column = 0; column < Size; column++)
{
var aliveNeighbors = CountAliveNeighbors(row, column);
var currentState = Cells[row, column];
CellState nextState = Cells[row, column];
if (currentState == CellState.Alive && aliveNeighbors < 2)
{
// Rule 1 – Underpopulation
nextState = CellState.Dead;
}
else if (
currentState == CellState.Alive &&
(aliveNeighbors == 2 || aliveNeighbors <= 3))
{
// Rule 2
nextState = CellState.Alive;
}
else if (currentState == CellState.Alive && aliveNeighbors > 3)
{
// Rule 3 – Overpopulation
nextState = CellState.Dead;
}
else if (currentState == CellState.Dead && aliveNeighbors == 3)
{
// Rule 4 – Reproduction
nextState = CellState.Alive;
}
_nextGeneration[row, column] = nextState;
}
}
// Swap!
(Cells, _nextGeneration) = (_nextGeneration, Cells);
}
private int CountAliveNeighbors(int row, int column)
{
int aliveCount = 0;
for (var rowOffset = 1; rowOffset <= 1; rowOffset++)
{
for (var columnOffset = 1; columnOffset <= 1; columnOffset++)
{
if (rowOffset == 0 && columnOffset == 0)
{
continue;
}
var targetRow = row + rowOffset;
var targetColumn = column + columnOffset;
if (targetRow >= 0 && targetColumn >= 0 &&
targetRow < Size && targetColumn < Size)
{
aliveCount += (int)Cells[targetRow, targetColumn];
}
}
}
return aliveCount;
}

view raw
GameState.Tick.cs
hosted with ❤ by GitHub

The Tick method uses a second 2D array _nextGeneration to construct the next state of the Game of Life. To avoid allocating a new block of memory each time Tick is called, we keep the second array and reuse it next time.

For each cell, we first calculate the number of alive neighbors using CountAliveNeighbors. This simply looks at all eight neighbors of the cell (by testing offsets -1, 0 and 1 for both coordinates, and skipping the tested cell itself) and adding up the values. Because our enum has value 0 for Dead and 1 for alive, we can simply sum all the neighbors to get our alive count.

With the number of alive neighbors in hand, we can apply our four rules and then store the next state in the _nextGeneration array.

Finally, we swap Cells and _nextGeneration. The code swaps using the cool value tuple syntax, which is basically equivalent to:

var swap = Cells;
Cells = _nextGeneration;
_nextGeneration = swap;

view raw
Swap.cs
hosted with ❤ by GitHub

And that’s the whole logic! Now let’s build our UI.

Building the UI

Let’s start working on the user interface. What will we need?

  • Button to start the game
  • Button to “clear” the board (e.g. set all cells to dead state)
  • Button to advance to the next generation
  • Number box to choose the game board size
  • Toggle to switch auto-play on and off
  • Game canvas

Based on these requirements, here is a simple XAML layout:

<Page
xmlns:controls="using:Microsoft.UI.Xaml.Controls">
<Grid
Padding="12"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
FontSize="20"
Style="{ThemeResource HeaderTextBlockStyle}"
Text="Conway's Game of Life"
TextAlignment="Center" />
<Grid
Grid.Row="1"
ColumnSpacing="8"
RowSpacing="8"
HorizontalAlignment="Left">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
HorizontalAlignment="Stretch"
Click="{x:Bind StartNewGame}"
Content="Start new game" />
<Button
Grid.Column="1"
HorizontalAlignment="Stretch"
Click="{x:Bind Clear}"
Content="Clear" />
<Button
Grid.Column="2"
Click="{x:Bind NextGeneration}"
Content="Next generation" />
<controls:NumberBox
x:Name="BoardSizeNumberBox"
Maximum="50"
Minimum="3"
Header="Board size"
SpinButtonPlacementMode="Inline"
Value="10" />
<ToggleSwitch
x:Name="AutoPlayToggleSwitch"
Grid.Row="1"
Grid.Column="2"
Header="Auto-play"
Toggled="{x:Bind AutoPlayToggled}" />
</Grid>
<Border
x:Name="GameCanvasContainer"
Grid.Row="2"
SizeChanged="{x:Bind LayoutGameBoard}">
<Canvas x:Name="GameCanvas" />
</Border>
</Grid>
</Page>

view raw
MainPage.xaml
hosted with ❤ by GitHub

You may notice I have already included several x:Bind statements there, which we will wire up in a moment. Please remove them from your code for now, and re-add them gradually as we implement the required methods. For now, the design looks as follows:

Our simple UI
Our simple UI

Code-behind

The last piece of our puzzle is to implement the code-behind. First, the StartNewGame method:

private GameState _gameState;
private void StartNewGame()
{
var boardSize = (int)BoardSizeNumberBox.Value;
if (_gameState?.Size != boardSize)
{
_gameState = new GameState(boardSize);
PrepareGrid();
LayoutGameBoard();
}
_gameState.Randomize();
RedrawBoard();
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

The method checks if we are trying to start a new game with new board size. In such case, it creates a new GameState accordingly and prepares the grid and layout. Afterwards, it randomizes the game and renders it. Let’s look at the missing methods in turn.

PrepareGrid will initialize the required Image elements that will be used to render the 2D grid of cells:

private readonly List<Image> _cells = new List<Image>();
private void PrepareGrid()
{
GameCanvas.Children.Clear();
for (var row = 0; row < _gameState.Size; row++)
{
for (var column = 0; column < _gameState.Size; column++)
{
var cell = GetCell(row, column);
cell.Source = _gameState.Cells[row, column] == CellState.Alive ?
_aliveBitmap : _deadBitmap;
GameCanvas.Children.Add(cell);
}
}
}
private Image GetCell(int row, int column)
{
var index = row * _gameState.Size + column;
return GetCell(index);
}
private Image GetCell(int order)
{
while (_cells.Count <= order)
{
_cells.Add(CreateCell());
}
return _cells[order];
}
private Image CreateCell() => new Image();

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

We use List<Image> to cache the cell images and create them on demand when a bigger board is needed.

The LayoutGameBoard method takes the Image elements and lays them out into a regular 2D using Canvas absolute positioning:

private void LayoutGameBoard()
{
var minDimension = Math.Min(GameCanvasContainer.ActualHeight, GameCanvasContainer.ActualWidth);
var cellSize = (int)minDimension / _gameState.Size;
// make cell size a multiple of 4 for proper scaling
cellSize = (cellSize / 4) * 4;
if (cellSize <= 0)
{
cellSize = 4;
}
GameCanvas.Height = GameCanvas.Width = cellSize * _gameState.Size;
for (var row = 0; row < _gameState.Size; row++)
{
for (var column = 0; column < _gameState.Size; column++)
{
var cell = GetCell(row, column);
cell.SetValue(Canvas.LeftProperty, (double)(column * cellSize));
cell.SetValue(Canvas.TopProperty, (double)(row * cellSize));
cell.Height = cellSize 1;
cell.Width = cellSize 1;
}
}
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

We leave a one-pixel space between the cells, setting their actual size to cellSize-1.

Finally, let’s implement the RedrawBoard method:

private BitmapImage _aliveBitmap = new BitmapImage(new Uri($"ms-appx:///Assets/Classic_alive.png"));
private BitmapImage _deadBitmap = new BitmapImage(new Uri($"ms-appx:///Assets/Classic_dead.png"));
private void RedrawBoard()
{
for (var row = 0; row < _gameState.Size; row++)
{
for (var column = 0; column < _gameState.Size; column++)
{
var cell = GetCell(row, column);
cell.Source = _gameState.Cells[row, column] == CellState.Alive ?
_aliveBitmap : _deadBitmap;
}
}
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

This simple method sets the Image.Source for each cell based on its current state. I have added two simple solid square png images in the Assets folder in the Shared project:

Assets
Assets

If you don’t want to create your own images, you can download them from my sample project here on GitHub.

We can now launch the app and try out the Start new game button. We can even choose a custom game board size!

Start new game
Start new game

The Clear button is quite simple and requires just a single method Clear:

private void Clear()
{
_gameState?.Clear();
RedrawBoard();
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

The GameState.Clear method only sets all cells to “dead” state:

public void Clear()
{
for (int row = 0; row < Size; row++)
{
for (int column = 0; column < Size; column++)
{
Cells[row, column] = CellState.Dead;
}
}
}

view raw
GameState.cs
hosted with ❤ by GitHub

The Next generation button is super simple:

private void NextGeneration()
{
_gameState?.Tick();
RedrawBoard();
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

The GameState class handles the logic and we just redraw the game board afterwards. Easy-peasy!

Next generation
Next generation

Auto-play

The auto-play toggle will automatically step through generations in one-second intervals when turned on.

private readonly DispatcherTimer _timer = new DispatcherTimer()
{
Interval = TimeSpan.FromSeconds(1)
};
private void AutoPlayToggled()
{
if (AutoPlayToggleSwitch.IsOn)
{
_timer.Start();
}
else
{
_timer.Stop();
}
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

When we toggle the ToggleSwitch on, we start the timer, otherwise, we stop it.

We attach the Tick event handler in the page constructor. The handler just calls GameState to advance to next generation and redraws.

public MainPage()
{
this.InitializeComponent();
_timer.Tick += OnTimerTick;
}
private void OnTimerTick(object sender, object e)
{
_gameState?.Tick();
RedrawBoard();
}

view raw
gistfile1.txt
hosted with ❤ by GitHub

Auto-play enabled
Auto-play enabled

Tapping on cells

To make the game more interactive for the user, we can allow toggling the state of the cells by tapping them – essentially allowing the modification of state mid-game.

private Image CreateCell()
{
var cell = new Image();
cell.Tapped += OnCellTapped;
return cell;
}
private void OnCellTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
var index = _cells.IndexOf((Image)sender);
int row = index / _gameState.Size;
int column = index % _gameState.Size;
_gameState.Cells[row, column] = _gameState.Cells[row, column] == CellState.Alive ?
CellState.Dead : CellState.Alive;
RedrawBoard();
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

Notice we use the index of the Image in our cache and calculate its row and column based on the result.

Adding themes

To make things more interesting, we will introduce themes to switch between various images to display. First, let’s make an enum with supported themes:

public enum GameTheme
{
Classic,
CatMouse,
Bacteria,
FireWater
}

view raw
GameTheme.cs
hosted with ❤ by GitHub

In the main page’s code-behind, we will expose the list of themes and create a property for the currently selected theme:

private GameTheme _currentTheme = GameTheme.Classic;
public GameTheme[] Themes { get; } = new GameTheme[]
{
GameTheme.Classic,
GameTheme.Bacteria,
GameTheme.CatMouse,
GameTheme.FireWater
};
public GameTheme CurrentTheme
{
get => _currentTheme;
set
{
_currentTheme = value;
UpdateBitmaps();
RedrawBoard();
}
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

The UpdateBitmaps method will load the bitmaps accordingly, based on the enum value:

private void UpdateBitmaps()
{
_aliveBitmap = new BitmapImage(new Uri($"ms-appx:///Assets/{_currentTheme}_alive.png"));
_deadBitmap = new BitmapImage(new Uri($"ms-appx:///Assets/{_currentTheme}_dead.png"));
}

view raw
MainPage.xaml.cs
hosted with ❤ by GitHub

Finally, let’s add a ComboBox to our UI to allow user selection:

<ComboBox
x:Name="ThemeComboBox"
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Stretch"
Header="Theme"
ItemsSource="{x:Bind Themes}"
SelectedItem="{x:Bind CurrentTheme, Mode=TwoWay}" />

view raw
MainPage.xaml
hosted with ❤ by GitHub

And voilà, we can now switch between themes! In my sample, I am using beautiful icons made by Icons8, thanks!

Theming in action
Theming in action

And it runs everywhere

Of course, the ultimate beauty of all this is that thanks to Uno Platform, this app will now run absolutely everywhere – mobile, web, desktop, you name it. And because Uno supports both dark and light theme, it respects the OS theme on each device too! Note: SkiaSharp-based rendering is still in preview, so NumberBox value rendering is not yet working. This is coming soon!

This slideshow requires JavaScript.

And of course, here it is also running in the Tesla browser ⚡ :

Game of Life on Tesla
Game of Life on Tesla

Live demo

The resulting app in Uno Platform WebAssembly running on .NET 5 and Microsoft Azure is available here!

Fun shapes

Now that the app is complete, we can play with different configurations of the Game of Life and discover fun patterns. For example, let’s build a spaceship:

Spaceship, spaceship, spaceship!
Spaceship, spaceship, spaceship!

Or if you like something more elaborate – a pulsar:

Pulsar
Pulsar

You can search the internet for many more interesting patterns and combinations or unleash your own creativity.

Try it out on the live demo and share your creations on Twitter, using hashtag #unolife 🚀

Source code

The sample code for this article is available on my GitHub. Feel free to clone, fork and update any way you like!

Buy me a coffeeBuy me a coffee

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.