Four scripts to learn from
Four whole scripts, each explained. The first three are in the editor under Try an example; the fourth is a level of its own, made with File > New. Each makes everything it needs, so all of them work in an empty level: add one, press F5, then change things and see what happens.
On the beat
A text that counts the song's beats, and a kick of the camera on every fourth one.
-- A text made by the script, a little above the middle of the screen.
local counter = things.new("text", { x = camera.x, y = camera.y - 3, size = 1.2, text = "Beat 0" })
-- level.beat is an event: it happens on every beat of the song, with the beat's number.
level.beat:connect(function(beat)
counter.text = `Beat {beat}`
if beat % 4 == 0 then
camera:effect("pulse", 0.5, 0.4)
print(`bar {beat // 4 + 1} begins`)
end
end)What to notice:
- The script is over after two statements: it makes a text and connects a function. The function keeps being called for the whole level. See events.
beat % 4 == 0is true on beats 0, 4, 8...:%gives what is left after dividing, and nothing is left on every fourth beat.- The text between backticks is a string with a value put into it:
{beat}becomes the beat's number.
Try: make it kick on every second beat. Make the text bigger on the kick and glide back with counter:tween({ size = 1.2 }, 0.3).
A button and a door
Touch the yellow square, and the door glides out of the way.
local x, y = camera.x, camera.y
-- A strip of floor, a door across it, and a button before the door.
things.new("floor", { x = x + 4, y = y, w = 12, h = 3 })
local door = things.new("block", { x = x + 5, y = y, w = 1, h = 3 })
local button = things.new("deco", { x = x + 2, y = y, ["color.r"] = 255, ["color.g"] = 214, ["color.b"] = 92 })
-- The script waits on this line until the cube touches the button...
button.touched:wait()
print("the button was pressed")
sound.play("@door")
button.alpha = 0.3
-- ...then the door glides up and out of the way, in one second.
door:tween({ y = door.y - 3 }, 1, "quad", "out"):wait()
print("the door is open")What to notice:
- The script reads from top to bottom like a story: make the room, wait for the button, open the door.
:wait()on an event is what makes that possible. - A block kills the cube, so the door really is in the way until it has moved.
- A colour's parts have a dot in their names (
color.r), so in a table they are written with square brackets and quotes.
Try: close the door again after three seconds. Make a second button that only works once the first was pressed.
Three lives
What would kill the cube takes a life instead, and nothing hurts it for three seconds after.
local lives = 3
local x, y = camera.x, camera.y
things.new("floor", { x = x + 3, y = y, w = 11, h = 5 })
things.new("saw", { x = x + 4, y = y, w = 1.5, h = 1.5 })
local label = things.new("text", { x = x, y = y - 3.5, text = `Lives: {lives}` })
-- player.hurt is an event: it happens in the moment the cube touches something that kills it.
player.hurt:connect(function()
if lives <= 0 then
return -- no lives left: this time the run is over
end
lives -= 1
label.text = `Lives: {lives}`
player:revive()
camera:effect("flash", 0.4, 0.3)
-- Nothing hurts the cube for three seconds (it is drawn smaller meanwhile, to show it).
player.god = true
player.size = 0.7
wait(3)
player.size = 1
player.god = false
end)What to notice:
player:revive()only works insideplayer.hurt: it saves the cube in the very moment it would die.- Without the three seconds of
player.god, the cube would still be on the saw the moment after, and lose its next life at once. - The connected function waits three seconds in the middle. That is fine: each call of it is a task of its own. And while god mode is on,
player.hurtdoes not happen at all. - When the last life is gone the run fails, the level starts again, and so does the script:
livesis 3 again.
Try: give a life back for every coin. Make the cube blink while it is safe, with a loop that switches player.visible and waits a tenth of a second.
A maze in 30 seconds
File > New > A maze in 30 seconds makes a level whose script builds a new maze every time. The level itself holds almost nothing: the pad the cube starts on (named start), a text for the clock (named clock), and a camera that stands still. The script does the rest.
const COLUMNS, ROWS = 9, 5 -- how many rooms the maze has, across and down
const ROOM = 2 -- how wide a room and a corridor are, in tiles
const GAP = 1 -- how much water lies between two corridors
const SECONDS = 30 -- the time to get through
const STEP = ROOM + GAP
local pad = things.find("start") -- the piece of floor the cube starts on: the maze's first room
local startX, startY = pad.centerX, pad.centerY
-- random.new() gives new numbers at every try. random.new(7) would build the same maze every time.
local rng = random.new()
-- A level's scripts make 64 things in one step at most: a breath after every 40.
local made = 0
local function floor(x, y, w, h)
things.new("floor", { x = x, y = y, w = w, h = h })
made += 1
if made % 40 == 0 then
wait()
end
end
-- Digging: from a room, go to a neighbour that isn't dug out yet, picked at random; with none left, go back a room.
-- dug[room] is how many rooms away from the start that room is.
local function room(c, r)
return r * COLUMNS + c
end
local dug = { [room(0, 0)] = 0 }
local trail = { { 0, 0 } }
local farthest, goalC, goalR = 0, 0, 0
const ways = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
while #trail > 0 do
local c, r = trail[#trail][1], trail[#trail][2]
local open = {}
for _, way in ways do
local nc, nr = c + way[1], r + way[2]
if nc >= 0 and nc < COLUMNS and nr >= 0 and nr < ROWS and dug[room(nc, nr)] == nil then
table.insert(open, way)
end
end
if #open == 0 then
table.remove(trail)
else
local way = rng:pick(open)
local nc, nr = c + way[1], r + way[2]
local steps = dug[room(c, r)] + 1
dug[room(nc, nr)] = steps
if steps > farthest then
farthest, goalC, goalR = steps, nc, nr
end
-- The new room, and the corridor that leads to it.
floor(startX + nc * STEP, startY + nr * STEP, ROOM, ROOM)
floor(startX + (c + way[1] / 2) * STEP, startY + (r + way[2] / 2) * STEP, way[1] ~= 0 and GAP or ROOM, way[2] ~= 0 and GAP or ROOM)
table.insert(trail, { nc, nr })
end
end
-- The way out: a yellow square in the room furthest from the start.
local goal = things.new("deco", {
x = startX + goalC * STEP, y = startY + goalR * STEP, w = 1.2, h = 1.2,
["color.r"] = 255, ["color.g"] = 214, ["color.b"] = 92,
})
-- A checkpoint: a try that fails starts again from here, with the same maze and the clock back at 30.
level.checkpoint()
local clock = things.find("clock")
local began = time.now
goal.touched:connect(function()
print(`out in {math.round(time.now - began, 1)} seconds`)
sound.play("@coin")
level.win()
end)
while true do
local left = SECONDS - (time.now - began)
if left <= 0 then
clock.text = "0.0"
level.fail()
break
end
clock.text = string.format("%.1f", left)
wait()
endHow it works:
- The settings are
constat the top: changeCOLUMNS,ROWSorSECONDSand the whole maze follows. (A bigger maze needs the camera zoomed out: Level > Camera.) - The digging is a classic way to make a maze. Stand in a room; pick a neighbour that has not been dug yet, at random; break through to it and stand there. When a room has no such neighbour, walk back one room along
trailand look again. When the trail is empty, every room has been reached, and there is exactly one way between any two rooms. - The maze is made of floor. There are no walls: the cube must stay on the floor, as in every level, and the water does the rest. A room is a square of floor, a corridor a smaller piece between two rooms.
made % 40 == 0spreads the building over a few steps, because a level makes 64 things in one step at most.- The goal is put in the room that took the most steps to reach, which the digging remembered on the way (
farthest). - The checkpoint right after building is what keeps the maze the same when the player fails: the run goes back to that moment, script and all.
beganis part of what is kept, and so is the time, so the clock is at 30 again. - The clock is a loop that waits one step in every round. When it runs out it calls
level.fail().
Try: make the maze 12 by 7 and give it 45 seconds. Put a coin in every dead end (a room with only one way in). Let the walls close in: destroy a few pieces of floor every ten seconds.