loomgui.com ↗

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:

DirectionMechanismWhen
C# -> UI[BridgeState] propertiesWhenever you assign the property
UI -> C#[BridgeAction] methodsWhenever the UI calls bridge.actions.foo(...)
C# -> UI[BridgeEvent] Event propertiesWhenever 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.

C#
[BridgeState]
public float Health { get; set; } = 100f;

// later, on damage:
Health = Mathf.Max(0f, Health - 10f);
TS
const bridge = useBridge()
// reactive: re-renders when Health changes
bridge.state.health  // number

The 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.

C#
[BridgeAction]
public void TakeDamage(float amount) {
  Health = Mathf.Max(0f, Health - amount);
}
TS
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.

C#
[BridgeEvent]
public Event<DamageEvent> Damaged { get; } = new();

// later:
Damaged.Fire(new DamageEvent { Amount = 10, IsCritical = true });
TS
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.