Skip to content

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.

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()
}

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 <= 0 behaves like Async.nextFrame().
Async.run {
hud.show("Ready")
Async.wait(2.0)
hud.show("Go!")
}

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()
}

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
}

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 level
  • The engine drives it. Async.tick_ runs once per frame from the engine (after physics, before Game.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.run runs the body up to its first suspend right away; from there the scheduler resumes it on subsequent frames.
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.