Luawe the Wayfarer Engine's scripting language All pages

Waiting and tasks

A level is played over time, so most of what a script does is about when. This page is about pausing, and about doing several things side by side.

Waiting

wait(seconds) pauses the script for that long. The level carries on meanwhile; only this script stands still.

print("three...")
wait(1)
print("two...")
wait(1)
print("one...")
wait(1)
print("go!")

wait() with no number pauses until the next step of the level. A level takes 48 steps a beat, so that is the shortest pause there is: use it in a loop that should run "all the time".

wait gives back how long it really waited, which is a touch more than asked, because a script carries on at a step.

In time with the music

beats(n) is how many seconds n beats of the song take, so waits can be written in beats:

local lamp = things.find("lamp")
while true do
    lamp.alpha = 1
    wait(beats(1))
    lamp.alpha = 0.2
    wait(beats(1))
end

For something that should happen on the beat, do not count waits: small differences add up. Use the level's own beat instead:

level.beat:connect(function(beat)
    print("beat", beat) -- on every beat of the song
end)

level.onBeat(32, function()
    print("the drop") -- once, when the song gets to beat 32
end)

time.now is the seconds since the level began and time.beat the beat right now, with decimals. Both follow the song.

A loop must wait

A loop that should go on for the whole level has to wait in every round:

local saw = things.find("saw")
while true do
    saw.rotation += 4
    wait()
end

Without the wait(), the loop would never let the level take its next step. The game does not let that happen: a task that runs for too long without waiting is stopped, and the script output says which line it was on.

Tasks

A script starts as one task: one thing being done, from the first line down. task.spawn starts another one, which runs beside it. Each task waits on its own.

local function blink(name, seconds)
    local lamp = things.find(name)
    while lamp do
        lamp.visible = not lamp.visible
        wait(seconds)
    end
end

task.spawn(blink, "red lamp", 0.5)
task.spawn(blink, "blue lamp", 0.8)
print("both lamps are blinking, and the script goes on")

The values after the function are handed to it. A spawned task starts at once and runs until it first waits (or ends); then the task that spawned it carries on.

The other ways to start one:

WrittenStarts the function
task.spawn(f, ...)Now
task.defer(f, ...)When this task next waits or ends, still in the same step
task.delay(seconds, f, ...)After that many seconds
task.delay(0.5, function()
    print("half a second later")
end)
task.defer(function()
    print("as soon as the script pauses")
end)
print("first")
wait(1)
print("last")

It prints

first
as soon as the script pauses
half a second later
last

Each of them gives back the task, which task.cancel can stop while it is waiting:

local alarm = task.delay(5, function()
    print("too slow!")
end)

things.find("button").touched:wait()
task.cancel(alarm) -- the button was pressed in time

Tasks never run at the same instant. One runs until it waits, then the next. So two tasks cannot trip over each other in the middle of a line, and a value does not change under a task's feet between two waits.

Coroutines

A coroutine is a function that can stop half way and be carried on later, by hand. A task is carried on by the game when its wait is over; a coroutine is carried on when you say so. They are for the rarer case where two pieces of a script hand values back and forth.

local steps = coroutine.create(function(first)
    print("got", first)
    local second = coroutine.yield("ready for more")
    print("got", second)
    return "done"
end)

print(coroutine.resume(steps, "one"))
print(coroutine.status(steps))
print(coroutine.resume(steps, "two"))
print(coroutine.status(steps))

It prints

got one
true ready for more
suspended
got two
true done
dead

coroutine.yield stops the coroutine and hands its values to whoever resumed it. The next coroutine.resume carries it on, and its values become what yield gives back. resume answers like pcall: true and the values, or false and an error.

coroutine.wrap makes a plain function out of a coroutine, each call of which carries it on. It is a neat way to write something that gives one value after another:

local function countTo(n)
    return coroutine.wrap(function()
        for i = 1, n do
            coroutine.yield(i)
        end
    end)
end

local nextNumber = countTo(3)
print(nextNumber(), nextNumber(), nextNumber())

It prints

1 2 3

A coroutine can wait like any task.

Next: events.