Home / Blog / State Management with ScriptableObjects

State Management with ScriptableObjects

Global variables create spaghetti code. ScriptableObjects create clean, testable architecture.

The Problem with Traditional State Management

Beginner Unity projects use static variables or singleton patterns to share state between scenes and systems. This creates tight coupling—every system that needs player health references the same PlayerManager singleton. Testing is impossible (singletons persist across test runs). State is hidden across dozens of files. Debugging requires mentally tracking implicit dependencies. As projects grow, this becomes unmaintainable. ScriptableObjects offer a better way: data-driven, inspector-visible, loosely coupled state management.

ScriptableObject Event System

We use ScriptableObjects as event channels: PuzzleCompletedEvent, RealmTransitionEvent, AccessibilityModeChangedEvent. Systems raise events by calling Invoke() on these assets. Other systems subscribe by implementing OnEnable/OnDisable listeners. This creates decoupling: the puzzle system doesn't know or care what happens when puzzles complete—it just raises an event. Analytics, UI, progression, and audio systems independently listen and react. Adding new reactions requires zero changes to existing code.

Runtime Sets for Dynamic Collections

We created a generic "RuntimeSet" ScriptableObject that acts as a dynamic collection. ActiveNPCs is a RuntimeSet—NPCs add themselves OnEnable, remove themselves OnDisable. Any system can query ActiveNPCs without needing references or FindObjectsOfType (which is slow). This is especially powerful for UI: a "Nearby NPCs" panel simply observes the RuntimeSet and updates automatically when NPCs enter/leave. No manual reference management, no null checks, no stale data.

Testability and Debugging Benefits

ScriptableObjects are assets visible in the Project window—you can inspect state without running the game. During play mode, you can watch ScriptableObjects update in real-time in the Inspector. Testing is trivial: create test-specific ScriptableObject instances (PlayerData_Test, GameState_Test) and inject them into systems. No singleton teardown, no static state persistence between tests. We reduced debugging time by ~40% simply by making state visible and isolated in ScriptableObjects instead of hidden in static memory.