| name | uw-dependency-injection |
| description | Opt-in DI architecture using Reflex for Unity 6+. Covers container setup, scoping, injection, command pattern, update management, and class taxonomy. Use when ProjectConfig.yaml -> architecture_pattern is "di-first", or when the user asks about dependency injection, inversion of control, service architecture, or testable game code. Triggers on "set up DI", "add dependency injection", "create a service", "wire up dependencies", "make this testable", "decouple these systems", "add a command pattern", "cross-feature communication", or architecture discussions involving testability, decoupling, and service layers. Only activates when ProjectConfig.yaml -> architecture_pattern is "di-first". For simpler projects, use uw-scriptable-object-arch instead. |
Dependency Injection (Reflex)
DI-first architecture for complex Unity projects. Only applies when ProjectConfig.yaml -> architecture_pattern: di-first. For simpler projects, use uw-scriptable-object-arch (SO-first) instead.
Before You Start
- Read
docs/ProjectConfig.yaml for:
architecture_pattern — must be "di-first" for this skill to apply.
di_framework — should be "reflex" (the default and recommended framework).
mcp.unity_mcp — if true, call refresh_unity after creating files.
- Read
docs/CODING_STANDARDS.md for async patterns (Awaitable + CancellationToken), access modifiers (internal for cross-class helpers within an asmdef), and class structure.
- Read
docs/NAMING_CONVENTIONS.md for file/class naming.
- Ensure each feature module has its own
.asmdef — create with uw-unity-feature-scaffold. Installers, Controllers, Services all live inside feature asmdefs.
When to Use DI vs SO-First
| SO-First (default) | DI-First |
|---|
| Small/medium scope | Large/complex scope |
| Solo or small team | Team with testing culture |
| Rapid prototyping | Production architecture |
| Simple event-driven communication | Cross-feature orchestration via Commands |
| ScriptableObject event channels | Interface-based injection + factories |
Both can coexist: DI containers can bind ScriptableObject data assets as singletons.
Reflex Setup
Install (OpenUPM): openupm install com.gustavopsantos.reflex
Or UPM Git URL: https://github.com/gustavopsantos/reflex.git?path=/Assets/Reflex/
Container Hierarchy
RootScope (app lifetime — singletons, core services)
+-- SceneScope (scene lifetime — per-scene services, controllers)
+-- Manual child scopes (gameplay round, level, etc.)
Installer Pattern
Each feature module has one Installer that registers its bindings. The Installer lives inside the feature's .asmdef.
using Reflex.Core;
using UnityEngine;
namespace {RootNamespace}.Core
{
public class CoreInstaller : MonoBehaviour, IInstaller
{
[SerializeField] private AudioSettingsData _audioSettings;
public void InstallBindings(ContainerBuilder builder)
{
builder.AddSingleton(typeof(IAudioService), typeof(AudioService));
builder.AddSingleton(typeof(IStateMachineService), typeof(StateMachineService));
builder.AddInstance(_audioSettings);
}
}
}
Binding Types
| Method | What It Does | Lifetime |
|---|
AddSingleton(type, impl) | Container creates one instance | App/Scene |
AddTransient(type, impl) | New instance each resolve | Per-resolve |
AddInstance(obj) | Register existing object (SO, config) | Singleton |
Injection
using Reflex.Attributes;
namespace {RootNamespace}.Combat
{
public class ArrowController
{
[Inject] private readonly IAudioService _audio;
[Inject] private readonly IArrowMovementController _movement;
public ArrowController(IAudioService audio, IArrowMovementController movement)
{
_audio = audio;
_movement = movement;
}
}
}
Constructor injection is preferred for plain C# classes — it makes dependencies explicit and prevents forgetting to inject. Use [Inject] attribute injection for MonoBehaviours (which Unity constructs).
Interface-First Rule
Always bind to interfaces, not concrete types. This enables mock injection for testing, makes dependencies explicit, and follows the Dependency Inversion Principle.
builder.AddSingleton(typeof(IAudioService), typeof(AudioService));
builder.AddSingleton(typeof(AudioService));
Circular Dependency Prevention
Circular dependencies (A depends on B, B depends on A) cause infinite resolution loops. Prevent them with:
- Commands — if two features need to communicate, use a Command that resolves its deps at execution time instead of at construction time.
- Events / ScriptableObject channels — decouple with
uw-scriptable-object-arch event channels.
- Interfaces in a shared assembly — extract the interface into
{RootNamespace}.Core.asmdef so both features depend on the interface, not on each other.
If the container throws a circular dependency error, trace the dependency chain and break it at the point where a Command or event channel makes more sense than a direct reference.
App Lifecycle / Single Entry Point (SEP)
Only one scene has a Start() method. All other initialization flows from there.
CoreScene loads -> RootScope binds -> CoreInitiator.Start()
-> Load GameScene (additively) -> SceneScope binds -> GameInitiator.Init()
-> Load GamePlayScene -> SceneScope binds -> GamePlayInitiator.Init()
Rules:
- CoreScene is always the first scene (set in Build Settings index 0, or via editor DefaultSceneSelector).
- Each scene has one Installer (bindings) and one Initiator (entry/exit points).
- Only CoreInitiator has a
Start() method — everything else is initialized via async InitEntryPoint().
Class Taxonomy, Commands & Update Management
See references/class-taxonomy-and-commands.md for the full 9-class taxonomy, Command pattern, and centralized Update management.
Key rule: Commands are the ONLY classes allowed to cross feature boundaries. This prevents circular dependencies — each Command resolves its own references from the DI container at execution time.
Cross-Feature Command Example
When Feature A (Combat) needs to notify Feature B (Score) about a hit:
public class OnHitScoredCommand : ICommand
{
[Inject] private readonly IScoreController _score;
[Inject] private readonly IAudioService _audio;
public void Execute()
{
_score.AddPoints(10);
_audio.PlayOneShot(AudioClipType.Hit);
}
}
The Combat feature doesn't reference Score directly — it resolves and executes the Command, which the container wires up. Register in the installer: builder.AddTransient(typeof(OnHitScoredCommand));
Integration with ScriptableObject Data
DI and SO work together — SO holds data, DI manages services and wiring:
[SerializeField] private WeaponDatabase _weaponDatabase;
public void InstallBindings(ContainerBuilder builder)
{
builder.AddInstance(_weaponDatabase);
}
See uw-scriptable-object-arch for data container and event channel patterns that complement DI.
After Setup
- Scaffold features: Use
uw-unity-feature-scaffold to create feature modules with their own .asmdef — each feature gets its own Installer.
- State management: Use
uw-state-machine for game flow states, with states receiving dependencies via constructor injection.
- Write tests: Use
uw-unity-test-runner — interface-based DI makes testing easy: construct classes with mock dependencies, no container needed in tests.
- SO data: Use
uw-scriptable-object-arch for game data assets that get injected into services via AddInstance.
Rules
- Only applies when
ProjectConfig.yaml -> architecture_pattern: di-first.
- Always bind to interfaces, not concrete types (interface-first rule).
- Constructor injection preferred for plain C# classes.
[Inject] attribute for MonoBehaviours.
- Commands are the ONLY classes allowed to cross feature boundaries.
- Each feature module needs its own
.asmdef (per NAMING_CONVENTIONS.md). Installers live inside the feature's asmdef.
[SerializeField] private for Inspector-exposed fields on Installers and MonoBehaviours — never public fields.
- Use
Awaitable with CancellationToken for async operations (per CODING_STANDARDS.md).
- CoreScene is Build Settings index 0. Only CoreInitiator has
Start().
- Break circular dependencies with Commands, events, or shared interface assemblies — never with direct cross-feature references.
- If
ProjectConfig.yaml -> mcp.unity_mcp is true, call refresh_unity after creating files.