Engineering

ReactiveEntitySet: Bringing ECS Insights to GameObject Workflows

  • Unity
  • ScriptableObject
  • ECS
  • Architecture
  • Reactive
  • Game Development

This article introduces ReactiveEntitySet, a feature coming in v2.1.0 of Reactive SO.

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.

It’s based on my research into ECS frameworks and an attempt to bring some of their benefits to traditional Unity development.

In the previous article, I discussed the paradigm shift from “ownership” to “belonging.” This article focuses on the technical decisions behind ReactiveEntitySet.

The Uncomfortable Either-Or

Unity developers often face a binary choice:

  1. Traditional OOP — GameObject workflows, scattered state, limited optimization options
  2. Full ECS migration — Steep learning curve, existing code becomes obsolete, completely new workflow

It can feel like being told “abandon everything if you want data-oriented benefits.”

Is there a third way?

Learning from ECS Frameworks

I spent time studying how various ECS frameworks approach entity management:

FrameworkLanguageStorageReactive Feature
EnTTC++Sparse SetSignal
EntitasC#GroupReactiveSystem
FlecsC/C++ArchetypeObserver
BevyRustArchetypeChange Detection
Svelto.ECSC#GroupIReactOn* interfaces
Unity DOTSC#ArchetypeChangeFilter

Sparse Set vs Archetype

AspectSparse SetArchetype
Add/RemoveO(1)O(n) component moves
IterationSlower (indirection)Fast (contiguous)
MemoryHigher (sparse array)Lower (packed)
Best forFrequent changesLarge-scale iteration

What I found interesting: Sparse Set’s weakness — “slow iteration” — assumes you’re iterating every frame.

Reactive Patterns in ECS

I found that “process only on change” isn’t unique to reactive programming — ECS frameworks have similar concepts:

  • Entitas ReactiveSystem: Collects only changed entities via GroupObserver
  • Flecs Observer: Reacts to OnAdd / OnRemove / OnSet events
  • Bevy Change Detection: Changed<T>, Added<T> query filters
  • Svelto.ECS: IReactOn* interfaces for add/remove/swap events

This was encouraging — the reactive approach isn’t foreign to ECS thinking.

The Key Insight

What if we combine Sparse Set with Reactive patterns?

flowchart LR
    subgraph Problem["Sparse Set Weakness"]
        W["Slow large-scale iteration"]
    end
    subgraph Solution["Reactive Characteristic"]
        R["Iteration is infrequent"]
    end
    Problem --> Cancel["Weakness neutralized"]
    Solution --> Cancel
    Cancel --> Result["Only strengths remain"]

If we rarely iterate (because we only process changes), Sparse Set’s iteration weakness doesn’t seem to matter as much. We keep the O(1) add/remove/access and contiguous data layout.

What I Learned from EnTT

EnTT, used in Minecraft, employs Sparse Set:

  • Paginated sparse array for memory efficiency
  • O(1) add, remove, and access
  • Signal feature for change notifications

What I Learned from Entitas

Entitas introduced the ReactiveSystem concept:

// Only processes entities that changed
public class MovementSystem : ReactiveSystem<GameEntity>
{
    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
        => context.CreateCollector(GameMatcher.Position);

    protected override void Execute(List<GameEntity> entities)
    {
        // Only entities whose position changed arrive here
    }
}

This “process only what changed” philosophy resonated with what I was trying to achieve.

ReactiveEntitySet Design

The Core Idea

flowchart LR
    RS["RuntimeSet: who exists"] --> RES["ReactiveEntitySet: who exists + what they have"]

ReactiveEntitySet is a natural extension of RuntimeSet. Where RuntimeSet tracks “who exists,” ReactiveEntitySet tracks “who exists and what state they have.”

Data Structure

public class ReactiveEntitySet<TData> : ScriptableObject
    where TData : unmanaged
{
    // Sparse Set structure
    private Dictionary<int, int> idToIndex;  // ID → Index
    private List<TData> dataList;            // Contiguous data
    private List<int> indexToId;             // Index → ID

    // Reactive events
    public event Action<int> OnItemAdded;
    public event Action<int> OnItemRemoved;
    public event Action<int, TData> OnDataChanged;
}

Operation Complexity

OperationComplexity
RegisterO(1)
UnregisterO(1) via Swap & Pop
AccessO(1)

Swap & Pop Deletion

To maintain array contiguity, deletion swaps with the last element:

flowchart TB
    subgraph Before["Before: Delete B"]
        B1["A, B, C, D"]
    end
    subgraph Swap["Swap: Move D to B position"]
        B2["A, D, C, D"]
    end
    subgraph Pop["Pop: Remove last"]
        B3["A, D, C"]
    end
    Before --> Swap --> Pop

O(1) deletion while keeping the array hole-free.

Comparison with Existing ECS

AspectUnity DOTSEntitasReactiveEntitySet
Learning curveSteepModerateGentle
Existing code coexistenceDifficultModerateEasy
GameObject workflowAbandonedSeparateMaintained
ScriptableObject-basedNoNoYes
Gradual adoptionDifficultPossibleEasy

Positioning

quadrantChart
    title Framework Positioning
    x-axis GameObject Workflow --> ECS Workflow
    y-axis Lower Performance --> Higher Performance
    quadrant-1 Full ECS
    quadrant-2 High-perf GameObject
    quadrant-3 Traditional Unity
    quadrant-4 Hybrid approaches
    Unity DOTS: [0.85, 0.9]
    Svelto.ECS: [0.8, 0.85]
    Entitas: [0.7, 0.75]
    ReactiveEntitySet: [0.25, 0.6]
    Unity Atoms: [0.2, 0.4]

ReactiveEntitySet sits in a space that’s underserved: keeping GameObject workflows while gaining some data-oriented benefits.

What Makes This Combination Unique

ReactiveEntitySet brings together:

  1. ScriptableObject-based — Editor integration, asset management
  2. GameObject workflow preserved — Existing code can coexist
  3. Reactive (change notifications) — Efficient update detection
  4. Contiguous memory layout — Cache-friendly data access
  5. Unified API with Event Channels / Variables — Consistent architecture

To my knowledge, there doesn’t appear to be an existing framework that offers this particular combination.

A Note on Performance

The Reactive approach tends to work well when changes are infrequent — which covers many game systems like UI updates, damage events, AI decisions, inventory changes, and state transitions.

I don’t have rigorous benchmarks to share, but the combination of Sparse Set’s O(1) operations and change-based processing seems promising for these use cases. Your results may vary depending on your specific scenario.

Summary

ReactiveEntitySet is an attempt to:

  • Selectively adopt ECS insights — Sparse Set, Reactive patterns
  • Avoid ECS constraints — Keep GameObject workflows
  • Offer a pragmatic middle ground — Not all-or-nothing

For developers who want data-oriented benefits but aren’t ready for full ECS migration, this might be worth exploring.


References