State, collections, and events
A bridge surface is built from three things: [BridgeState] properties (engine
→ UI state), reactive collections for lists and maps, and Event<T> for one-shot
fan-outs.
State properties
State is a plain { get; set; } property tagged [BridgeState]. Assigning it
pushes the new value to the UI; reading it returns the current value. Set the
initial value in the property initializer.
[BridgeState] public int Score { get; set; } = 0;
// somewhere in your game logic:
Score = 100; // triggers a state push
int now = Score; // read back The setter is required: Loom rewrites it to sync, so assigning the property is
what pushes. There’s no separate subscribe step and no .Value wrapper; the
property is the value. A push is skipped when the assigned value equals the
current one.
Nested DTO state works the same way: a [BridgeState] property whose type is a
DTO pushes wholesale when reassigned. Give the nested class its own [BridgeState] members to push a single inner field on its own. See DTO conventions.
Collections
You have two ways to expose a list or map, depending on how it changes.
Snapshot collections
List<T>, T[], and Dictionary<K,V> are pushed as a whole snapshot. The UI
sees a typed array or object. Reassign the property to sync; mutating it in place
won’t push.
[BridgeState] public string[] Messages { get; set; } = new[] { "hello" };
Messages = new[] { "hi", "there" }; // pushes the new snapshot
// Messages[0] = "x"; // does not push; reassign instead Reactive collections
For lists and maps that change often, ReactiveList<T> and ReactiveMap<K,V> push granular operations (a single insert, remove, move, or element change)
instead of resending the whole collection. Declare them get-only and mutate
in place:
[BridgeState] public ReactiveList<Player> Players { get; } = new();
Players.Add(new Player { Name = "Ada", Score = 0 });
Players.RemoveAt(0);
Players[0].Score += 1; // pushes only that element's score Element types use [BridgeState] properties so a single field change pushes just
that field, not the whole element. ReactiveMap keys must not contain . or #.
The UI sees a keyed, ordered collection it can render with a keyed <For>; C#
owns the order.
Event<TPayload>
A one-shot fan-out. C# fires with a payload; the UI receives a typed callback.
[BridgeEvent] public Event<DamageEvent> Damaged { get; } = new();
Damaged.Fire(new DamageEvent { Amount = 10 });onEvent('damaged', (e) => {
flashRed(e.amount)
})Events do not buffer. If the UI is not yet connected (e.g. boot phase), fired events are dropped. If your engine fires an event before the UI is up, either delay the fire or use a state field instead.
Threading
State, collections, and events are all designed for the main thread. Pushing
from a background thread is undefined behaviour. If you need to update from a
worker, marshal back to the main thread first (e.g. UnityMainThreadDispatcher.Enqueue(...), or your engine’s equivalent).