Repository navigation
Godot Integration
๐ Rzeka is engine-agnostic, but Godot is its primary target host. This page covers the discipline you need at the rzekaโGodot boundary - schedulers and scene-tree mutation safety from inside rzeka chains.
For the initial hosting setup (autoload + main-thread scheduler), see Getting Started. For wrapping async engine APIs into rzeka observables, see Async Operations.
Godot's _Ready, _Process, _PhysicsProcess, and signal callbacks all run on the main thread. Anything you Strand or Pluck from those entry points starts on the main thread, and rzeka itself never hops threads - the river stays single-threaded by design.
What does hop threads is anything async you introduce: Task, await, ResourceLoader.LoadThreadedRequest, Observable.FromAsync, HTTP clients. The moment one of those reports a result you may be on a worker thread, and any subsequent rzeka emission from that callback is on a worker thread too.
Expose a main-thread scheduler once at startup (see Getting Started) and use .ObserveOn(MainThread) to re-enter the river safely. See Async Operations for the full pattern.
๐๐งญ For scene-tree mutations from inside rzeka callbacks, use
CallDeferred(orSetDeferredfor property changes). This queues the change to Godot's idle window instead of executing it inline, which avoids two classes of bugs:
- Re-entrant tree modification while Godot is mid-traversal from another signal or callback.
- Calling tree-modifying methods from a callback that turns out to be off the main thread (scheduler hops inside
SelectMany,Observable.FromAsync,awaitcontinuations).
Even when your rzeka chain looks like it's safely on the main thread, individual Rx operators can re-enter or hop schedulers in ways that are easy to miss. CallDeferred is the cheap defence:
Q += rzeka.Loom<GameOpened, MainSceneLoaded>(
this,
spell => spell.SelectMany(gameStarted => rzeka
.Ask<LoadSceneRequest, LoadSceneResponse>(
this,
new LoadSceneRequest(_mainLevel).WithCircumstances(gameStarted))
.Take(1)
.Where(r => r.WasSuccessful)
.Reacting(r => CallDeferred(Node.MethodName.AddChild, r.PackedScene.Instantiate()))
.Select(r => new MainSceneLoaded().WithCircumstances(gameStarted, r))));Prefer the typed MethodName form (Node.MethodName.AddChild) over the string form ("add_child"). It uses Godot 4's StringName cache, has compile-time safety, and renames cleanly. For deferring a lambda rather than a single method call, use Callable.From(() => /* work */).CallDeferred().
The rule is specifically about mutating the scene tree or scene-aware properties. Reactions that don't touch the tree run inline:
| Reaction | Inline OK | Defer? |
|---|---|---|
AddChild, RemoveChild, Reparent, SetParent
|
โ | |
QueueFree |
โ (already deferred internally) | |
| Setting transform / physics / render properties on Nodes | โ (use SetDeferred) |
|
| Updating your own non-Node state (services, caches, fields) | โ | |
rzeka.Pluck(...) more matter |
โ | |
rzeka.Whisper(...) log messages |
โ | |
| Pure computation, math, string building | โ |
๐๐งจ With
CallDeferred(AddChild, node), the child is instantiated immediately but not in the tree until the next idle frame.
This bites when later matter in the same chain assumes the mutation is complete:
.Reacting(r => CallDeferred(Node.MethodName.AddChild, r.PackedScene.Instantiate()))
.Select(r => new MainSceneLoaded().WithCircumstances(gameStarted, r))MainSceneLoaded ships on the current frame. Anything downstream subscribing to MainSceneLoaded that immediately reaches into the new subtree (scene.GetNode("Player")) sees an empty tree until next idle frame.
For high-level matter that means "the scene's load request resolved", this is fine. For code that wants to act on a freshly mounted subtree, either:
- Move the dependent work into the deferred call itself, or
- Have the spawned node
Plucksomething likeSceneTreeAttachedfrom its own_Readycallback. Then the downstream Loom listens forSceneTreeAttachedinstead ofMainSceneLoaded, and timing is guaranteed.
The second pattern is preferred for rzeka systems - it keeps causality explicit (the matter chain shows when the scene actually became live) and avoids fragile assumptions about frame timing.