Engineering

Ownership vs Belonging: A New Paradigm for State Management in Unity

  • Unity
  • ScriptableObject
  • State Management
  • ECS
  • Architecture
  • Game Development

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:

OptimizationInstancerRESWhy
PaginationNoYesRequires central ID→Index management
Slice-based detectionNoYesCan’t slice without contiguous array
Double bufferingNoYesBuffer concept doesn’t exist
Backward iterationNoYesNo 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 PrincipleRESNotes
EntityYesMonoBehaviour.GetInstanceID
ComponentYesTData struct
SystemPartialNo explicit System class
Data-orientedYesContiguous 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:

  1. Not an extension of VariableSO — A fundamentally different paradigm
  2. A natural evolution of RuntimeSet — From “existence” to “existence + state”
  3. “Ownership” to “Belonging” — Objects don’t hold state; state exists in the set
  4. A physical memory layout difference — From scattered to contiguous
  5. 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.


References

PostReactiveEntitySet: Bringing ECS Insights to GameObject WorkflowsNotes from researching EnTT, Entitas, Flecs, Bevy, Svelto, and DOTS — and how ReactiveEntitySet combines Sparse Set storage with reactive change detection to give Unity GameObject projects a pragmatic data-oriented middle ground.