Tutorial: Reactive Logic with Reactor¶
Hook scene behaviour onto changes — a value crossing a threshold, an event firing, a signal arriving, a frame cycle beginning. The Reactor component is the host for that wiring: zero scripting, all in the Inspector, all reusing the same operation/command vocabulary you already use elsewhere.
By the end of this tutorial you'll have a Reactor that:
- Fires a "level up" sound when an XP float crosses 100.
- Plays an animation while a signal is being broadcast.
- Stops a process when a bool flips to true.
Prerequisites: Basic familiarity with the Process system and DataStorage.
Mental Model¶
A Reactor hosts a list of targets. Each target owns one source and a list of (trigger, body) reactions.
Reactor
└─ Targets
├─ Float target ─── source: IObservableFloatValue
│ └─ Reactions
│ ├─ Threshold (Above 100) → [IProcess body]
│ └─ Range (0..100) → [IProcess body]
└─ Signal target ── source: SignalType
└─ Reactions → [IProcess body]
Each target is dedicated to one source. Inside the target, triggers decide when to fire and bodies decide what to run.
The Reactor itself is the lifecycle: when it begins, every target subscribes; when it ends, every target unsubscribes.
1. Add the Reactor¶
Create a GameObject named GameLogic (or use any controller you already have). Add the Reactor component.
Add Component → Jungle → Reactions → Reactor
You'll see a Targets list. Each item is a typed target.
2. Set Up an XP Threshold Reaction¶
Goal: when the player's XP float value crosses 100, play a level-up sound.
Prepare the source value¶
Create a DataStorageAsset named PlayerStats with a float entry keyed by an XP Key. (See Data for the basics.)
Add a Float target¶
Click + on Targets. Pick Float Target (or FloatReactionTarget, whichever the picker shows).
Configure:
- Source —
ObservableFloatValueFromKeyreading the XP key on PlayerStats.
Add a reaction with a threshold trigger¶
Inside the target, click + on Reactions. Pick Threshold Float Trigger:
| Field | Value |
|---|---|
| Comparison | Above |
| Threshold | 100 |
| Hysteresis | 5 (prevents flutter near the boundary) |
Wire the body¶
The reaction's Body is a List<IProcess>. The picker is filtered to commands and operations that contain at least one float-reactive op — anything implementing IFloatReactionTarget.
Pick a command (e.g. an AudioCommand) and inside it pick a PlayAudioOperation. The Reactor auto-wires the operation's ReactionInput: SmartFloat slot to the observed XP value the first time the body dispatches — no manual setup needed.
Press Play and bump the XP key past 100. The sound plays. Drop it back below 95 (95 = 100 − hysteresis) and the threshold re-arms.
Triggers vs sources
The source decides what's observed. The trigger decides what condition counts as "fire". A single source can host many triggers in the same target — fire on each change, fire when above 100, fire when between 50 and 75 — each running its own body.
3. Run a Process While a Signal Is Broadcasting¶
Goal: while a "level-up celebration" signal is being broadcast, play a flashing animation. Stop the animation when the signal stops.
Prepare the signal¶
Create a SignalType asset named LevelUpSignal.
Add a Signal target¶
Click + on Targets. Pick Signal Target.
- Source — drag the
LevelUpSignalSignalType in.
Inside, add a reaction. The signal trigger fires when Reactor.Receive(LevelUpSignal) is called externally. The body runs the matching process.
Raise the signal¶
The signal arrives via someone calling Reactor.Receive(signal). Common ways:
- A Raise Signal On Transmitter or Raise Signal On Owner Transmitter operation wired to fire from another process. (See LocalSignalTransmitter for the per-instance pub/sub side of signals.)
- A
JointBreakSignalSourceon a joint, which pushes a signal into a configured Reactor when the joint breaks. - Your own code calling
reactor.Receive(LevelUpSignal).
When the signal arrives, the body runs. When the body finishes, the reaction rearms for the next signal.
Overlap policy¶
If a second signal arrives while the body is still running, the trigger's overlap policy decides:
| Strategy | Behaviour |
|---|---|
| Complete and Restart | Finish the current body, then start fresh. |
| Complete | Finish without restarting. |
| Restart | Restart immediately. |
| Toggle Pause | Pause / unpause the running body. |
| Ignore | Discard the new signal. |
Default is usually Complete and Restart.
4. Bool Target — Run While True¶
Goal: while a "Door Open" bool is true, play an ambient sound.
Add a Bool target¶
- Source —
ObservableBoolValueFromKeyon a "DoorOpen" key. - Add a reaction. Pick the On True trigger (or whatever the bool-trigger options call it). The body begins when the bool flips true and is asked to complete when the bool flips false.
This is exactly how the previous Watcher's ValueChangedEntry worked, but with typed access to the bool value inside the body if you need it.
5. Event Target¶
Goal: when a IEvent raises — a UI button, a video finished, a duration timer — run something.
- Source — any
IEvent. The picker offers all event types includingEventFromAssetandSignalEventFromHub. - Body — runs once per raise. The trigger here is simply "raised".
This is the canonical way to wire a designer-configured "X happens → do Y" rule without writing a MonoBehaviour.
6. Frame Cycle Target¶
Some targets watch a frame cycle — a per-frame begin/end pair, often emitted by another component's lifecycle. Use these when you want a body to run while something is happening (and stop when it stops), rather than only on edges.
A common case: subscribe to an AttachableObject's Attached cycle so a Reactor body runs while the attachable is held.
7. Retrigger and Interrupt¶
Two policies control how the Reactor handles edges and shutdown:
- ProcessRetriggerStrategy — what happens when the trigger re-fires while the body is still running (per-reaction).
- ReactionInterruptPolicy — what happens to in-flight bodies when the Reactor itself ends:
ForceComplete— snap each in-flight process to its final state.Cancel— leave each at its current state and stop ticking it.
Pick ForceComplete for state changes that must land (sound off, animation reset). Pick Cancel for fire-and-forget effects that should just stop.
8. Auto-Wiring Body Inputs¶
Many body operations expose a SmartX ReactionInput slot — e.g. IFloatReactionTarget exposes SmartFloat ReactionInput. On first dispatch the Reactor wires the slot to the observed value. The operation sees the current value when it runs without any manual configuration. Disable this only when you specifically want to feed the operation a different source.
9. Reactor vs LocalSignalTransmitter¶
Both route signals, but they're different surfaces:
| Surface | Routing scope | When to use |
|---|---|---|
| Reactor + Signal target | The Reactor's lifecycle. Push via Reactor.Receive(signal). |
When the signal feeds into typed reactions on a Reactor. |
| LocalSignalTransmitter | Per-GameObject. Subscribe via SignalEventFromHub, raise via RaiseSignalOnTransmitter. |
When you want pub/sub on a single GameObject without going through a Reactor. |
A SignalEventFromHub can also be plugged into a Reactor's Event target, so the two surfaces compose: instance-local pub/sub on the LocalSignalTransmitter side, typed reactions on the Reactor side.
Next Steps¶
- Setter tutorial — push values into sinks (the inverse of Reactor).
- Detector tutorial — source GameObjects for reactor bodies.
- LocalSignalTransmitter — per-instance pub/sub on signals.
- Reactor reference — full property list.