Coroutines
Async is a cooperative coroutine scheduler. A coroutine lets you write “do X, wait,
then do Y” as straight-line code — sequenced animation, timed events, simple AI — instead
of hand-rolling timers and state flags. Start one with Async.run { ... }; inside it,
Async.wait(seconds) pauses without blocking the frame and Async.nextFrame() resumes on
the next tick.
The engine auto-ticks the scheduler every frame — after physics, before Game.update —
so the game wires nothing into its own update loop. A game that never imports Async
pays nothing.
A coroutine is a Wren Fiber, so this is cooperative and single-threaded: it is
frame-scheduling, not parallelism or threads. A coroutine runs on the main thread until
it suspends; a coroutine that never yields (an infinite loop with no wait/nextFrame)
blocks the frame, exactly like any other infinite loop. Timing is measured in scene time
(the frame delta the engine passes), not wall-clock, so coroutines stay in lockstep with the
simulation and behave deterministically under a fixed step or a headless run (engine
ADR 0037).
Unlike an instance API, every Async method is static — call it on the class.
Async.run(body)
Section titled “Async.run(body)”Starts a coroutine. body is a zero-arg block (or function); it runs immediately
until its first Async.wait / Async.nextFrame suspend, or until it returns.
Parameters:
body(Fn) — A zero-argument block. Written most naturally as a trailing block:Async.run { ... }.
import "engine" for Async
Async.run { door.open() Async.wait(1.5) // pause 1.5s of scene time without blocking the frame enemy.spawn()}Async.wait(seconds)
Section titled “Async.wait(seconds)”Suspends the current coroutine for seconds of scene time, then resumes it on the first
frame at or after that delay has elapsed. Only call this from inside a coroutine (a body
passed to Async.run).
Parameters:
seconds(Num) — How long to suspend, in scene seconds (accumulated frame deltas). A value<= 0behaves likeAsync.nextFrame().
Async.run { hud.show("Ready") Async.wait(2.0) hud.show("Go!")}Async.nextFrame()
Section titled “Async.nextFrame()”Suspends the current coroutine until the next frame, then resumes. Use it to spread work across frames or to step one iteration of a loop per frame.
Async.run { for (i in 1..3) { countdown.set(3 - i + 1) // 3, 2, 1 — one per frame Async.nextFrame() } race.start()}Async.pending
Section titled “Async.pending”Returns: Num — The number of coroutines currently suspended (waiting on a delay or
the next frame). A coroutine that has run to completion is not counted.
if (Async.pending == 0) { // every sequenced coroutine has finished}Async.clear()
Section titled “Async.clear()”Cancels every suspended coroutine, dropping them without resuming. Call it when the work they were sequencing is no longer relevant — for example on a scene change or a reset — so a stale coroutine can’t fire into the new scene.
Async.clear() // e.g. before loading the next levelThe cooperative model
Section titled “The cooperative model”- The engine drives it.
Async.tick_runs once per frame from the engine (after physics, beforeGame.update). You never tick the scheduler yourself, and there is no per-game wiring. - Scene time, not wall-clock.
wait(seconds)counts accumulated frame deltas, so a coroutine advances with the simulation and is deterministic under a fixed step / headless run — the same reason the rest of the engine’s timing uses scene delta. - Cooperative, single-threaded. Coroutines run on the main thread and only ever suspend
at a
wait/nextFrame. This is frame-scheduling, not parallelism — a coroutine that loops forever without yielding blocks the frame. - A coroutine started mid-frame first resumes the following frame.
Async.runruns the body up to its first suspend right away; from there the scheduler resumes it on subsequent frames.
Worked example
Section titled “Worked example”import "engine" for Async
class Game { init() { // A self-sequencing opener: open the door, wait, spawn, then flash the HUD next frame. Async.run { door.open() Async.wait(1.5) // pause 1.5s without blocking the frame enemy.spawn() Async.nextFrame() // resume next frame hud.flash() } }
update(dt) {} // nothing to tick — the engine resumes coroutines for you draw() {}}See the Coroutine Demo example (apps/coroutine_demo) for a
self-cycling traffic light and a per-frame counter, both driven entirely by coroutines.