Conways game of life on a game engine
Conways game of life Is a game or simulation with the aim to create interisting results.
It is played on a 2d grid full of cells, where cells can either be alive or dead.
There are only a few simple rules that dictate the state of a cell:
- Any live cell with fewer than two live neighbours dies
- Any live cell with more than three live neighbours dies
- Any live cell with two or three live neighbours lives on
- Any dead cell with exactly three live neighbours comes alive
Let’s implement this using the game engine Ebitengine, which is a simple 2D game engine for GO.
EBitengine is open source, written in pure GO, and multiplatform.
It works by creating an instance that conforms to the following interface:
type Game string {}
func (g *Game) Update() error {
// game logic
}
func (g *Game) Draw(screen *ebiten.Image) {
// rendering
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
{
// return the game logical screen size
}
The update function is where our game logic will run, calculating and keeping track of our cell states.
The game loop runs the update function 60 times a second and the draw function is called the number of times matching your refresh rate, in my case 60hz.
The problem
To create the game of life, we need to create a grid of cells, store their states, calculate and update the states on every update and then render the grid of cells.
To create the grid we will create a multi dimensional array or slice in GO to store the cell states:
var gridSize = 100
func createGrid() [][]bool {
grid := make([][]bool, gridSize)
for i := range grid {
grid[i] = make([]bool, gridSize)
}
// Blinker pattern
grid[2][1] = true
grid[2][2] = true
grid[2][3] = true
return grid
}
We are setting our grid to 100x100, where the array stores rows and columns, and using a boolean, true for alive or false for dead.
We have seeded the grid with a blinker pattern, experiement to find interesting patterns as that is infact the game.
We can add this to our Game struct allowing us to access it in the update function.
func (g *Game) Update() error {
newGrid := make([][]bool, len(g.Grid))
for i := 0; i < len(g.Grid); i++ {
newGrid[i] = make([]bool, len(g.Grid[i]))
for j := 0; j < len(g.Grid[i]); j++ {
newGrid[i][j] = GetState(g.Grid, i, j)
}
}
g.Grid = newGrid
return nil
}
Here we are creating a new grid, looping through each row and column and calling our GetState function to calculate the new cell state.
func GetState(grid [][]bool, x, y int) bool {
count := LiveCount(grid, x, y)
// should die
if count < 2 {
return false
}
if count > 3 {
return false
}
// should be reborn
if count == 3 && !grid[x][y] {
return true
}
// should live on
if count == 2 || count == 3 {
return true
}
// remain
return grid[x][y]
}
GetState calls our LiveCount function which calculates the live neighbour count for a cell position and then applies the game rules.
func LiveCount(grid [][]bool, x, y int) int {
liveCount := 0
xsize := len(grid)
ysize := len(grid[0])
var fromX, toX, fromY, toY int
if x == 0 {
fromX = 0
} else {
fromX = x - 1
}
if x == xsize-1 {
toX = xsize - 1
} else {
toX = x + 1
}
if y == 0 {
fromY = 0
} else {
fromY = y - 1
}
if y == ysize-1 {
toY = ysize - 1
} else {
toY = y + 1
}
for i := fromX; i <= toX; i++ {
for j := fromY; j <= toY; j++ {
if i == y && j == x {
continue
} else if grid[i][j] {
liveCount++
}
}
}
return liveCount
}
I am sure there are more efficient ways to implement this algorithm but this was easiest for me to get my head around. We work out the beginning and end indexes for our row and column position, some cells may not have neighbouring cells in either direction.
With our game logic done we now need to implement draw.
func (g *Game) Draw(screen *ebiten.Image) {
for i := 0; i < gridSize; i++ {
for j := 0; j < gridSize; j++ {
if g.Grid[i][j] {
vector.DrawFilledCircle(screen, float32(j*int(g.sectionSize)), float32(i*int(g.sectionSize)), float32(g.sectionSize/2), getColor(g.Grid[i][j]), false)
}
}
}
}
Draw is actually very straightforward, loop through our rows and columns drawing a circle for each cell, alive cells are white and dead cells are black.
There is an example of the game of life on the ebitengine website.