Asset-based Dependency Injection
Purpose
This page introduces the concept of Asset-based Dependency Injection (DI). You will learn why Reactive SO treats ScriptableObjects not just as data containers, but as the primary mechanism for resolving dependencies between systems.
What is Dependency Injection?
Before diving into Asset-based DI, let’s clarify what Dependency Injection actually means.
The definition
Martin Fowler defined Dependency Injection in 2004 as:
“The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation.”
In simpler terms: a component receives its dependencies from outside rather than creating them itself.
Three forms of DI
| Form | Description | Example |
|---|---|---|
| Constructor Injection | Dependencies passed via constructor | new Service(dependency) |
| Setter Injection | Dependencies assigned via setter methods | service.SetDependency(dep) |
| Field Injection | Dependencies assigned directly to fields | service.dependency = dep |
DI ≠ DI Container
Dependency Injection is a design pattern. DI Containers (like VContainer or Zenject) are tools that automate DI. You do not need a DI Container to practice Dependency Injection.
This distinction matters:
| Term | What it is |
|---|---|
| Dependency Injection | The pattern of providing dependencies externally |
| DI Container | A tool that automates dependency resolution and lifecycle management |
| Pure DI / Manual DI | Practicing DI without a container, assembling dependencies by hand |
Many developers assume “DI = Zenject” or “DI = VContainer”. This conflates the tool with the pattern. VContainer performs DI internally, but VContainer itself is not DI.
Pure DI in Unity
When you write this code:
public class PlayerController : MonoBehaviour
{
[SerializeField] private IntVariableSO health; // Dependency
}
And drag an asset into the Inspector field, you are practicing Pure DI via Field Injection. The Unity Inspector acts as the “assembler” that populates the dependency. No DI Container required.
Beyond data containers
A common misconception is that ScriptableObjects are only for static data (like weapon stats or game settings). While they excel at that, their true power lies in their lifecycle and identity.
ScriptableObjects are:
- Shared Instances: They exist independently of scenes.
- Asset-Referenced: They have a unique GUID and can be assigned in the Inspector.
Reactive SO leverages these properties to use ScriptableObjects as dynamic connection points between systems.
Comparison of Unity types
Each approach has its own strengths. Understanding these trade-offs helps you choose the right tool for each situation.
| Feature | ScriptableObject | Static Class (Singleton) | MonoBehaviour | Pure C# Class |
|---|---|---|---|---|
| Lifecycle | Project-scoped (Lives as long as referenced) | App Domain (Reset on domain reload) | Scene-scoped (Bound to scene lifecycle) | Managed by GC |
| Injection | Inspector (Asset) Drag & Drop | Direct accessManager.Instance | Inspector (Scene) or GetComponent | Constructor / Factory |
| Testability | CreateInstance in tests | Requires reset between tests | Requires GameObject setup | Easy to instantiate |
| Configuration | Serialized Edit in Inspector | Code or config files | Serialized Per-instance in scene | Code only |
| Multiplicity | Multiple assets possible | Single global instance | Multiple GameObjects | Multiple instances |
When to use each
| Approach | Best suited for |
|---|---|
| ScriptableObject | Shared state across scenes, data-driven configuration, decoupled communication |
| Static Class / Singleton | Truly global services (e.g., logging), simple prototypes, quick access patterns |
| MonoBehaviour | Scene-specific behavior, GameObjects with lifecycle needs, visual components |
| Pure C# Class | Business logic, algorithms, domain models, maximum testability |
Trade-offs of ScriptableObject
ScriptableObjects are not universally superior. Consider these trade-offs:
- Runtime data persistence - Non-serialized data can be lost if the asset is unloaded (see Troubleshooting)
- Not scene-specific by design - For per-scene state, MonoBehaviours may be more appropriate
- Asset Management Hell - This is real. A mature project using this pattern will have dozens or hundreds of assets:
OnPlayerDeath,OnEnemySpawned,PlayerHealth,EnemySet… Finding the right asset, avoiding duplicates, and maintaining naming conventions becomes a genuine challenge. Reactive SO provides tools like Asset Browser and Dependency Analyzer to help, but the overhead is unavoidable.
Reactive SO chooses ScriptableObjects because they excel at cross-scene shared state and decoupled communication - the specific problems this architecture addresses. But be aware: you are trading code complexity for asset complexity.
The concept: Asset-based DI
Asset-based Dependency Injection is Pure DI via Inspector, where dependencies are resolved by referencing Assets instead of Classes or Scene Objects.
Traditional (Tight Coupling)
public class EnemySpawner : MonoBehaviour
{
// Dependency on a specific Scene Object or Singleton
void Spawn()
{
// "I need to find the specific Manager in this scene"
GameManager.Instance.RegisterEnemy(newEnemy);
}
}
This approach creates a hard dependency on GameManager. You cannot test EnemySpawner without GameManager, and you cannot have multiple enemy lists.
Asset-based DI (Loose Coupling)
public class EnemySpawner : MonoBehaviour
{
// Dependency injected via Inspector (Field Injection)
[SerializeField] private GameObjectRuntimeSetSO enemySet;
void Spawn()
{
// "I don't care who holds this list, I just add to it"
enemySet.Add(newEnemy);
}
}
This is Pure DI: the dependency (enemySet) is provided externally via the Inspector. The Unity Inspector is the “assembler” in Fowler’s terminology, manually wiring dependencies without requiring a DI Container framework.
Case study: Runtime Sets as DI
Runtime Sets are the most primitive and clear example of Asset-based DI.
- The Problem: Code needs a list of all enemies.
- The Naive Solution: Make a static
public static List<Enemy> AllEnemiesin a Manager.- Issue: Hard to test, hard to manage multiple contexts, creates hidden dependencies.
- The Asset-based Solution: Create an
EnemySetasset (ScriptableObject).- Enemies register themselves to the asset:
set.Add(this). - Systems read from the asset:
foreach (var e in set.Items).
- Enemies register themselves to the asset:
The EnemySet asset becomes a stable anchor in your project. Scenes can load and unload, MonoBehaviours can be created and destroyed, but the connection point (the Asset) remains constant.
Why Reactive SO focuses on this
Reactive SO is designed to maximize the benefits of Asset-based DI:
- Event Channels - Assets that act as method calls (
Action<T>). - Variables - Assets that act as shared fields (
T value). - Reactive Entity Sets - Assets that act as in-memory databases.
By moving these responsibilities to Assets, your MonoBehaviours become stateless logic containers that are easier to write, read, and test.