🏠 Home page > 🌖 LÖVE tutorials
A tutorial for Lua and LÖVE 11
The levels used in this tutorial are from Rockbox.
Push all the boxes on to the storage locations.
Boxes can only be moved if there is a free space beyond it (not a wall or another box).
Player | |
Player on storage | |
Box | |
Box on storage | |
Storage | |
Wall |
Arrow keys | Move |
r | Reset level |
n | Next level |
p | Previous level |
The different states a cell can be in are represented by the following strings:
'@' | Player |
'+' | Player on storage |
'$' | Box |
'*' | Box on storage |
'.' | Storage |
'#' | Wall |
The level is stored as a grid of these strings.
When an arrow key is pressed, the grid is looped through to find where the player is.
If the position on the grid adjacent to the player in the direction of the arrow pressed is movable (i.e. empty or a storage location), the values of the grid are changed to reflect the new player position.
If the position adjacent to the player is a box and the position beyond the box is movable, the grid is changed to reflect the new player and new box positions.
If there are no boxes left which aren't on storage locations then the level is complete.
Each level is stored as a grid of strings. For now, a single level is stored, and a square is drawn for every cell which isn't a space (i.e. empty).
function love.load() level = { {' ', ' ', '#', '#', '#'}, {' ', ' ', '#', '.', '#'}, {' ', ' ', '#', ' ', '#', '#', '#', '#'}, {'#', '#', '#', '$', ' ', '$', '.', '#'}, {'#', '.', ' ', '$', '@', '#', '#', '#'}, {'#', '#', '#', '#', '$', '#'}, {' ', ' ', ' ', '#', '.', '#'}, {' ', ' ', ' ', '#', '#', '#'}, } end function love.draw() for y, row in ipairs(level) do for x, cell in ipairs(row) do if cell ~= ' ' then local cellSize = 23 love.graphics.rectangle( 'fill', (x - 1) * cellSize, (y - 1) * cellSize, cellSize, cellSize ) end end end end
The cell's string is drawn on top of the cell.
function love.draw() for y, row in ipairs(level) do for x, cell in ipairs(row) do if cell ~= ' ' then local cellSize = 23 love.graphics.setColor(1, 1, 1) love.graphics.rectangle( 'fill', (x - 1) * cellSize, (y - 1) * cellSize, cellSize, cellSize ) love.graphics.setColor(0, 0, 0) love.graphics.print( level[y][x], (x - 1) * cellSize, (y - 1) * cellSize ) end end end end
The background color is set, and the color of each cell is set based on its type.
function love.load() love.graphics.setBackgroundColor(1, 1, .75) -- etc. end function love.draw() for y, row in ipairs(level) do for x, cell in ipairs(row) do if cell ~= ' ' then local cellSize = 23 local colors = { ['@'] = {.64, .53, 1}, ['+'] = {.62, .47, 1}, ['$'] = {1, .79, .49}, ['*'] = {.59, 1, .5}, ['.'] = {.61, .9, 1}, ['#'] = {1, .58, .82}, } love.graphics.setColor(colors[cell]) love.graphics.rectangle( 'fill', (x - 1) * cellSize, (y - 1) * cellSize, cellSize, cellSize ) love.graphics.setColor(1, 1, 1) love.graphics.print( level[y][x], (x - 1) * cellSize, (y - 1) * cellSize ) end end end end
So we don't have to remember which string refers to which cell type, the cell type strings are stored in variables.
function love.load() -- etc. player = '@' playerOnStorage = '+' box = '$' boxOnStorage = '*' storage = '.' wall = '#' empty = ' ' end function love.draw() for y, row in ipairs(level) do for x, cell in ipairs(row) do if cell ~= empty then local cellSize = 23 local colors = { [player] = {.64, .53, 1}, [playerOnStorage] = {.62, .47, 1}, [box] = {1, .79, .49}, [boxOnStorage] = {.59, 1, .5}, [storage] = {.61, .9, 1}, [wall] = {1, .58, .82}, } -- etc. end end end end
The first step in moving the player is finding which cell position they are at.
The cell type of the player's current position and the cell type of the adjacent position in the direction of the arrow key pressed are stored in variables, and, for now, are printed.
function love.keypressed(key) if key == 'up' or key == 'down' or key == 'left' or key == 'right' then local playerX local playerY for testY, row in ipairs(level) do for testX, cell in ipairs(row) do if cell == player or cell == playerOnStorage then playerX = testX playerY = testY end end end -- Temporary print(playerX, playerY) end end
5 5
For now, the cell adjacent to the player in the direction of the key pressed has its type printed out.
function love.keypressed(key) if key == 'up' or key == 'down' or key == 'left' or key == 'right' then local dx = 0 local dy = 0 if key == 'left' then dx = -1 elseif key == 'right' then dx = 1 elseif key == 'up' then dy = -1 elseif key == 'down' then dy = 1 end local current = level[playerY][playerX] local adjacent = level[playerY + dy][playerX + dx] -- Temporary print('current = level['..playerY..']['..playerX..'] ('..current..')') print('adjacent = level['..playerY + dy..']['..playerX + dx..'] ('..adjacent..')') print() end end
current = level[5][5] (@) adjacent = level[4][5] ( ) current = level[5][5] (@) adjacent = level[5][6] (#) current = level[5][5] (@) adjacent = level[6][5] ($) current = level[5][5] (@) adjacent = level[5][4] ($)
A test level is made for testing player movement.
function love.load() level = { {'#', '#', '#', '#', '#'}, {'#', '@', ' ', '.', '#'}, {'#', ' ', '$', ' ', '#'}, {'#', '.', '$', ' ', '#'}, {'#', ' ', '$', '.', '#'}, {'#', '.', '$', '.', '#'}, {'#', '.', '*', ' ', '#'}, {'#', ' ', '*', '.', '#'}, {'#', ' ', '*', ' ', '#'}, {'#', '.', '*', '.', '#'}, {'#', '#', '#', '#', '#'}, } -- etc. end
If the value at the player's current position is player (i.e. not playerOnStorage) and the adjacent cell is empty, then the player's position becomes empty and the adjacent position becomes player.
function love.keypressed(key) -- etc. if current == player and adjacent == empty then level[playerY][playerX] = empty level[playerY + dy][playerX + dx] = player end end end
If the adjacent position is storage, then the new adjacent position becomes playerOnStorage.
Currently the player can move on to storage, but not off storage.
function love.keypressed(key) -- etc. if current == player then if adjacent == empty then level[playerY][playerX] = empty level[playerY + dy][playerX + dx] = player elseif adjacent == storage then level[playerY][playerX] = empty level[playerY + dy][playerX + dx] = playerOnStorage end end end end
The new adjacent position (either player or playerOnStorage) is set based on the type of adjacent, so a table is made which returns the next adjacent cell type when indexed by the current adjacent cell type.
Indexing by anything other than the values empty or storage will be result in nil, so it is also used to check if the player can move to the adjacent position.
function love.keypressed(key) -- etc. local nextAdjacent = { [empty] = player, [storage] = playerOnStorage, } if current == player and nextAdjacent[adjacent] then level[playerY][playerX] = empty level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] end -- etc. end
If the player is on storage, then the player's current position is set to storage.
function love.keypressed(key) -- etc. if nextAdjacent[adjacent] then if current == player then level[playerY][playerX] = empty level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif current == playerOnStorage then level[playerY][playerX] = storage level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] end end end end
A table is made which returns the next cell type for the player's previous position when indexed by the current player cell type.
function love.keypressed(key) -- etc. local nextCurrent = { [player] = empty, [playerOnStorage] = storage, } if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] end end end
The cell beyond the adjacent cell is stored in a variable.
level[playerY + dy + dy] is checked to see if it is not nil before it is indexed by [playerX + dx + dx].
(The adjacent position isn't checked in the same way because there is always a border of walls around each level, so level[playerY + dy] won't ever be nil.)
If the adjacent cell is a box and the beyond cell is empty, then the adjacent position is set to player and the beyond position is set to box.
function love.keypressed(key) -- etc. local current = level[playerY][playerX] local adjacent = level[playerY + dy][playerX + dx] local beyond if level[playerY + dy + dy] then beyond = level[playerY + dy + dy][playerX + dx + dx] end -- etc. if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif adjacent == box and beyond == empty then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = player level[playerY + dy + dy][playerX + dx + dx] = box end end end
If the beyond position is storage, then beyond position is set to boxOnStorage.
function love.keypressed(key) -- etc. if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif adjacent == box then if beyond == empty then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = player level[playerY + dy + dy][playerX + dx + dx] = box elseif beyond == storage then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = player level[playerY + dy + dy][playerX + dx + dx] = boxOnStorage end end end end
A table is made which returns the next beyond cell type when indexed by the current beyond cell type.
function love.keypressed(key) -- etc. local nextBeyond = { [empty] = box, [storage] = boxOnStorage, } if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif adjacent == box and nextBeyond[beyond] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = player level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond] end end end
If the adjacent cell is a box on storage, then the adjacent position is set to boxOnStorage.
function love.keypressed(key) -- etc. if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif nextBeyond[beyond] then level[playerY][playerX] = nextCurrent[current] if adjacent == box then level[playerY + dy][playerX + dx] = player elseif adjacent == boxOnStorage then level[playerY + dy][playerX + dx] = playerOnStorage end level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond] end end end
A table is made which returns the next adjacent cell type when a box is pushed when indexed by the current adjacent cell type.
function love.keypressed(key) -- etc. local nextAdjacentPush = { [box] = player, [boxOnStorage] = playerOnStorage, } if nextAdjacent[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacent[adjacent] elseif nextBeyond[beyond] and nextAdjacentPush[adjacent] then level[playerY][playerX] = nextCurrent[current] level[playerY + dy][playerX + dx] = nextAdjacentPush[adjacent] level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond] end end end
The levels are stored in a table.
The number of the current level is also stored.
The cells of the current level are copied from the table containing all the levels into the current level table.
function love.load() -- etc. levels = { { {' ', ' ', '#', '#', '#'}, {' ', ' ', '#', '.', '#'}, {' ', ' ', '#', ' ', '#', '#', '#', '#'}, {'#', '#', '#', '$', ' ', '$', '.', '#'}, {'#', '.', ' ', '$', '@', '#', '#', '#'}, {'#', '#', '#', '#', '$', '#'}, {' ', ' ', ' ', '#', '.', '#'}, {' ', ' ', ' ', '#', '#', '#'}, }, { {'#', '#', '#', '#', '#'}, {'#', ' ', ' ', ' ', '#'}, {'#', '@', '$', '$', '#', ' ', '#', '#', '#'}, {'#', ' ', '$', ' ', '#', ' ', '#', '.', '#'}, {'#', '#', '#', ' ', '#', '#', '#', '.', '#'}, {' ', '#', '#', ' ', ' ', ' ', ' ', '.', '#'}, {' ', '#', ' ', ' ', ' ', '#', ' ', ' ', '#'}, {' ', '#', ' ', ' ', ' ', '#', '#', '#', '#'}, {' ', '#', '#', '#', '#', '#'}, }, { {' ', '#', '#', '#', '#', '#', '#', '#'}, {' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#'}, {'#', '#', '$', '#', '#', '#', ' ', ' ', ' ', '#'}, {'#', ' ', '@', ' ', '$', ' ', ' ', '$', ' ', '#'}, {'#', ' ', '.', '.', '#', ' ', '$', ' ', '#', '#'}, {'#', '#', '.', '.', '#', ' ', ' ', ' ', '#'}, {' ', '#', '#', '#', '#', '#', '#', '#', '#'}, }, } currentLevel = 1 level = {} for y, row in ipairs(levels[currentLevel]) do level[y] = {} for x, cell in ipairs(row) do level[y][x] = cell end end end
When the r key is pressed the level is reset.
The code for copying the current level from the levels table is reused, so a function is made.
function love.load() -- etc. function loadLevel() level = {} for y, row in ipairs(levels[currentLevel]) do level[y] = {} for x, cell in ipairs(row) do level[y][x] = cell end end end loadLevel() end function love.keypressed(key) -- etc. elseif key == 'r' then loadLevel() end end
When the n key is pressed, the next level is loaded, and when the p key is pressed, the previous level is loaded.
function love.keypressed(key) -- etc. elseif key == 'n' then currentLevel = currentLevel + 1 loadLevel() elseif key == 'p' then currentLevel = currentLevel - 1 loadLevel() end end
If the next level is after the last level, then the first level is loaded.
If the previous level is before the first level, then the last level is loaded.
function love.keypressed(key) -- etc. elseif key == 'n' then currentLevel = currentLevel + 1 if currentLevel > #levels then currentLevel = 1 end loadLevel() elseif key == 'p' then currentLevel = currentLevel - 1 if currentLevel < 1 then currentLevel = #levels end loadLevel() end end
After the player has moved, all of the cells in the level are looped through, and if none of the cells are boxes (i.e. all the boxes are on storage), then the level is complete and the next level is loaded.
function love.keypressed(key) if key == 'up' or key == 'down' or key == 'left' or key == 'right' then -- etc. local complete = true for y, row in ipairs(level) do for x, cell in ipairs(row) do if cell == box then complete = false end end end if complete then currentLevel = currentLevel + 1 if currentLevel > #levels then currentLevel = 1 end loadLevel() end elseif key == 'r' then -- etc. end