Build Log

Why ReactiveEntitySet needed its own scene-persistence holder

  • Unity
  • Reactive SO
  • ScriptableObject
  • Scene Management
  • Native Containers
  • Game Development

I’ve already written about ScriptableObject data quietly vanishing on scene transitions — non-serialized fields reset when Unity unloads an unreferenced asset, and the fix is to keep something referencing it:

PostWhy ScriptableObject Data Disappears on Scene Transitions (And How to Fix It)Non-serialized fields on Unity ScriptableObjects vanish when the asset is unloaded between scenes. Here's the RuntimeInitializeOnLoadMethod-based pattern I used to fix it during a Unity 1 Week Game Jam.

ReactiveEntitySet has the same failure mode with the stakes raised. This post is about why the same bug is worse there, and the small holder component that exists because of it.

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.

Why the same bug is worse here

For a regular ScriptableObject, an unexpected unload costs you the values in non-serialized fields. Painful, but the shape of the data survives — a Dictionary comes back empty and code can repopulate it.

A ReactiveEntitySet keeps its state in native memory: a NativeHashMap for the ID-to-index lookup and NativeArrays for the entity data. Those containers are allocated unmanaged memory, so the set disposes them in OnDisable — which is exactly what runs when Unity unloads the asset between scenes. After that, there is no “reload and start empty” path that any amount of [SerializeField] fixes: the registered entities, their states, the lookup — all of it was in containers that no longer exist. Systems still holding the asset reference are now talking to a set that has forgotten everything, including how to remember.

So where the regular-SO version of this bug produces weird values, the RES version produces a dead set. Same trigger, worse outcome.

The holder

The countermeasure is the same one from the earlier post, packaged: keep a living reference so the unload never happens. ReactiveEntitySetHolder is a deliberately tiny MonoBehaviour:

public class ReactiveEntitySetHolder : MonoBehaviour
{
    [Header("References")]
    [Tooltip("ReactiveEntitySetSO assets to keep in memory across scene transitions")]
    [SerializeField] private List<ReactiveEntitySetSO> references = new();
}

That’s the whole runtime job: hold the list, exist in a scene that stays loaded. The component does nothing in Update, subscribes to nothing, manages nothing — its value is being a root that keeps the assets reachable.

The Inspector side has the one convenience that makes it usable: a “Find All in Project” button that runs AssetDatabase.FindAssets("t:ReactiveEntitySetSO") and fills the list. Hand-maintaining asset lists is the kind of chore that silently falls out of date; one button keeps the setup honest.

The scene that stays loaded

The holder only works if its GameObject survives scene transitions, which means it needs a home. The pattern I use is a Manager scene loaded additively — a scene with no camera and no gameplay, just the infrastructure objects that should outlive any one level.

For testing the pattern, the project carries a ManagerSceneBootstrap that loads the Manager scene additively on startup and re-checks after every scene load, skipping the work if it’s already there. Two details about it are deliberate:

  • It’s idempotent — sceneLoaded fires for every transition, and the bootstrap checks GetSceneByName(...).isLoaded before doing anything
  • It ships disabled — the [RuntimeInitializeOnLoadMethod] attribute is commented out, opt-in by uncommenting

The second point is really a packaging decision: the bootstrap lives in my verification project, not in the package itself. A library that silently injects an additive scene into every user’s game is making a structural decision on their behalf, and I’d rather document the pattern than impose it. Users who already have a persistent scene — most projects of any size do — just drop the holder into it.

Verifying it actually works

The failure this prevents is timing-dependent and editor-state-dependent (the original bug famously didn’t reproduce while the asset was selected in the Inspector), so I didn’t want to trust it by inspection. The verification setup is a scene round trip — Scene A, an intermediate scene, back to Scene A — with a RESPersistenceVerifier component that registers entities, records their states, transitions, and checks what survived.

Without the holder, the round trip comes back to an empty set. With it, the states match what was recorded. Not a sophisticated test, but it turns “I think the holder fixes it” into something that fails loudly when broken.

Trade-offs

  • The Manager scene has to be in Build Settings and loaded before any scene that touches the sets. The bootstrap handles ordering; the build-settings entry is on you.
  • DontDestroyOnLoad on a plain GameObject is the lighter-weight alternative — no scene setup, same effect. I prefer the explicit scene because the infrastructure is visible in the hierarchy instead of appearing at runtime, but it’s a preference, not a correctness argument.
  • None of this is automatic in a build. If no loaded scene carries the holder, the protection simply isn’t there — it’s opt-in infrastructure, and forgetting it fails exactly the way the original bug does.

Things still bugging me

  • Adding a new RES asset means remembering to re-run “Find All in Project”. The button makes it cheap, not automatic.
  • “Find All” is project-wide with no folder scoping, which is fine until a test fixture asset sneaks into the production holder list.
  • I still go back and forth on whether the holder should be a ScriptableObject holding other ScriptableObjects instead of a MonoBehaviour. The current answer is that a MonoBehaviour in a loaded scene is the thing Unity’s unload logic respects, and cleverness here buys nothing.

Wrap-up

The earlier post’s lesson was “if a field isn’t serializable, treat its lifetime as one Play session at best.” The RES version is stricter: if the state lives in native memory, treat the asset’s unload as destruction, full stop. The holder exists so that destruction happens when I decide — on play mode exit — and not whenever a scene transition catches the asset unreferenced.

References