v0.1.x → v0.2.x
Loom v0.2 is a breaking release with four source changes you make by hand:
- Bridge setup. Your
[Bridge]class inherits a base class, and a single Loom UI component does the wiring and runtime setup you used to write yourself. - Bridge state API.
Observable<T>andObservableList<T>are gone. State is now plain{ get; set; }properties; lists and maps useReactiveList<T>/ReactiveMap<K,V>(or snapshot collections). - Membership. The
[BridgeState]and[BridgeEvent]member attributes are gone. Apublicproperty is state and apublic Event<T>property is an event by convention. Actions stay explicit: tag each UI-callable method[BridgeAction](a plainpublicmethod is internal, not an action). - Flattened UI surface. The UI reads state and calls actions directly on the
bridge:
bridge.currentScreen,bridge.addPoint(), instead of the oldbridge.state.*/bridge.actions.*split.
What changed at a glance
| v0.1.x | v0.2.x | |
|---|---|---|
| Bridge class | class UIBridge | class UIBridge : LoomBridgeBase |
| Membership | [BridgeState] / [BridgeAction] / [BridgeEvent] tags | state & events by convention; actions tagged [BridgeAction] |
| UI bridge access | bridge.state.x / bridge.actions.y() | bridge.x / bridge.y() |
| Construction | new UIBridge() in your bootstrap | Loom constructs it |
| Registration | you call RegisterWithLoom() | automatic |
| Runtime | you call LoomRuntime.Initialize() / Shutdown() | automatic |
| Scene setup | a bootstrap MonoBehaviour + Loom View | one Loom UI component |
| Bridge access | MyBootstrap.Bridge.X | UIBridge.Instance.X |
| Scalar state | Observable<int> X { get; } = new(0) | int X { get; set; } |
| Read / write state | X.Value | X |
| List state | ObservableList<T> | ReactiveList<T> (or snapshot List<T>) |
long / ulong in TS | number | bigint |
Update the Unity package.
Update
com.loomguito0.2.0, then restart the Unity Editor so the new native plugin loads.Inherit
LoomBridgeBase.Your
[Bridge]class must now inheritLoomBridgeBase.[Bridge] public partial class UIBridge { public partial class UIBridge : LoomBridgeBase { // members }Move state from
Observable<T>to plain properties.A bridge state member is now a plain
{ get; set; }property. Assigning it pushes to the UI. Remove theObservable<T>wrapper and the.Valueaccessor everywhere (reads and writes use the property directly), and move the initial value into the property initializer.[Bridge] public partial class UIBridge : LoomBridgeBase { [BridgeState] public Observable<int> Score { get; } = new(0); [BridgeState] public int Score { get; set; } = 0; [BridgeAction] public void AddPoint() { Score.Value += 1; } [BridgeAction] public void AddPoint() { Score += 1; } }If you used
Observable<T>.Subscribe(...)for engine-side reactivity, move that logic to where you set the value (a plain property has noSubscribe).Move lists to
ReactiveList<T>(or a snapshot collection).ObservableList<T>is replaced by two options:ReactiveList<T>/ReactiveMap<K,V>: declare get-only and mutate in place (Add,Remove,Clear, indexer). Each change pushes granularly.- Snapshot
List<T>/T[]/Dictionary<K,V>: a{ get; set; }property you reassign to sync (mutating in place won’t update the UI).
[BridgeState] public ObservableList<string> Messages { get; } = new(); [BridgeState] public ReactiveList<string> Messages { get; } = new();Remove all state/event attributes.
State and events are now by convention, so the
[BridgeState]and[BridgeEvent]attributes are gone. Apublicproperty is state and apublic Event<T>property is an event. Actions stay explicit: keep[BridgeAction]on every UI-callable method. Delete only the state/event tags:[Bridge] public partial class UIBridge : LoomBridgeBase { [BridgeState] public int Score { get; set; } = 0; public int Score { get; set; } = 0; [BridgeAction] public void AddPoint() { Score += 1; } // keep, actions stay tagged [BridgeEvent] public Event<ScoredEvent> Scored { get; } = new(); public Event<ScoredEvent> Scored { get; } = new(); }[BridgeAction]may also go on a method of a nested state DTO; it surfaces at its dotted path on the bridge (bridge.boot.setProgress(v)). If a state/event member must staypublicfor other game code but isn’t part of the UI contract, tag it[BridgeIgnore](or make it non-public). Use[BridgeName("...")]to keep a wire name that no longer matches the member name.One footgun the convention introduces: a computed get-only property (
public int Doubled => Score * 2;) is now treated as state and won’t update, mark it[BridgeIgnore]or make it a method.Delete your bootstrap and add the Loom UI component.
Remove the MonoBehaviour that constructed the bridge and started the runtime. Everything it did is now handled for you:
// DELETE this whole file (e.g. LoomBootstrap.cs). [DefaultExecutionOrder(-1000)] public class LoomBootstrap : MonoBehaviour { public static UIBridge Bridge { get; private set; } private void Awake() { Bridge = new UIBridge(); Bridge.RegisterWithLoom(); LoomRuntime.Initialize(/* … */); } private void OnDestroy() => LoomRuntime.Shutdown(); }In your startup scene, run
Loom -> Setup UI in Current Scene. This adds a single Loom UI GameObject that builds the canvas, input capture, and ticker and initializes the runtime on Play. Delete the old “Loom Bootstrap” GameObject if you had one.See Loom UI for the full component reference.
Update how you reach the bridge.
The generator now emits a static
Instanceaccessor on your bridge class. Replace references to your old bootstrap singleton, and drop.Value:LoomBootstrap.Bridge.Score.Value = 10; UIBridge.Instance.Score = 10;Flatten how the UI reads state and calls actions.
The UI bridge no longer has a
.state/.actionssplit. State, actions, and nested objects live directly on the bridge. Find-and-replace in your Solid components:bridge.state.currentScreen bridge.currentScreen bridge.actions.addPoint() bridge.addPoint()A nested-DTO action would look like
bridge.boot.setProgress(v).Check
long/ulongreads in your UI.64-bit integers now cross to TypeScript as
bigint(they werenumber). If your UI reads along/ulongstate field or event payload, use bigint literals (1n), andString(v)to display, sinceJSON.stringifythrows on a bigint. If you only need a large id as text, use astringfield instead.
New in v0.2: built-in loom.* state
Every bridge now exposes a read-only loom block (active scene name, viewport
size, device pixel ratio, connection status) with no declaration on your part.
The UI reads it as bridge.loom.*. It’s additive, so adopting it is
optional. See Built-in Loom state.