A Loom bridge is a C# class tagged with [Bridge]. It defines the
contract between your game code and your UI. Three things cross the wire:
| Direction | Mechanism | When |
|---|---|---|
| C# -> UI | [BridgeState] properties | Whenever you assign the property |
| UI -> C# | [BridgeAction] methods | Whenever the UI calls bridge.actions.foo(...) |
| C# -> UI | [BridgeEvent] Event properties | Whenever C# calls .Fire(payload) |
Each member of your bridge class falls into one of these three buckets.
State
State is reactive. You declare a plain { get; set; } property; assigning it
pushes to the UI, which sees a typed Solid signal-like accessor.
[BridgeState]
public float Health { get; set; } = 100f;
// later, on damage:
Health = Mathf.Max(0f, Health - 10f);const bridge = useBridge()
// reactive: re-renders when Health changes
bridge.state.health // numberThe source generator names match by convention: C# Health becomes TS health. Acronyms follow C# casing, so HudVisible becomes hudVisible.
Container types
Beyond scalars, Loom provides:
- Nested DTO classes. A
[BridgeState]property can itself be a plain C# class with its own[BridgeState]properties. The generated TS types nest the same way; assigning a leaf pushes just that leaf. - Snapshot collections.
List<T>,T[],Dictionary<K,V>. The UI sees a typed array or object; reassign the whole property to sync (Items = newList). - Reactive collections.
ReactiveList<T>/ReactiveMap<K,V>. For lists and maps that change often, these push granular per-element inserts, removes, and updates instead of the whole snapshot. Mutate them in place (Players.Add(p),Players[i].Score += 1); element DTOs use[BridgeState]properties so a single field change pushes only that field.
See the State, collections & events reference for the exact runtime semantics.
Built-in loom.* state
Beyond the state you declare, every bridge carries a read-only loom.* block
that Loom maintains: the active scene name, viewport size, device pixel ratio,
and connection status. The UI reads it as bridge.state.loom.* without
declaring anything. See Built-in state.
Actions
Actions are methods. The UI calls them; C# executes them on the main thread during the next pump.
[BridgeAction]
public void TakeDamage(float amount) {
Health = Mathf.Max(0f, Health - amount);
}bridge.actions.takeDamage(10)An action takes at most one parameter; pass a DTO when you need several fields.
On the UI side, bridge.actions.foo(...) always returns a Promise. A void C# action resolves it with no value; an async Task action resolves it
when the work completes; an async Task<T> action resolves it with the returned
value, so the UI can await a result:
// C#: async Task<bool> SubmitScore(ScoreEntry entry)
const ok = await bridge.actions.submitScore({ name: 'Ada', score: 9000 }) // bool If you’d rather not await, you can also surface results by mutating state from inside the action, the UI sees the change reactively.
Events
Events are one-shot fan-outs. C# fires them; the UI listens.
[BridgeEvent]
public Event<DamageEvent> Damaged { get; } = new();
// later:
Damaged.Fire(new DamageEvent { Amount = 10, IsCritical = true });import { onEvent } from '@loomgui/bridge'
onEvent('damaged', (e) => {
// e: { amount: number; isCritical: boolean }
flashRed()
})Events carry typed payloads. The DTO class (DamageEvent above) is a plain
C# class with [Serializable]-style fields. The source generator picks up
its shape and types the TS callback parameter.
See the DTO conventions reference for the rules.
Example: the sample HUD
The in-repo Unity sample’s unity/sample/Assets/Scripts/SampleBridge.cs puts all three together in ~30 lines. Its unity/sample/UI/src/screens/Hud.tsx shows the matching Solid component.