🏠 Home page > 🌖 LÖVE tutorials

Life

A tutorial for Lua and LÖVE 11

Download life.love

Rules

There is a grid of cells, which are either alive or dead.

After a step of time:

All other cells die or remain dead.

Create an initial configuration of cells, hold any key to step forward in time, and observe.

Controls

Left clickMake cell alive
Right clickMake cell dead
Any keyStep forward in time

Overview

The cells in the grid are stored as boolean values: true for alive, false for dead.

When time steps forward, a new grid is created, and whether the cells of this new grid are alive or dead is based on the current grid.

After the new grid is complete, the current grid is replaced by the new grid.

Coding

Drawing a cell

A cell is drawn as a square.

Full code at this point

function love.draw()
    love.graphics.rectangle(
        'fill',
        0,
        0,
        4,
        4
    )
end

Drawing a row of cells

A row of cells is drawn, with 1 pixel between each cell.

Full code at this point

function love.draw()
    for x = 1, 70 do
        local cellSize = 5
        local cellDrawSize = cellSize - 1

        love.graphics.rectangle(
            'fill',
            (x - 1) * cellSize,
            0,
            cellDrawSize,
            cellDrawSize
        )
    end
end

Drawing all the cells

All of the rows are drawn.

Full code at this point

function love.draw()
    for y = 1, 50 do
        for x = 1, 70 do
            local cellSize = 5
            local cellDrawSize = cellSize - 1

            love.graphics.rectangle(
                'fill',
                (x - 1) * cellSize,
                (y - 1) * cellSize,
                cellDrawSize,
                cellDrawSize
            )
        end
    end
end

Setting colors

The background and dead cell colors are set.

Full code at this point

function love.load()
    love.graphics.setBackgroundColor(1, 1, 1)
end

function love.draw()
    for y = 1, 50 do
        for x = 1, 70 do
            local cellSize = 5
            local cellDrawSize = cellSize - 1

            love.graphics.setColor(.86, .86, .86)
            love.graphics.rectangle(
                'fill',
                (x - 1) * cellSize,
                (y - 1) * cellSize,
                cellDrawSize,
                cellDrawSize
            )
        end
    end
end

Selecting cells

The cell position that the mouse cursor is over is stored.

This is calculated by taking the mouse position and dividing it by the cell size, flooring this number, then adding 1 to it.

For example, if the mouse is at position 17 on the X axis and the cell size is 5, dividing 17 by 5 gives 3.4, flooring 3.4 gives 3, and adding 1 gives 4, meaning that the mouse is over cell 4 on the X axis.

The cell size is needed to calculate this, so it is moved into love.load.

math.min is used to give the position a maximum value, so that it isn't outside the grid.

For now, this position is drawn to the screen as text.

Full code at this point

function love.load()
    -- etc.

    cellSize = 5
end

function love.update()
    selectedX = math.floor(love.mouse.getX() / cellSize) + 1
    selectedY = math.floor(love.mouse.getY() / cellSize) + 1
end

function love.draw()
    -- etc.

    -- Removed: local cellSize = 5

    -- Temporary
    love.graphics.setColor(0, 0, 0)
    love.graphics.print('selected x: '..selectedX..', selected y: '..selectedY)
end

Confining selected cell to grid

math.min is used to give the selected position a maximum value, so that it won't be outside the grid even if the mouse is outside the grid.

The grid's width/height in cells is reused from drawing the cells, so variables are made for them.

Full code at this point

function love.load()
    -- etc.

    gridXCount = 70
    gridYCount = 50
end

function love.update()
    selectedX = math.min(math.floor(love.mouse.getX() / cellSize) + 1, gridXCount)
    selectedY = math.min(math.floor(love.mouse.getY() / cellSize) + 1, gridYCount)
end

function love.draw()
    for y = 1, gridYCount do
        for x = 1, gridXCount do
-- etc.

Highlighting cells

The square under the mouse cursor is set to the highlight color.

Full code at this point

function love.draw()
    for y = 1, gridYCount do
        for x = 1, gridXCount do
            local cellDrawSize = cellSize - 1

            if x == selectedX and y == selectedY then
                love.graphics.setColor(0, 1, 1)
            else
                love.graphics.setColor(.86, .86, .86)
            end

            love.graphics.rectangle(
                'fill',
                (x - 1) * cellSize,
                (y - 1) * cellSize,
                cellDrawSize,
                cellDrawSize
            )
        end
    end
end

Creating the grid

A grid is created to store the cells.

Each cell is represented by a boolean value: true for alive, false for dead.

If the cell is alive, then the alive color is used to draw the cell.

To test this, some cells are manually set to alive.

Full code at this point

function love.load()
    -- etc.

    grid = {}
    for y = 1, gridYCount do
        grid[y] = {}
        for x = 1, gridXCount do
            grid[y][x] = false
        end
    end

    -- Temporary
    grid[1][1] = true
    grid[1][2] = true
end

function love.draw()
    for y = 1, gridYCount do
        for x = 1, gridXCount do
            local cellDrawSize = cellSize - 1

            if x == selectedX and y == selectedY then
                love.graphics.setColor(0, 1, 1)
            elseif grid[y][x] then
                love.graphics.setColor(1, 0, 1)
            else
                love.graphics.setColor(.86, .86, .86)
            end

            love.graphics.rectangle(
                'fill',
                (x - 1) * cellSize,
                (y - 1) * cellSize,
                cellDrawSize,
                cellDrawSize
            )
        end
    end
end

Set cells to alive with the left mouse button

If the left mouse button is down, then the selected cell is set to alive.

Full code at this point

function love.update()
    -- etc.

    if love.mouse.isDown(1) then
        grid[selectedY][selectedX] = true
    end
end

Getting number of neighbors

Updating the grid after a step of time requires knowing how many alive neighbors each cell has.

For now, right clicking a cell will print out how many alive neighbors it has.

grid[selectedY + dy] is checked to see if it exists before grid[selectedY + dy][selectedX + dx] is, because if selectedY + dy is not inside the grid, then grid[selectedY + dy] will return nil, and grid[selectedY + dy][selectedX + dx] will error because the [selectedX + dx] part is trying to index nil.

Full code at this point

-- Temporary
function love.mousepressed(mouseX, mouseY, button)
    if button == 2 then
        local neighborCount = 0

        print('Finding neighbors of grid['..selectedY..']['..selectedX..']')

        for dy = -1, 1 do
            for dx = -1, 1 do

                print(' Checking grid['..selectedY + dy..']['..selectedX + dx..']')

                if not (dy == 0 and dx == 0)
                and grid[selectedY + dy]
                and grid[selectedY + dy][selectedX + dx] then

                    print('  Neighbor found')
                    neighborCount = neighborCount + 1
                end
            end
        end

        print('Total neighbors: '..neighborCount)
        print()
    end
end
Finding neighbors of grid[10][10]
 Checking grid[9][9]
 Checking grid[9][10]
 Checking grid[9][11]
  Neighbor found
 Checking grid[10][9]
 Checking grid[10][11]
 Checking grid[11][9]
 Checking grid[11][10]
  Neighbor found
 Checking grid[11][11]
Total neighbors: 2

Changing grid on key press

When a key is pressed, a new grid is created, and the old grid is replaced by the new grid.

For now, all of the cells in the new grid will be alive.

Full code at this point

function love.keypressed()
    local nextGrid = {}

    for y = 1, gridYCount do
        nextGrid[y] = {}
        for x = 1, gridXCount do
            nextGrid[y][x] = true
        end
    end

    grid = nextGrid
end

Changing grid based on neighbors

The code for finding the number of alive neighbors a cell has is moved to here.

A cell in the new grid is alive if it has 3 neighbors, or it is alive in the old grid and has 2 neighbors.

Full code at this point

function love.keypressed()
    local nextGrid = {}

    for y = 1, gridYCount do
        nextGrid[y] = {}
        for x = 1, gridXCount do
            -- Moved
            local neighborCount = 0

            for dy = -1, 1 do
                for dx = -1, 1 do
                    if not (dy == 0 and dx == 0)
                    and grid[y + dy]
                    and grid[y + dy][x + dx] then
                        neighborCount = neighborCount + 1
                    end
                end
            end

            nextGrid[y][x] = neighborCount == 3
                or (grid[y][x] and neighborCount == 2)
        end
    end

    grid = nextGrid
end

-- Removed: function love.mousepressed(mouseX, mouseY, button)

Making cells dead with right click

When a cell is right clicked it becomes dead.

Full code at this point

function love.update()
    -- etc.

    if love.mouse.isDown(1) then
        grid[selectedY][selectedX] = true
    elseif love.mouse.isDown(2) then
        grid[selectedY][selectedX] = false
    end
end

Turning key repeat on

Key repeat is turned on, so that holding a key down will call love.keypressed repeatedly.

Full code at this point

function love.load()
    -- etc.

    love.keyboard.setKeyRepeat(true)
end