Skip to content

Getting Started

kimja edited this page Jul 31, 2026 · 9 revisions

🌱 Getting Started

πŸ“œ New to Rx? Rzeka is built on Rx.NET and assumes you know your way around the core operators (Observable.Create, Subscribe, Select, SelectMany, ObserveOn) and the IScheduler concept. If those are unfamiliar, check out official Rx.NET intro before going deeper.

πŸ“œπŸŒ± Make sure to also take a look at little-river, a tiny example Godot game using rzeka.

Create a single river at startup and share its IRzeka reference with the systems that need it:

IRzeka rzeka = new Spring().Create("Styx", mainThread: ...); // "mainThread"? hmmm, see below!

πŸ“œπŸ§¨ One rzeka per application. Spring enforces this - calling Create a second time throws an error. Circumstance chain tracking currently depends on a single shared context.

Rzeka normally lives for the application's lifetime, so you rarely dispose it. If you do need to do that - tests, hot-reload, multi-scene cleanup - rzeka.Dispose() releases Library and Eris cleanly.

πŸ“œπŸ§šπŸ»β€β™€οΈ The name passed to Create serves currently a purely mythological role, it has no direct usage in rzeka codebase, but one should have the capacity to name the river that they live along.

Spring.Create parameters

Spring.Create takes one required mainThread scheduler and two optional engine-adapter hooks:

IRzeka rzeka = new Spring().Create(
    name:                   "Crash Bandicoot",
    mainThread:             MainThread,
    describeOwner:          who => (who as Node)?.Name,
    onUnhandledSourceError: (spell, ex) => GD.PrintErr(ex)
);
Parameter Type Purpose
mainThread IScheduler Required. The scheduler representing the engine's main thread. All conjuring spells (Strand, Loom, Shuttle) auto-ObserveOn this scheduler before publishing matter, so matter publication is guaranteed to be on the main thread. This scheduler is also exposed for your own use as rzeka.MainThread. See Threading.
describeOwner Func<object, string?> Optional. Extracts an instance label from a who object. Eris uses it to distinguish instances of the same type in the debugger - e.g. "Enemy (goblin-3)" vs "Enemy (troll-7)". Without it, all instances of the same type look identical.
onUnhandledSourceError Action<ISpell, Exception> Optional. Called after rzeka's error boundary has caught and whispered an unhandled source error. Use for your own (crash) logging needs. See Error Boundary.

Hosting rzeka in Godot

The simplest pattern is a Autoload that owns the river instance and a main-thread scheduler:

using System.Reactive.Concurrency;
using System.Threading;
using Godot;
using Rzeka;
using Rzeka.Dev;

public partial class LittleSource : Node
{
    public static IRzeka Rzeka { get; private set; }

    CollectibleDisposable Q { get; set; } = new();

    public override void _EnterTree()
    {
        SynchronizationContext.SetSynchronizationContext(new GodotMainThreadContext());
        var mainThread = new SynchronizationContextScheduler(SynchronizationContext.Current);

        Spring spring = new();

        // See notes on Eris debugger, you will want that only in dev builds
        Q += spring.EnableDevServer();

        Rzeka = spring.Create(
            "little-river",
            mainThread: mainThread,
            describeOwner: who => (who as Node)?.Name
        );

        GD.Print("🌊 Rzeka is operational!");
    }

    public override void _ExitTree()
    {
        Q.Dispose();
        Rzeka.Dispose();
    }

    // Posts callbacks to Godot's main thread via CallDeferred - backs the MainThread scheduler above.
    private sealed class GodotMainThreadContext : SynchronizationContext
    {
        public override void Post(SendOrPostCallback d, object state) =>
            Callable.From(() => d(state)).CallDeferred();
    }
}

Nodes access the river via River.Rzeka and the main-thread scheduler via River.MainThread.

All Godot lifecycle callbacks (_Ready, _Process, _PhysicsProcess, etc.) run on the main thread, so Strand, Pluck, Loom, and Weave all work without any extra setup.

Async operations that return on a background thread need manual circumstance handling and must be shifted back to River.MainThread before their results re-enter the river - see Async Operations.

For the live debugger during development, see Eris.

Once you summoned your rzeka, you are ready to shape Matter.

Threading

The mainThread scheduler does two jobs:

  1. Automatically set every output matter back to the main thread. Every Strand, Loom, and Shuttle has .ObserveOn(mainThread) injected just before publishing matter. How cheap that is depends on the scheduler you provide - see Picking a main-thread scheduler.
  2. Surfaces as rzeka.MainThread. For the post-await case in your own lambdas - awaits resume on arbitrary threads, so you need to get them back on the main thread yourself:
.Ask<LoadSceneRequest, LoadSceneResponse>(...)
.SelectMany(async r => {
    var scene = r.PackedScene.Instantiate();
    _parent.AddChild(scene);
    await scene.ToSignal(scene, Node.SignalName.Ready);
    return new SceneReady().WithCircumstances(r);
})
.ObserveOn(rzeka.MainThread)   // post-await scheduling - your responsibility

Clone this wiki locally