Introduction
Game of Life is a cellular automation invented by John Horton Conway in 1970. It's a zero player game of evolution. What if we took this game and let it live on a server, continuosly thus allowing two people observe the same game board, even if they're on different continents. That's the idea behind this article: create a persistent, almost "MMO"-like simulation on the server and allow clients to observe and render this simulation remotely.
Background
Let's start with a short summary explaining that this article accomplishes, and its main highlights:
- The game of life simulation runs continuously on the server. The grid then broadcasted to every client who is "observing" the game.
- The simulation introduces random mutations and board randomizations so it can run continusly without getting too boring.
- The rendering is built using javascript Obelisk.js library.
- It uses websockets internally, but abstracted by Spike Engine.
- The application server is a self-hosted executable and the client is just a plain html file.
Since the simulation runs on the server and rendered by the clients, we need to split the roles and what each node will do. In our case:
- Server is responsible for entire simulation execution, from one generation to another.
- Server is also rseponsible for managing a list of observers of the game world and for periodically senging the state of the game world to the observers.
- Clients (or observers) are responsible for joining/leaving the server and rendering the game world they receive.
The following figure illustrates the process:
Server-Side Implementation
Let's begin by examining the definition that represents the process of client-server communication. We have 3 operations:
JoinGameOfLife
: called by the observer to join the game. This tells the server to start sending updates to that particular client. LeaveGameOfLife
: called by the observer to leave the game. This tells the server to stop sending updates. NewGeneration
: initiated by the server, hence Direction="Push"
, simply send the grid of cells to the client. This grid is filled with zeros and/or ones (binary matrix) and represents live or empty cells of the map.
="1.0"="UTF-8"
<Protocol Name="MyGameOfLifeProtocol" xmlns="http://www.spike-engine.com/2011/spml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Operations>
<Operation Name="JoinGameOfLife" Direction="Pull" SuppressSecurity="true"></Operation>
<Operation Name="LeaveGameOfLife" Direction="Pull" SuppressSecurity="true"></Operation>
<Operation Name="NewGeneration"
Direction="Push"
SuppressSecurity="true">
<Outgoing>
<Member Name="Grid" Type="ListOfInt16" />
</Outgoing>
</Operation>
</Operations>
</Protocol>
We are not going to go through the actual implementation of the game of life, as it's just one of many and it's pretty straightforward. However, we added a couple of interesting modifications to spice up the simulation and a couple of nice performance tricks to speed up things. If you look at the snippet of code below, the function UpdateCell is the one responsible for updating a single cell of the field. If at least one cell is changed, we mark the entire generation as changed.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateCell(int i, int j)
{
var oldState = GetAt(this.FieldOld, i, j);
var neighbors = CountNeighbours(this.FieldOld, i, j);
this.Field[i * FieldSize + j] =
(short) (neighbors == 2 ? oldState : (neighbors == 3 ? 1 : 0));
if (this.Field[i * FieldSize + j] != oldState)
this.FieldChange = true;
}
During the update, we also do two additional things:
- If the field wasn't changed, which can happen quite often in the game of life, we randomize the board again and reinitialize it. This allows us to run the simulation forever and automatically restart the simulation if there's no more living cells, for example.
- Each generation got 5% of chance to create some random mutations. Once a single mutation have occured, we have 95% probability to have more mutation within the same generation. This allows us to "break" stable structures in the game of life, spicing things up.
private void Mutate()
{
if(!this.FieldChange)
this.Randomize();
var probability = 0.05;
while (Dice.NextDouble() < probability)
{
var x = Dice.Next(0, this.FieldSize);
var y = Dice.Next(0, this.FieldSize);
probability = 0.95;
this.Field[x * FieldSize + y] =
(short)(this.Field[x * FieldSize + y] == (short)1 ? 0 : 1);
}
}
Now that we have the simulation implemented, how do we actually connect the simulation to our networking backend? Join and leave operations are pretty straightforward and they simply put/remove an IClient instance to/from an IList<IClient>. We also start a game loop using Spike.Timer which handles all the threading for us. It is important to notice that if you have several timers, they will all share the same thread, avoiding performance problems such as oversubscription. The speed of game loop itself can be adjusted here, on the piece of code below we call it every 50 milliseconds and in our live demo it's set to 200 milliseconds.
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
MyGameOfLifeProtocol.JoinGameOfLife += OnJoinGame;
MyGameOfLifeProtocol.LeaveGameOfLife += OnLeaveGame;
Timer.PeriodicCall(TimeSpan.FromMilliseconds(50), OnTick);
}
The game loop does pretty much that you would expect. It updates the game of life, performing the simulation and then sends the grid (32 by 32 binary matrix) to every client. We defined in both, our protocol and our Game class the same matrix to be a IList<Int16>. So we simply pass that list to the send method, without doing any conversion at all.
private static void OnTick()
{
World.Update();
lock (Observers)
{
foreach (var observer in Observers)
observer.SendNewGenerationInform(World.World);
}
}
Client-Side Implementation
Let's examine now the client side. The client needs to connect to the server and join the game, we also need to hook newGenerationInform event which will be invoked every time we receive a new grid from the server. Once we receive a grid, we copy it from an Array to an Int8Array and draw it.
$(document).ready(function () {
var server = new spike.ServerChannel("127.0.0.1:8002");
server.on('connect', function () {
server.joinGameOfLife();
server.on('newGenerationInform', function (p) {
var field = new Int8Array(gridSize * gridSize);
for (var i = 0; i < gridSize * gridSize; ++i)
field[i] = p.grid[i];
render(field);
});
});
});
We have used a rendering engine called Obelisk.js to render our isometric blocks and inspired by the work of @Safx, implementing the game of life in javascript. However, we do not have any game of life-related logic in our client. We simply have a render function that draws a Int8Array grid we receive from the server. Since the server pushes the data, we do not even have to have a render loop and simply redraw all the elements of our canvas on every corresponding receive.
function render(field) {
pixelView.clear();
var boardColor = new obelisk.CubeColor().getByHorizontalColor(obelisk.ColorPattern.GRAY);
var p = new obelisk.Point3D(cubeSide / 2, cubeSide / 2, 0);
var cube = new obelisk.Cube(boardDimension, boardColor, false);
pixelView.renderObject(cube, p);
for (var i = 0; i < gridSize; ++i) {
for (var j = 0; j < gridSize; ++j) {
var z = field[i * gridSize + j];
if (z == 0) continue;
var color = new obelisk.CubeColor().getByHorizontalColor((i * 8) << 16 | (j * 8) << 8 | 0x80);
var p = new obelisk.Point3D(cubeSide * i, cubeSide * j, 0);
var cube = new obelisk.Cube(dimension, color, false);
pixelView.renderObject(cube, p);
}
}
}
I hope you liked this article, please check out other Spike Engine articles we've written and feel free to contribute!
History
- 23/06/2015 - Source code & article updated to Spike v3
- 19/06/2014 - Initial Version