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 variablesLOCAL.
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 program | its own thread, the one it starts on |
CALL RENDER | my render thread, once a frame |
Behaviors, ONCOLLISION | my tick, sixty times a second |
CALL TIMER, ASYNC !! after or every | my timer, when the time comes |
CALL EVENT | me, a moment after the EVENT |
CALL THREAD, ASYNC | a 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
ATOMICvariable 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), thenscore = 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.