The game loop and tasks
Pick a program shape, end each frame with Sync(), wait on game time, and run several jobs side by side with cooperative tasks.
In one line#
The Engine draws the frames, not your program. A BASIC program does some work,
then hands control back so the frame can be drawn. Sync() is that hand-off.
Tasks let several pieces of work take turns at those hand-offs.
Three program shapes#
| Shape | What it looks like | When it runs | Use it for |
|---|---|---|---|
| Straight-line | statements, top to bottom | once, when the program starts | setup, tools, tests |
| Main loop | one DO … LOOP with Sync() | the loop body runs once per frame | a classic AGK or DarkBASIC game |
| Lifecycle functions | FUNCTION start(), FUNCTION update(dt)… | the Engine calls them | the same model as JavaScript scripts |
Straight-line#
lives = 3
Console("Lives: " + Str(lives)) ' Lives: 3
The program runs once, top to bottom, and ends.
The main loop#
[statements] ' run once, before the first frame
DO
[statements] ' run every frame
Sync()
LOOP
[statements] ' run once, after EXIT leaves the loop
frames = 0
Console("start") ' start
DO
frames = frames + 1
IF frames = 3 THEN EXIT
Sync()
LOOP
Console("left after " + Str(frames)) ' left after 3
- One
DO … LOOPper program, at the top level. Not insideIF,WHILEor aFUNCTION. - The loop must contain
Sync()(orSwap(), orYieldTask()). Without it the compiler stops with "DO/LOOP requires SYNC() so the loop can yield a frame". EXITleaves the loop.ENDstops the whole program at once.
DO
Sync()
LOOP
DO ' refused: a second main loop
Sync()
LOOP
In AGK and DarkBASIC,
DO … LOOPis just an endless loop you can put anywhere. In aukimi it is the frame loop: one per program. For any other loop, useWHILE … ENDWHILEorREPEAT … UNTIL.
⚠️ Do not write a FUNCTION main() in a program with a DO loop. The loop
is compiled into a function of that name, and the two collide. The compiler
does not warn you, and the loop never runs.
Lifecycle functions#
GLOBAL elapsed# = 0
FUNCTION start()
Console("ready") ' ready
ENDFUNCTION
FUNCTION update(dt#)
elapsed# = elapsed# + dt#
Print("Time: " + Str(elapsed#, 1))
ENDFUNCTION
The Engine calls start once, then update every frame with the frame
duration in seconds. There is no loop to write and no Sync() to call: the
Engine draws the frame when update returns. The full list of hooks
(fixedUpdate, onCollisionEnter, onMessage…) is in
Functions.
⚠️ A lifecycle program cannot wait. Wait(), Sleep(), AWAITTASK,
AWAITALL and AWAITANY are refused inside a FUNCTION. Worse, one of them
anywhere in a program without DO, even inside a task label, turns the
whole program into a single main(), and the Engine then no longer finds
start or update. Nothing tells you. To time something in a lifecycle
program, count frames in a task with YieldTask() (see Tasks, below), or
add up dt.
Ending a frame: Sync(), Swap(), YieldTask()#
| Call | What it does |
|---|---|
Sync() | ends the frame: the Engine draws, and the program resumes on the next frame. It also waits for 3D models still loading, so the frame after LoadObject(...) and Sync() has the model. |
Swap() | the same thing. Kept for the classic Render2DBack(), Render3D(), Render2DFront(), Swap() sequence: the three Render calls do nothing, Swap() draws the whole frame, and you get the right picture. |
YieldTask() | the same thing again, under a name that reads well inside a task |
All three need parentheses: SYNC alone is refused.
When they really yield#
Sync()yields a frame only where the program can wait: the program has a top-levelDO … LOOP, or it has anENDstatement, or the call is inside a label (aGOSUBsubroutine or a task).
Anywhere else, Sync() draws once and returns at once. The program does not
pause.
⚠️ The straight-line WHILE trap. This is the classic "wait for a click"
loop. In a program with no DO and no END, its Sync() never yields: the
loop spins without a break and freezes the browser tab. There is no error.
' No DO and no END in this program: do not run it.
ready = 0
WHILE ready = 0
IF PointerPressed() THEN ready = 1
Sync() ' draws, but does not wait for the next frame
ENDWHILE
The fix is one line. With an END, the same loop yields every frame:
ready = 0
WHILE ready = 0
IF PointerPressed() THEN ready = 1
Sync() ' yields: the program has an END
ENDWHILE
Console("clicked") ' clicked
END
- A
Wait()or aSleep()elsewhere in the program does not fix it. OnlyDO … LOOP,END, or a label does. - In AGK,
Sync()ends the frame wherever it is written. This is the main difference to remember.
⚠️ Never call Sync() inside a FUNCTION. Without a DO loop it does not
yield. With one, the program fails to start. Lifecycle functions never need
it: the Engine draws when they return.
Before DO and after LOOP#
| Where | Runs | Allowed |
|---|---|---|
before DO | once, before the first frame | anything: loading, Wait(), a WHILE … Sync() loading screen |
between DO and LOOP | once per frame | anything |
after LOOP | once, when EXIT leaves the loop | anything but Wait() and Sleep() |
Console("loading") ' loading
Wait(0.5) ' half a second of game time
frames = 0
DO
frames = frames + 1
IF frames = 120 THEN EXIT
Sync()
LOOP
Console("played " + Str(frames) + " frames") ' played 120 frames
ENDinside the loop skips the code afterLOOP.FUNCTIONblocks and labels can sit anywhere, before or after the loop. They are not "code afterLOOP": they only run when called.Wait()orSleep()afterLOOPis refused:must appear before or inside the top-level DO/LOOP.
Waiting: Wait() and Sleep()#
Wait(<seconds>)
Sleep(<milliseconds>)
Console("3") ' 3
Wait(1) ' one second
Console("2") ' 2
Sleep(500) ' half a second
Console("go") ' go
- Both wait on game time. They stop while the game is paused, and they follow the time scale. The Engine keeps drawing frames during the wait.
Waitcounts seconds,Sleepcounts milliseconds.Waitis an aukimi addition;Sleepis the AGK command.- ⚠️ In DarkBASIC,
WAITcounts milliseconds. HereWait(1000)is a wait of sixteen minutes.
| Place | Wait() / Sleep() | Sync() / Swap() / YieldTask() |
|---|---|---|
| straight-line program | yes | yields only if the program has an END |
before DO or inside the loop | yes | yes |
after LOOP | refused | yes |
a label (GOSUB subroutine or task) | yes | yes |
inside a FUNCTION, lifecycle hooks included | refused | does not yield: do not use |
FUNCTION Pause()
Wait(1) ' refused: a FUNCTION cannot wait
ENDFUNCTION
The error reads Wait(...) is supported in the main BASIC program, not inside FUNCTION. To wait in a reusable piece of code, write a label and reach it with
GOSUB, or start it as a task. A GOSUB to a label that waits needs a DO
loop or an END in the program, or the compiler refuses it.
Print draws, Console logs#
| Call | Where it goes | How often to call it |
|---|---|---|
Print(...) | drawn on the screen, top left | every frame, inside the loop |
PrintC(...) | the same, without a line break | every frame |
Console(...) | the editor's console | when something happens |
Debug(...) | the same as Console, under PureBasic's name | when something happens |
ConsoleWarning, ConsoleError, ClearConsole | the console | when something happens |
Print writes into the frame being built. Each new frame that draws anything
starts from an empty text layer, so text printed once disappears. Print it on
every frame.
Console writes one line, once. Do not call it every frame: the real errors
drown. Identical lines in a burst are grouped and counted.
score = 0
DO
score = score + 1
Print("Score: " + Str(score)) ' drawn again every frame
IF score = 100 THEN Console("100 reached") ' logged once
IF score = 200 THEN EXIT
Sync()
LOOPTasks#
A task runs a label alongside the main program. Tasks are cooperative: only one piece of code runs at a time, and they take turns only when the running one yields.
<handle> AS TASKHANDLE ' or AS JOB; optional
STARTTASK <handle>, <Label>
AWAITTASK <handle>
AWAITALL <handle>, <handle> [, <handle>…]
AWAITANY <handle>, <handle> [, <handle>…]
CANCELTASK <handle>
YieldTask()
<Label>:
[statements]
RETURN <value>
Start a task and wait for its result#
job AS TASKHANDLE
STARTTASK job, CountDown
Console("started") ' started
AWAITTASK job
Console("result " + Str(TaskResult(job))) ' result 42
END
CountDown:
FOR i = 3 TO 1 STEP -1
Console("tick " + Str(i)) ' tick 3, tick 2, tick 1
Wait(0.5)
NEXT
RETURN 42
STARTTASKdoes not run the task on the spot. The task begins when the main program next yields:Sync(),Wait(),AWAITTASK… That is whystartedcomes first.- A task is a label that ends with
RETURN value. The value is whatTaskResultgives back. A bareRETURNleaves no usable result. - Tasks share the program's variables. A task can update the score; the main loop sees the new value on its next line.
- A task can start and await other tasks.
AWAITTASK,AWAITALLandAWAITANYare refused inside aFUNCTION, likeWait().STARTTASKandCANCELTASKare allowed there.
⚠️ Inside a label, give each yield a line of its own. The compiler decides
whether a label waits by reading its lines. It misses IF … THEN Sync() or
IF … THEN Wait(1) on one line, and it misses Sync(), Swap() or
YieldTask() followed by a comment. The program then fails to start, with no
error at compile time. Write a block IF, and put the comment on the line
above, as in the example below.
The handle#
STARTTASK creates the variable if it does not exist, even under
OPTION EXPLICIT. Declaring it AS TASKHANDLE (or AS JOB, the same type)
gives it a type:
- it starts as
NULL; - it refuses anything that is not a task:
job = 5stops the program withBASIC value is not a task handle.
⚠️ The handle is a plain variable. STARTTASK jobs[1], Worker is not
supported: start the task into a variable, then copy it into the array.
Read a task's state#
| Call | Gives |
|---|---|
TaskDone(h) | true once the task returned |
TaskFailed(h) | true if the task stopped on an error |
TaskCancelled(h) | true as soon as CANCELTASK was called on it |
TaskResult(h) | the RETURN value. An error if the task has not finished, or failed. |
⚠️ These four answer true or false, not 1 or 0. Test them bare or with
NOT: IF TaskDone(h) THEN …. Written IF TaskDone(h) = 1, the test is
always false, and Str(TaskDone(h)) prints true.
Polling from the main loop keeps the game drawing while the work goes on:
loader AS TASKHANDLE
STARTTASK loader, LoadLevel
DO
IF TaskDone(loader)
Console("rooms: " + Str(TaskResult(loader))) ' rooms: 3
EXIT
ENDIF
Print("Loading...")
Sync()
LOOP
Console("play") ' play
LoadLevel:
FOR room = 1 TO 3
' one room per frame
Sync()
NEXT
RETURN 3
Errors inside a task#
An error inside a task does not stop the program. The task is marked failed, and the error comes back where you wait for it:
t AS TASKHANDLE
STARTTASK t, Reload
TRY
AWAITTASK t
CATCH e$
Console("caught: " + e$) ' caught: out of ammo
ENDTRY
IF TaskFailed(t) THEN Console("failed") ' failed
END
Reload:
Wait(0.2)
THROW "out of ammo"
RETURN 0
| Wait | When a task failed |
|---|---|
AWAITTASK h | raises the task's error |
AWAITALL a, b | raises the first error it meets |
AWAITANY a, b | raises nothing: check TaskFailed afterwards |
TaskResult(h) | raises the task's error |
Errors are covered in Errors.
Cancel a task#
CANCELTASK h asks the task to stop. The task stops at its next yield:
Wait(), Sleep(), Sync(), Swap() or YieldTask(). Normally the code after
that yield never runs. The exception is a yield inside a TRY: see below.
slow AS TASKHANDLE
fast AS TASKHANDLE
STARTTASK slow, Slow
STARTTASK fast, Fast
AWAITANY slow, fast
IF TaskDone(fast) THEN Console("fast finished first") ' fast finished first
CANCELTASK slow
AWAITALL slow, fast
IF TaskCancelled(slow) THEN Console("slow cancelled") ' slow cancelled
END
Slow:
Wait(10)
Console("never printed")
RETURN 1
Fast:
Sync()
RETURN 2
TaskCancelledis true at once, andAWAITTASKon a cancelled task returns at once, without an error.TaskResulton a cancelled task is an error: it has no result.CANCELTASKon a task that already finished does nothing.- ⚠️ A task cancelled right after
STARTTASKstill runs up to its first yield, because it had not started yet. - ⚠️ A task that never yields cannot be stopped: it runs to its
RETURNin one go.AWAITTASKinside a task is not a cancellation point either. - ⚠️ A
TRYaround a yield catches the cancellation. ItsCATCHreceives the messageBASIC task cancelled, and the code afterENDTRYkeeps running until the next yield. To let the cancellation through, re-raise it:CATCH e$, thenIF e$ = "BASIC task cancelled", thenRETHROW.
Why there are no threads#
Threads with shared memory are deliberately left out. Not "not yet": left out.
With tasks, only one piece of code runs at a time, and the switch happens only at a yield. Between two yields, nothing else can touch your variables. You need no lock, and there is no data race to hunt.
PureBasic offers CreateThread with mutexes; AGK and DarkBASIC have no tasks at
all. In aukimi, real parallel work goes elsewhere:
- heavy number crunching goes to the GPU, see GPU compute;
CreateWorkerstarts a Web Worker written in JavaScript. It exchanges JSON messages and shares no memory.
Limits and common snags#
Sync()freezes a straight-lineWHILEloop. Add anEND, or use aDO … LOOP.- One
DO … LOOP, at the top level. Other loops areWHILEorREPEAT. - No waiting inside a
FUNCTION. Use a label withGOSUB, or a task. Waitis in seconds,Sleepin milliseconds. Both on game time.Printevery frame,Consoleonce.- Test task states bare:
IF TaskDone(h), never= 1. - In a label, each yield on its own line: no
IF … THEN Sync(), no comment afterSync().