Conways game of life on a game engine

Conways game of life

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:

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.

© 2024 Timney.