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 pat...
Scanned 9/6/2026
Install to Claude Code
npx -y skills add IdoCohen560/claude-unity-game-studio --skill uw-dependency-injection --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Uw Dependency Injection?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/idocohen560-uw-dependency-injection-claude-unity-game-studio)More formats (shields.io, HTML) on the badges page.
---
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
1. 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.
2. Read `docs/CODING_STANDARDS.md` for async patterns (`Awaitable` + `CancellationToken`), access modifiers (`internal` for cross-class helpers within an asmdef), and class structure.
3. Read `docs/NAMING_CONVENTIONS.md` for file/class naming.
4. 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`.
```csharp
using Reflex.Core;
using UnityEngine;
namespace {RootNamespace}.Core
{
public class CoreInstaller : MonoBehaviour, IInstaller
{
[SerializeField] private AudioSettingsData _audioSettings;
public void InstallBindings(ContainerBuilder builder)
{
// Services (singleton, app lifetime)
builder.AddSingleton(typeof(IAudioService), typeof(AudioService));
builder.AddSingleton(typeof(IStateMachineService), typeof(StateMachineService));
// SO data (inject existing asset as value)
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
```csharp
using Reflex.Attributes;
namespace {RootNamespace}.Combat
{
public class ArrowController
{
[Inject] private readonly IAudioService _audio;
[Inject] private readonly IArrowMovementController _movement;
// OR constructor injection (preferred for non-MonoBehaviours):
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.
```csharp
// Correct — bind to interface
builder.AddSingleton(typeof(IAudioService), typeof(AudioService));
// Wrong — binding concrete directly prevents mock injection
builder.AddSingleton(typeof(AudioService));
```
## Circular Dependency Prevention
Circular dependencies (A depends on B, B depends on A) cause infinite resolution loops. Prevent them with:
1. **Commands** — if two features need to communicate, use a Command that resolves its deps at execution time instead of at construction time.
2. **Events / ScriptableObject channels** — decouple with `uw-scriptable-object-arch` event channels.
3. **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](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:
```csharp
// Lives in a shared Commands assembly or in the feature that initiates it
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:
```csharp
// In installer: inject SO data asset into container
[SerializeField] private WeaponDatabase _weaponDatabase;
public void InstallBindings(ContainerBuilder builder)
{
builder.AddInstance(_weaponDatabase); // SO data available via [Inject]
}
```
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!