Singleton Utilities (Runtime + ScriptableObject)¶
Visual
This package includes two lightweight, production‑ready singleton helpers that make global access patterns safe, consistent, and testable:
RuntimeSingleton<T>— a component singleton that ensures one instance exists in play mode, optionally persists across scenes, and self‑initializes when first accessed.ScriptableObjectSingleton<T>— a configuration/data singleton backed by a single asset underResources/, with an editor auto‑creator to keep assets present and correctly placed.
Odin compatibility: When Odin Inspector is present (
ODIN_INSPECTORdefined), these types derive fromSerializedMonoBehaviour/SerializedScriptableObjectfor richer serialization. Without Odin, they fall back to Unity base types. No code changes required.
TL;DR — What Problem This Solves¶
- Stop hand‑rolling global access. Get a single, safe instance you can call from anywhere.
- Choose between a scene‑resident component or a project asset for settings/data.
- No manual setup: instances auto‑create on first use; ScriptableObject assets auto‑create/move under
Resources/in the Editor.
Auto-loading singletons¶
- Add
[AutoLoadSingleton]to anyRuntimeSingleton<T>orScriptableObjectSingleton<T>to have it instantiated automatically. - The editor’s Attribute Metadata generator discovers those attributes (via
TypeCache) and serializes the type name + load phase intoAttributeMetadataCache. No manual registration or code-generation is required. - At runtime (play mode only),
SingletonAutoLoaderreads the serialized entries and uses reflection to touch each singleton’sInstanceduring the configuredRuntimeInitializeLoadType(defaultBeforeSplashScreen). - Prefer auto-loading only for global services/data that every scene requires; optional or level-specific systems should still call
Instancemanually. - Example:
| C# | |
|---|---|
Quick decision guide
- Need a behaviour that runs (Update, events, coroutines) and may persist across scenes? Use
RuntimeSingleton<T>. - Need global config/data you edit in the Inspector and load from any scene? Use
ScriptableObjectSingleton<T>.
Quick Start (1 minute)¶
RuntimeSingleton
| C# | |
|---|---|
ScriptableObjectSingleton
Contents
- Odin Compatibility
- When To Use / Not To Use
- RuntimeSingleton
- Lifecycle diagram, examples, pitfalls
- ScriptableObjectSingleton
- Lookup + auto‑creator diagrams, examples, tips
- Scenarios & Guidance
- Troubleshooting
Odin Compatibility¶
- With Odin installed (symbol
ODIN_INSPECTOR), base classes inherit fromSerializedMonoBehaviourandSerializedScriptableObjectto enable serialization of complex types (dictionaries, polymorphic fields) with Odin drawers. - Without Odin, bases inherit from Unity’s
MonoBehaviour/ScriptableObjectwith no behavior change.
When To Use¶
RuntimeSingleton<T>- Cross‑scene services (thread dispatcher, audio router, global managers).
- Utility components that should always be available via
T.Instance. -
Creating the instance on demand when not found in the scene.
-
ScriptableObjectSingleton<T> - Global settings/configuration (graphics, audio, feature flags).
- Data that should be edited as an asset and loaded via
Resources. - Consistent project setup for teams (auto‑created asset on editor load).
When Not To Use¶
- Prefer DI/service locators for heavily decoupled architectures requiring multiple implementations per environment, or for test seams where global state is undesirable.
- Avoid
RuntimeSingleton<T>for ephemeral, per‑scene logic or objects that should be duplicated in additive scenes. - Avoid
ScriptableObjectSingleton<T>for save data or level‑specific data that should not live in Resources or should have multiple instances.
RuntimeSingleton<T> Overview¶
- Access via
T.Instance(creates a newGameObjectnamed"<Type>-Singleton"and addsTif none exists; otherwise finds an existing active instance). HasInstancelets you check for an existing instance without creating one.Preserve(virtual, defaulttrue) controlsDontDestroyOnLoad.- Handles duplicate detection and cleans up instance reference on destroy. Instance is cleared on domain reload before scene load.
Example: Simple service
Odin note: With Odin installed, the class inherits SerializedMonoBehaviour, enabling dictionaries and other complex serialized types.
Common pitfalls:
- If an inactive instance exists in the scene,
Instancewon’t find it (search excludes inactive objects) and will create a new one. - If two active instances exist, the newer one logs an error and destroys itself.
- If
Preserveistrue, the instance is detached and markedDontDestroyOnLoad.
Lifecycle diagram:
| Text Only | |
|---|---|
Notes:
- To avoid creation during a sensitive frame, place a pre‑made instance in your bootstrap scene.
- For scene‑local managers, override
Preserve => false.
ScriptableObjectSingleton<T> Overview¶
- Access via
T.Instance(lazy‑loads fromResources/using either a custom path or the type name; warns if multiple assets found and chooses the first by name). HasInstanceindicates whether the lazy value exists and is not null.- Optional
[ScriptableSingletonPath("Sub/Folder")]to control theResourcessubfolder. - Editor utility auto‑creates and relocates assets: see the “ScriptableObject Singleton Creator” in the Editor Tools Guide.
Example: Settings asset
Odin note: With Odin installed, the class inherits SerializedScriptableObject, so you can safely serialize complex collections without custom drawers.
Asset management tips:
- Place the asset under
Assets/Resources/(or under the path from[ScriptableSingletonPath]). - The Editor’s “ScriptableObject Singleton Creator” runs on load to create missing assets and move misplaced ones. It also supports a test‑assembly toggle used by our test suite.
Lookup order diagram:
| Text Only | |
|---|---|
Auto‑creator flow (Editor):
| Text Only | |
|---|---|
Asset structure diagram:
| Text Only | |
|---|---|
Scenarios & Guidance¶
- Global dispatcher: See
UnityMainThreadDispatcherwhich derives fromRuntimeSingleton<UnityMainThreadDispatcher>. - Global data caches or registries: Use
ScriptableObjectSingleton<T>so data lives in a single editable asset and loads fast. - Cross‑scene managers: Keep
Preserve = trueto avoid duplicates across scene loads.
Data Registries & Lookups (Single Source of Truth)¶
ScriptableObject singletons excel as in‑project “databases” for content/config:
- Centralize definitions (items, abilities, buffs, NPCs, localization) in one asset.
- Build fast lookup indices (by ID/tag/category) at load or validation time.
- Keep workflows simple: edit in Inspector, no runtime bootstrapping needed.
Example: Items DB with indices
Tips
- The auto-creator now maintains
Assets/Resources/Wallstop Studios/Unity Helpers/ScriptableObjectSingletonMetadata.asset, which records the exactResourcesload path + GUID for every singleton asset. At runtime,ScriptableObjectSingleton<T>consults this metadata so it can callResources.Load("Folder/MySingleton")directly (orResources.LoadAllscoped toFolder/when it needs to detect duplicates) and never falls back toResources.LoadAll(string.Empty). - When metadata is missing or stale (e.g., if you deleted the metadata asset in a test project), the runtime logs a warning once per type and falls back to a bounded search (
Resources.Load<T>(typeName)+ editor-onlyAssetDatabaselookups) instead of scanning the entireResourcestree. - Keep serialized lists as your source of truth; build dictionaries at load/validate.
- Use
[ScriptableSingletonPath]to place the asset predictably underResources/. - Split huge DBs into themed sub‑assets and cross‑reference via indices.
- Consider GUIDs or string IDs for modding; validate uniqueness in
OnValidate.
Example: Content DB with tags, categories, GUIDs (Addressables)¶
Why this works well
- One authoritative asset; code reads through stable APIs.
- Deterministic load path via Resources; Addressables used only for content references.
- Indices rebuilt automatically to keep lookups fast and in sync while editing.
When Not To Use ScriptableObject Singletons as DBs¶
Use alternatives when one or more of these apply:
- Very large datasets (tens of thousands of records or >10–20 MB serialized)
- Prefer Addressables catalogs, binary blobs, streaming assets, or an external store; load by page or on demand.
- Frequent live updates/patches without app updates
- External data sources (remote JSON/Protobuf), Addressables content updates, or platform DBs are better suited.
- Strong need for async/background loading or partial paging
- Addressables + async APIs give finer loading control versus a monolithic Resources asset.
- Cross‑team/content pipelines that generate data at build time
- Import raw data into Addressables or assets at build; consider codegen for IDs and indices.
- Complex versioning/migrations of data formats
- Store version tags and migrate on load, or keep data outside Resources where migrations are simpler to stage.
- Sensitive/untrusted inputs
- Don’t deserialize untrusted data into SOs; use validated formats and sandboxed loaders.
- Save data
- Keep save/progression separate from the content DB; reference content by GUID/ID in saves.
Choosing a Data Distribution Strategy¶
Use this chart to pick an approach based on constraints:
Testing Patterns¶
Testing with RuntimeSingleton<T>¶
Runtime singletons require special handling in tests to avoid leaked GameObjects and unpredictable state across test runs. The recommended pattern uses CommonTestBase which handles cleanup automatically.
Pattern 1: Extend CommonTestBase¶
Pattern 2: Manual Scope Management¶
For tests that need finer control over singleton lifecycle:
Pattern 3: Temporarily Disable Auto-Creation¶
For specific tests that need to verify behavior when the singleton doesn't exist:
Testing with ScriptableObjectSingleton<T>¶
ScriptableObject singletons load from Resources/ and are typically tested in Editor tests where asset manipulation is possible.
Pattern 1: Use Editor Test Fixtures¶
Pattern 2: Create Test-Specific Assets¶
For tests that need controlled data:
Key Testing Guidelines¶
-
Inherit from
CommonTestBase: This handles most singleton cleanup automatically, including dispatcher scope management. -
Use
CreateTestScopefor dispatcher: TheUnityMainThreadDispatcher.CreateTestScope()method packages the common test setup pattern: disable auto-creation → destroy existing → re-enable auto-creation. -
Prefer
destroyImmediate: truein EditMode: EditMode tests should useDestroyImmediateto ensure synchronous cleanup without Unity's delayed destruction. -
Track created objects: Use
Track<T>()orTrackDisposable<T>()to ensure objects are cleaned up after tests. -
Clear singleton state between tests: Domain reloads clear singleton instances, but within a test run you may need explicit cleanup.
Troubleshooting¶
Best Practices¶
- Prefer placing a pre-made runtime singleton in a bootstrap scene when construction order matters; avoid first-access implicit creation during critical frames.
- For scene-local managers, override
Preserve => falseto prevent cross-scene persistence. - Keep exactly one singleton asset under
Resources/for eachScriptableObjectSingleton<T>; let the auto-creator relocate any strays. - Use
[ScriptableSingletonPath]to group related settings; avoid deep nesting that hurts discoverability. -
With Odin installed, take advantage of
Serialized*bases for complex serialized fields; without Odin, keep fields Unity-serializable. -
Multiple ScriptableObject assets found: a warning is logged and the first by name is used. Resolve by keeping only one asset in Resources or by letting the auto‑creator relocate the correct one.
Instancereturns null for ScriptableObject: Ensure the asset exists underResources/and the type name or custom path matches.- Domain reloads: Both singletons clear cached instances before scene load.
- Leaked GameObjects in tests: Use
CommonTestBaseor wrap test code withAutoCreationScope.Disabled()to ensure cleanup.
Related Docs¶
- Editor tool: ScriptableObject Singleton Creator.
- Tests:
Tests/Runtime/Utils/RuntimeSingletonTests.csandTests/Editor/Utils/ScriptableObjectSingletonTests.cs. - Dispatcher testing: Unity Main Thread Dispatcher Guide.