Coroutine typedef

Coroutine = Iterable Function()

A resumable piece of gameplay logic, written as a sync* generator.

Iterable entrance(Entity self) sync* {
  yield 0.5;                    // wait half a simulated second
  opacity[self] = 1.0;
  yield null;                   // wait one fixed step
  yield WaitUntil(() => landed);
  playSound(self);
}

Why sync* and not async*

This looked like Stream<FutureOr<double?>> Function() first, which is the obvious Dart spelling and is wrong here for a reason that has nothing to do with style. An async* generator resumes on a microtask, and this engine requires every component write to land between MemoryPool.beginTick and commitTick - data_layout.dart asserts it, because beginTick copies the last published snapshot over the write slot and would silently discard anything written outside that window.

A coroutine exists to write component data after waiting. Under async* every one of those writes lands on a microtask after commitTick and is therefore thrown away: silently in release, on an assert in debug. Not intermittently - every time, for every write after the first yield.

sync* resumes synchronously, so CoroutineScheduler.step can drive it from inside the tick window and the writes land where they must. It is also what Unity's IEnumerator has always been, for what is probably the same reason.

What a yield may be

  • null - resume on the next fixed step.
  • a num - resume after that many simulated seconds, accumulated from Game.fixedTimeStep. Simulated rather than wall-clock, so a coroutine replays identically; Future.delayed would not.
  • a YieldInstruction - resume when it says so, polled once per step.
  • another Iterable - run it to completion first, then carry on. Nesting is a plain stack, so a coroutine can be composed of coroutines without either knowing about the other.

Anything else is a programming error and throws, rather than being silently treated as "next frame".

The element type is left off deliberately. A coroutine yields a mixed bag by design - a null, a number, an instruction, another coroutine - so there is no element type worth writing, and Iterable is both shorter and more honest than pinning it to Object?.

Implementation

typedef Coroutine = Iterable Function();