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
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.