Skip to content

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, process) reactions.

Reactor
└─ Targets
   ├─ Float target ─── source: IObservableFloatValue
   │   └─ Reactions
   │       ├─ Threshold (Above 100) → [IProcess]
   │       └─ Range (0..100)        → [IProcess]
   └─ Signal target ── source: signal Key
       └─ Reactions → [IProcess]

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 (FloatReaction in code).

Configure:

  • Source — ObservableFloatValueFromKey reading 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 process

The reaction's Process is what runs when the trigger fires. Pick a command (e.g. an Audio Source command) and inside it a Set Volume operation. Its Volume field already reads Context Value: the XP value that fired the reaction. You can point it at any other source instead; nothing is wired behind your back.

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


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 Key asset named LevelUpSignal.

Add a Signal target

Click + on Targets. Pick Signal Target.

  • Source — drag the LevelUpSignal Key in.

Inside, add a reaction. The signal trigger fires when Reactor.Receive(LevelUpSignal) is called externally. The reaction 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 JointBreakSignalSource on 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 process runs. When the process finishes, the reaction rearms for the next signal.

Overlap policy

If a second signal arrives while the process is still running, the trigger's overlap policy decides:

Strategy Behaviour
Complete and Restart Finish the current process, then start fresh.
Complete Finish without restarting.
Restart Restart immediately.
Toggle Pause Pause / unpause the running process.
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 — ObservableBoolValueFromKey on a "DoorOpen" key.
  • Add a reaction. Pick the On True trigger (or whatever the bool-trigger options call it). The process begins when the bool flips true and is asked to complete when the bool flips false.

This is what the retired Watcher component's value-changed entry did, but with typed access to the bool value inside the process 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 including EventFromAsset and SignalEventFromHub.
  • Process — 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 process 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's process runs while the attachable is held.

To react to several cycles at once, pick Cycle Group as the source and list the cycles inside it. It begins once when enough of them are entered at the same time (All, Any, or At Least N) and ends once when that stops being true. A crafting machine with three input slots is one Cycle target with a group of three Holding cycles in All mode, instead of one target per slot that re-checks the other two.


7. Retrigger and Interrupt

Two policies control how the Reactor handles edges and shutdown:

  • ProcessRetriggerStrategy — what happens when the trigger re-fires while the process is still running (per-reaction).
  • ReactionInterruptPolicy — what happens to in-flight bodies when a band trigger exits or the Reactor itself ends:
  • ForceComplete — snap each in-flight process to its final state.
  • Cancel — leave each alone to finish on its own.
  • Pause — freeze each on a band exit and resume it on the next fire, so time inside the band adds up across visits.

Pick ForceComplete for state changes that must land (sound off, animation reset). Pick Cancel for fire-and-forget effects that can play out. Pick Pause for progress that should only advance while the value is in the band, like a rope that gives after enough hard pulling.


8. What a Run Carries: Context, Context Value, Each Object

Every time a reaction fires, its process starts a run, and the run carries what started it. Your actions read it through three sources:

  • Context: the object the run is about. The object a GameObject or Exclusive reaction fired for, the object entering or leaving a cycle, the object a signal was sent with.
  • Context Value: the value that fired the run. The new number of a Float or Int reaction, the true/false of a Bool reaction or cycle, the value a signal carries.
  • Each Object: inside a Spawn or For Each, the object being spawned or visited right now. Context stays the outer object, so a spawned ring can be placed on the detected item: target Each Object, position from Context.

They hold for the whole run, even in steps that take several frames, and every run keeps its own, so overlapping runs never mix them up. When you add an action inside a reaction, a Spawn or a For Each, its target and value fields are pre-filled with the matching source. Change them freely.

To send a value with a signal, set Value on the Invoke Signal operation (a number, text, a Vector3, or an object). The receiving Signal reaction reads it with Context Value, or as its Context for an object.


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