This article explores ReactiveEntitySet, a feature coming in v2.1.0 of Reactive SO — a ScriptableObject-based reactive architecture for Unity.
Unity Asset StoreReactive SO | Game ToolkitsScriptableObject-based reactive architecture for Unity. Variables, Event Channels, Runtime Sets, GPU Sync, Reactive Entity Sets, and dedicated debugging windows.When I first explained ReactiveEntitySet to other developers, most of them responded with:
“Oh, so it’s like VariableSO but as an array?”
This is a fundamental misunderstanding. The surface-level similarities — ScriptableObject-based, state management, event firing — obscure what’s actually a paradigm shift in how we think about game state.
The Instancer Approach: “Ownership”
Unity Atoms’ Instancer creates runtime copies of a base VariableSO, giving each instance its own independent state.
flowchart LR
Enemy1 --> H1["HealthVariableSO (copy)"]
Enemy2 --> H2["HealthVariableSO (copy)"]
Enemy3 --> H3["HealthVariableSO (copy)"]
This is the ownership paradigm. Each object “owns” its state. It’s a natural extension of OOP thinking — objects encapsulate data and behavior.
The Payroll Analogy
Imagine each employee carrying their own payroll slip.
- Want to see everyone’s salary? You have to ask each person individually.
- Want to process salaries in bulk? You need to collect all the data and convert it into an array first.
The ReactiveEntitySet Approach: “Belonging”
ReactiveEntitySet manages data centrally. Each instance accesses its state via an ID.
flowchart BT
subgraph EnemySet["EnemySet (contiguous memory)"]
direction LR
S0["State0"] --- S1["State1"] --- S2["State2"]
end
E1["Enemy1 (ID=0)"] --> S0
E2["Enemy2 (ID=1)"] --> S1
E3["Enemy3 (ID=2)"] --> S2
This is the belonging paradigm. Entities “register” with a set. State “exists” in the set, not in the object.
The Accounting Department Analogy
The accounting department manages all employee salary data centrally.
- Want to see everyone’s salary? Just look at the accounting spreadsheet.
- Want to process in bulk? Hand over the spreadsheet as-is.
- Each employee accesses their data via their employee ID.
The Correct Lineage
flowchart LR
subgraph VL["VariableSO Lineage"]
direction LR
VS["VariableSO<br/>(single value)"] --> Inst["Instancer<br/>(copies)"] --> Q["???"]
end
subgraph RL["RuntimeSet Lineage"]
direction LR
RS["RuntimeSet<br/>(existence)"] --> RES["ReactiveEntitySet<br/>(existence + state)"]
end
ReactiveEntitySet is not an extension of VariableSO. It’s an extension of RuntimeSet.
RuntimeSet manages “who exists.” ReactiveEntitySet manages “who exists + what they have.” This is a continuous evolution.
The jump from VariableSO to ReactiveEntitySet is a discontinuity. The paradigm is different.
Physical Memory Layout Matters
This isn’t just an abstract conceptual difference. It’s a physical memory layout difference.
flowchart TB
subgraph Instancer["Instancer (scattered in memory)"]
direction LR
E1["Enemy1<br/>[Health]"]
E2["Enemy2<br/>[Health]"]
E3["Enemy3<br/>[Health]"]
end
subgraph RES["ReactiveEntitySet (contiguous in memory)"]
direction LR
Data["[Health0, Health1, Health2, ...]"]
end
This physical arrangement determines everything.
Optimizations Enabled by Central Aggregation
The paradigm shift from “ownership” to “belonging” enables optimizations that are simply impossible with Instancer:
| Optimization | Instancer | RES | Why |
|---|---|---|---|
| Pagination | No | Yes | Requires central ID→Index management |
| Slice-based detection | No | Yes | Can’t slice without contiguous array |
| Double buffering | No | Yes | Buffer concept doesn’t exist |
| Backward iteration | No | Yes | No central array to iterate |
Try implementing any of these with Instancer. You can’t.
Slice-based Change Detection
Using bitmasks to detect changes in 64-entity slices. Without a contiguous array, the concept of “slice” doesn’t exist.
The Core Question
“Why does each object need to own its own state?”
OOP’s answer:
“Because objects encapsulate data and behavior.”
ReactiveEntitySet’s counter:
“Data is more efficient when managed centrally. Objects only need behavior. State is not an object’s ‘property’ — it’s an ‘entry’ in a set.”
Relationship with ECS
ReactiveEntitySet is a hybrid that selectively adopts ECS concepts.
| ECS Principle | RES | Notes |
|---|---|---|
| Entity | Yes | MonoBehaviour.GetInstanceID |
| Component | Yes | TData struct |
| System | Partial | No explicit System class |
| Data-oriented | Yes | Contiguous array |
But there’s a crucial difference:
- ECS: Full scan every frame (Query)
- RES: Notify only on change (Reactive)
Where ECS forces “all or nothing,” RES lets you get data-oriented benefits while keeping your GameObject workflow.
A Note on Performance
When updates are infrequent — which is common for things like UI state, damage events, or AI decisions — the Reactive approach can be efficient since it only processes entities that actually changed.
The combination of contiguous memory layout and change-based processing seems to work well for these scenarios, though your mileage may vary depending on the specific use case.
Summary
ReactiveEntitySet is:
- Not an extension of VariableSO — A fundamentally different paradigm
- A natural evolution of RuntimeSet — From “existence” to “existence + state”
- “Ownership” to “Belonging” — Objects don’t hold state; state exists in the set
- A physical memory layout difference — From scattered to contiguous
- Enabling new optimization spaces — Impossible with Instancer
You don’t have to choose between “abandon everything for data-oriented” or “stay with OOP.” ReactiveEntitySet offers a gradual adoption path while keeping your GameObject workflow.