Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Creating Life: Conway’s Game of Life

$
0
0

Sometimes even a simple set of basic rules can give you very interesting results. In this tutorial we’ll build the core engine of Conway’s Game of Life from the ground up.

Note: Although this tutorial is written using C# and XNA, you should be able to use the same techniques and concepts in almost any 2D game development environment.


Introduction

Conway’s Game of Life is a cellular automaton that was devised in the 1970s by a British mathematician named, well, John Conway.

Given a two-dimensional grid of cells, with some “on” or “alive” and others “off” or “dead”, and a set of rules that governs how they come alive or die, we can have an interesting “life form” unfold right in front of us. So, by simply drawing a few patterns onto our grid, and then starting the simulation, we can watch basic life forms evolve, spread, die off, and eventually stabilize. Download the final source files, or check out the demo below:

Now, this “Game of Life” is not strictly a “game” – it’s more a machine, mainly because there is no player and no goal, it simply evolves based on its initial conditions. Nonetheless, it’s a lot of fun to play with, and there are many principles of game design that can applied to its creation. So, without further ado, let’s get started!

For this tutorial, I went ahead and built everything in XNA because that’s what I’m most comfortable with. (There’s a guide to getting started with XNA here, if you’re interested.) However, you should be able to follow along with any 2D game development environment that you’re familiar with.


Creating the Cells

The most basic element in Conway’s Game of Life are the cells, which are the “life forms” that form the basis of the entire simulation. Each cell can be in one of two states: “alive” or “dead”. For the sake of consistency, we’ll stick to those two names for the cell states for the rest of this tutorial.

Cells do not move, they simply affect their neighbors based on their current state.

Now, in terms of programming their functionality, there are the three behaviors we need to give them:

  1. They need to keep track of their position, bounds, and state, so they can be clicked and drawn correctly.
  2. They need to toggle between alive and dead when clicked, which allows the user to actually make interesting things happen.
  3. They need to be drawn as white or black if they’re dead or alive, respectively.

All of the above can be accomplished by creating a Cell class, which will contain the code below:

class Cell
{
	public Point Position { get; private set; }
	public Rectangle Bounds { get; private set; }

	public bool IsAlive { get; set; }

	public Cell(Point position)
	{
		Position = position;
		Bounds = new Rectangle(Position.X * Game1.CellSize, Position.Y * Game1.CellSize, Game1.CellSize, Game1.CellSize);

		IsAlive = false;
	}

	public void Update(MouseState mouseState)
	{
		if (Bounds.Contains(new Point(mouseState.X, mouseState.Y)))
		{
			// Make cells come alive with left-click, or kill them with right-click.
			if (mouseState.LeftButton == ButtonState.Pressed)
				IsAlive = true;
			else if (mouseState.RightButton == ButtonState.Pressed)
				IsAlive = false;
		}
	}

	public void Draw(SpriteBatch spriteBatch)
	{
		if (IsAlive)
			spriteBatch.Draw(Game1.Pixel, Bounds, Color.Black);

		// Don't draw anything if it's dead, since the default background color is white.
	}
}

The Grid and Its Rules

Now that each cell is going to behave correctly, we need to create a grid that will hold them all, and implement the logic that tells each one whether it should come alive, stay alive, die, or stay dead (no zombies!).

The rules are fairly simple:

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

Here’s a quick visual guide to these rules in the image below. Each cell highlighted by a blue arrow will be affected by its corresponding numbered rule above. In other words, cell 1 will die, cell 2 will stay alive, cell 3 will die, and cell 4 will come alive.

So, as the game simulation runs an update at constant time intervals, the grid will check each of these rules for all of the cells in the grid. That can be accomplished by putting the following code in a new class I’ll call Grid:

class Grid
{
	public Point Size { get; private set; }

	private Cell[,] cells;

	public Grid()
	{
		Size = new Point(Game1.CellsX, Game1.CellsY);

		cells = new Cell[Size.X, Size.Y];

		for (int i = 0; i < Size.X; i++)
			for (int j = 0; j < Size.Y; j++)
				cells[i, j] = new Cell(new Point(i, j));
	}

	public void Update(GameTime gameTime)
	{
		(...)

		// Loop through every cell on the grid.
		for (int i = 0; i < Size.X; i++)
		{
			for (int j = 0; j < Size.Y; j++)
			{
				// Check the cell's current state, and count its living neighbors.
				bool living = cells[i, j].IsAlive;
				int count = GetLivingNeighbors(i, j);
				bool result = false;

				// Apply the rules and set the next state.
				if (living && count < 2)
 					result = false;
 				if (living && (count == 2 || count == 3))
 					result = true;
 				if (living && count > 3)
					result = false;
				if (!living && count == 3)
					result = true;

				cells[i, j].IsAlive = result;
			}
		}
	}

	(...)

}

The only thing we’re missing from here is the magic GetLivingNeighbors method, which simply counts how many of the current cell’s neighbors are currently alive. So, let’s add this method to our Grid class:

public int GetLivingNeighbors(int x, int y)
{
	int count = 0;

	// Check cell on the right.
	if (x != Size.X - 1)
		if (cells[x + 1, y].IsAlive)
			count++;

	// Check cell on the bottom right.
	if (x != Size.X - 1 && y != Size.Y - 1)
		if (cells[x + 1, y + 1].IsAlive)
			count++;

	// Check cell on the bottom.
	if (y != Size.Y - 1)
		if (cells[x, y + 1].IsAlive)
			count++;

	// Check cell on the bottom left.
	if (x != 0 && y != Size.Y - 1)
		if (cells[x - 1, y + 1].IsAlive)
			count++;

	// Check cell on the left.
	if (x != 0)
		if (cells[x - 1, y].IsAlive)
			count++;

	// Check cell on the top left.
	if (x != 0 && y != 0)
		if (cells[x - 1, y - 1].IsAlive)
			count++;

	// Check cell on the top.
	if (y != 0)
		if (cells[x, y - 1].IsAlive)
			count++;

	// Check cell on the top right.
	if (x != Size.X - 1 && y != 0)
		if (cells[x + 1, y - 1].IsAlive)
			count++;

	return count;
}

Note that in the above code, the first if statement of each pair is simply checking that we’re not on the edge of the grid. If we didn’t have this check, we would run into several Exceptions from exceeding the bounds of the array. Also, since this will lead to count never being incremented when we’re checking past the edges, that means the game “assumes” edges are dead, so it’s equivalent to having a permanent border of white, dead cells around our game windows.


Updating the Grid in Discrete Time-Steps

So far, all of the logic that we’ve implemented is sound, but it won’t behave properly if we’re not careful to make sure our simulation runs in discrete time steps. This is just a fancy way of saying that all of our cells will be updated at the exact same time, for the sake of consistency. If we didn’t implement this, we would get strange behavior because the order in which the cells were checked would matter, so the strict rules we just set would fall apart and mini-chaos would ensue.

For example, our loop above checks all the cells from left to right, so if the cell on the left we just checked came alive, this would change the count for the cell in the middle we’re now checking and might make it come alive. But, if we were checking from right to left instead, the cell on the right might be dead, and the cell on the left hasn’t come alive yet, so our middle cell would stay dead. This is bad because it’s inconsistent! We should be able to check the cells in any random order we want (like a spiral!) and the next step should always be identical.

Luckily, this is really quite easy to implement in code. All we need is to have a second grid of cells in memory for the next state of our system. Every time we determine the next state of a cell, we store it in our second grid for the next state of the whole system. Then, when we’ve found the next state of every cell, we apply them all at the same time. So we can add a 2D array of booleans nextCellStates as a private variable, and then add this method to the Grid class:

public void SetNextState()
{
	for (int i = 0; i < Size.X; i++)
		for (int j = 0; j < Size.Y; j++)
			cells[i, j].IsAlive = nextCellStates[i, j];
}

Finally, don’t forget to fix your Update method above so it assigns the result to the next state rather than the current one, then call SetNextState at the very end of the Update method, right after the loops complete.


Drawing the Grid

Now that we’ve finished the trickier parts of the grid’s logic, we need to be able to draw it onto the screen. The grid will draw each cell by calling their draw methods one at a time, so that all living cells will be black, and the dead ones will be white.

The actual grid doesn’t need to draw anything, but it’s much clearer from the user’s perspective if we add some grid lines. This allows the user to more easily see cell boundaries, and also communicates a sense of scale, so let’s create a Draw method as follows:

public void Draw(SpriteBatch spriteBatch)
{
	foreach (Cell cell in cells)
		cell.Draw(spriteBatch);

	// Draw vertical gridlines.
	for (int i = 0; i < Size.X; i++)
		spriteBatch.Draw(Game1.Pixel, new Rectangle(i * Game1.CellSize - 1, 0, 1, Size.Y * Game1.CellSize), Color.DarkGray);

	// Draw horizontal gridlines.
	for (int j = 0; j < Size.Y; j++)
		spriteBatch.Draw(Game1.Pixel, new Rectangle(0, j * Game1.CellSize - 1, Size.X * Game1.CellSize, 1), Color.DarkGray);
}

Note that in the above code, we’re taking a single pixel and stretching it to create a very long and thin line. Your particular game engine might provide a simple DrawLine method where you can specify two points and have a line by drawn between them, which would make it even easier than the above.


Adding High-Level Game Logic

At this point, we’ve got all the basic pieces we need to make the game run, we just need to bring it all together. So, for starters, in your game’s main class (the one that starts everything), we need to add a few constants like the dimensions of the grid and the framerate (how quickly it will update), and all the other things we need like the single pixel image, the screen size, and so on.

We also need to initialize many of these things, like creating the grid, setting the window size for the game, and making sure the mouse is visible so we can click on cells. But all of these things are engine-specific and not very interesting, so we’ll skip right over it and get to the good stuff. (Of course, if you’re following along in XNA, you can download the source code to get all the details.)

Now that we’ve got everything set up and ready to go, we should be able to just run the game! But not so fast, because there’s a problem: we can’t really do anything because the game is always running. It’s basically impossible to draw specific shapes because they’ll break apart as you draw them, so we really need to be able to pause the game. It would also be nice if we could clear the grid if it becomes a mess, because our creations will often grow out of control and leave a mess behind.

So, let’s add some code to pause the game whenever the spacebar is pressed, and clear the screen if backspace is pressed:

protected override void Update(GameTime gameTime)
{
	keyboardState = Keyboard.GetState();

	if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
		this.Exit();

	// Toggle pause when spacebar is pressed.
	if (keyboardState.IsKeyDown(Keys.Space) && lastKeyboardState.IsKeyUp(Keys.Space))
		Paused = !Paused;

	// Clear the screen if backspace is pressed.
	if (keyboardState.IsKeyDown(Keys.Back) && lastKeyboardState.IsKeyUp(Keys.Back))
		grid.Clear();

	base.Update(gameTime);

	grid.Update(gameTime);

	lastKeyboardState = keyboardState;
}

It would also help if we made it very clear that the game was paused, so as we write our Draw method, let’s add some code to make the background go red, and write “Paused” in the background:

protected override void Draw(GameTime gameTime)
{
	if (Paused)
		GraphicsDevice.Clear(Color.Red);
	else
		GraphicsDevice.Clear(Color.White);

	spriteBatch.Begin();
	if (Paused)
	{
		string paused = "Paused";
		spriteBatch.DrawString(Font, paused, ScreenSize / 2, Color.Gray, 0f, Font.MeasureString(paused) / 2, 1f, SpriteEffects.None, 0f);
	}
	grid.Draw(spriteBatch);
	spriteBatch.End();

	base.Draw(gameTime);
}

That’s it! Everything should now be working, so you can give it a whirl, draw some life forms and see what happens! Go and explore interesting patterns you can make by referring to the Wikipedia page again. You can also play with the framerate, cell size and grid dimensions to tweak it to your liking.


Adding Improvements

At this point, the game is fully functional and there’s no shame in calling it a day. But one annoyance you might have noticed is that your mouse clicks don’t always register when you’re trying to update a cell, so when you click and drag your mouse across the grid it’ll leave a dotted line behind rather than a solid one. This happens because the rate at which the cells update is also the rate at which the mouse is being checked, and it’s far too slow. So, we simply need to decouple the rate at which the game updates, and the rate at which is reads input.

Start by defining the update rate and the framerate separately in the main class:

public const int UPS = 20; // Updates per second
public const int FPS = 60;

Now, when initializing the game, use the framerate (FPS) to define how quickly it’ll read mouse input and draw, which should be a nice smooth 60 FPS at the very least:

IsFixedTimeStep = true;
TargetElapsedTime = TimeSpan.FromSeconds(1.0 / FPS);

Then, add a timer to your Grid class, such that it will only update when it needs to, independently of the framerate:

public void Update(GameTime gameTime)
{
	(...)

	updateTimer += gameTime.ElapsedGameTime;

	if (updateTimer.TotalMilliseconds > 1000f / Game1.UPS)
	{
		updateTimer = TimeSpan.Zero;

		(...) // Update the cells and apply the rules.

	}

}

Now, you should be able to run the game at whatever speed you want, even a very slow 5 updates per second so you can carefully watch your simulation unfold, while still being able to draw nice smooth lines at a solid framerate.


Conclusion

You now have a smooth and functional Game of Life on your hands, but in case you want to explore it further, there’s always more tweaks you can add to it. For example, the grid currently assumes that beyond its edges, everything is dead. You could modify it such that the grid wraps around, so a glider would fly forever! There’s no shortage of variations on this popular game, so let your imagination run wild.

Thanks for reading, I hope you’ve learnt some useful things today!


Viewing all articles
Browse latest Browse all 728

Trending Articles