Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Timers, events and threads

So far a game has been one loop, with my behaviors and CALL RENDER running beside it. This lesson is about the other ways I can call your functions for you: every so often, when something happens, or on a thread of their own while your loop gets on with the game.

Every so often

CALL TIMER calls a function every so often, for as long as the game runs or until STOP TIMER stops it. It takes the function, how long between calls, the unit ("seconds" unless you say "milliseconds" or "minutes"), and a name to stop it by.

SCREEN 320, 240
ATOMIC left = 10

FUNCTION tick()
    left = left - 1
    IF left = 0 THEN STOP TIMER "countdown"
END FUNCTION
CALL TIMER tick, 1, "seconds", "countdown"

DO
    IF COOLDOWN(1)
        FRAME
            CLS
            LOCATE 8, 17
            IF left > 0 THEN PRINT left ELSE PRINT "Lift off!"
        END FRAME
    END IF
LOOP

The timer only counts. The loop draws, sixty times a second, whatever left is now. A timer can stop itself, as tick does when it gets to 0.

With !! after, the function’s called once, after the time, instead of every time:

SCREEN 320, 240
ATOMIC rang = FALSE

FUNCTION ring()
    rang = TRUE
END FUNCTION
CALL TIMER ring, 0.5, "seconds", "alarm" !! after

ticks = 0
DO UNTIL rang
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
PRINT "ring ring!"
ring ring!

!! args([...]) gives the function the values to be called with, every time.

Crash-tan’s Tip: A timer for “a second from now” is easier to read than counting ticks in your loop, and it keeps counting while your loop is busy elsewhere, in a menu, say.

When something happens

An event is a name for something that happens in your game: "coin", "door", "boss_down". CALL EVENT says which function to call when it happens, and EVENT makes it happen, with !! args for what to tell the function.

SCREEN 320, 240
ATOMIC score = 0

FUNCTION on_coin(points)
    score = score + points
END FUNCTION
CALL EVENT on_coin, "coin"

EVENT "coin" !! args([10])
EVENT "coin" !! args([25])

ticks = 0
DO WHILE score < 35 AND ticks < 60
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
PRINT "score", score
score 35

EVENT doesn’t wait for on_coin. It hands the event to me, and I call the function a moment later, on a thread of my own. That’s why the loop waits for the score to arrive before it prints.

What an event is good for is that whoever fires it doesn’t need to know who’s listening. A sprite’s ONCOLLISION can say EVENT "coin" when it touches a coin, and the score, the sound and the “+10” floating up can each be someone else’s business. In a game for several players, EVENT can fire on the other players’ machines too.

A thread of its own

Some jobs take a while: building a big level, working out the best move. If your loop did them, the game would freeze until they were done. CALL THREAD runs a function on a thread of its own, and your loop keeps drawing. Here the long job is only adding up a lot of numbers, two hundred times over, but it could be anything.

SCREEN 320, 240
ATOMIC done = 0

FUNCTION long_job()
    FOR step = 1 ... 200
        LOCAL sum = 0
        FOR i = 1 ... 20000
            sum = sum + sin(i)
        NEXT
        done = step
    NEXT
END FUNCTION
CALL THREAD long_job, "worker"

DO
    IF COOLDOWN(1)
        IF keydown("Escape") THEN STOP THREAD "worker"
        MEMORYBUFFER "bar"
            LINE (60, 110) -> (260, 130), 8 !! box
            LINE (60, 110) -> (60 + done, 130), 10 !! box fill
        END MEMORYBUFFER
        FRAME
            CLS
            PUTIMAGE (0, 0), "bar"
            LOCATE 10, 13
            PRINT "steps done:", done, "of 200"
        END FRAME
    END IF
LOOP

STOP THREAD "worker" stops it before it’s done (press Escape), and STOP THREAD with no name stops every one.

Crash-tan’s Tip: Two calls running at once share every variable that isn’t LOCAL, the same as any two functions do. If a function might run while another copy of it runs, on a timer that comes round again or in two threads, make its own working variables LOCAL.

ASYNC

ASYNC in front of an ordinary call starts it on a thread of its own and gives you a future: a promise of the answer. RESOLVE waits for the answer and gives it to you, or all of them at once for several futures.

SCREEN 320, 240
FUNCTION add_up(n)
    LOCAL total = 0
    FOR i = 1 ... n
        total = total + i
    NEXT
    RETURN total
END FUNCTION

first = ASYNC add_up(1000)
second = ASYNC add_up(2000)
// Both are working now. Wait for both answers:
PRINT RESOLVE(first, second)
[500500, 2001000]

ASYNC has two modifiers that make it a timer. !! after(time, unit) starts the call once, later, and its future becomes the answer. !! every(time, unit) starts it again and again, and its future is what you hold on to so that STOP can stop it. The unit is milliseconds unless you say otherwise.

SCREEN 320, 240
FUNCTION shout(word)
    RETURN word + "!"
END FUNCTION
later = ASYNC shout("time's up") !! after(1, "seconds")
PRINT RESOLVE(later)

ATOMIC beats = 0
FUNCTION beat()
    beats = beats + 1
END FUNCTION
drum = ASYNC beat() !! every(100)
ticks = 0
DO WHILE beats < 3
    IF COOLDOWN(1) THEN ticks = ticks + 1
LOOP
STOP drum
PRINT "three beats, then", RESOLVE(drum)
time's up!
three beats, then NULL

A stopped future resolves to NULL, so anyone still waiting on it isn’t left waiting forever.

Which thread, and ATOMIC

Here’s who calls what, and where:

Your programits own thread, the one it starts on
CALL RENDERmy render thread, once a frame
Behaviors, ONCOLLISIONmy tick, sixty times a second
CALL TIMER, ASYNC !! after or everymy timer, when the time comes
CALL EVENTme, a moment after the EVENT
CALL THREAD, ASYNCa new thread, just for that call

All of them run at the same time as your program. That’s what ATOMIC is for. A variable that two of them change, like left above (the timer changes it and the loop reads it) or a score that a collision and the loop both change, is marked ATOMIC once, at the top of the program. Then left = left - 1 is one step: no other thread can sneak in between the reading and the writing, and nothing gets lost.

A variable only one of them ever uses doesn’t need it. Nor does one that’s set before the others start and only read after that.

Crash-tan’s Tip: A line that changes an ATOMIC variable using its own value can’t call a function, because every other thread that wants the variable is waiting on that line. Work the value out on the line before: bonus = points_for(level), then score = score + bonus.

Drawing somewhere else for a moment

WITH LAYER draws on another layer until END WITH LAYER, then goes back to the one you were on. It’s handy in a function that draws a part of the screen, the score, say, which shouldn’t leave whoever called it drawing on the wrong layer:

SCREEN 320, 240
CREATE LAYER 1, 320, 240

MEMORYBUFFER "ground"
    LINE (0, 200) -> (319, 239), 2 !! box fill
END MEMORYBUFFER
MEMORYBUFFER "border"
    LINE (4, 4) -> (315, 235), 15 !! box
END MEMORYBUFFER

FRAME
    CLS
    PUTIMAGE (0, 0), "ground"
    WITH LAYER 1
        PUTIMAGE (0, 0), "border"
        LOCATE 2, 3
        PRINT "lives 3"
    END WITH LAYER
    // Back on layer 0.
    LOCATE 13, 15
    PRINT "Ready?"
END FRAME
PAUSE

Layer 1 isn’t cleared, so it’s see-through apart from the border and the lives, and layer 0 shows under it.