Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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 patter
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 211% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 153% | 0% |
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.
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.docs/CODING_STANDARDS.md for async patterns (Awaitable + CancellationToken), access modifiers (internal for cross-class helpers within an asmdef), and class structure.docs/NAMING_CONVENTIONS.md for file/class naming..asmdef — create with uw-unity-feature-scaffold. Installers, Controllers, Services all live inside feature asmdefs.| 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.
Install (OpenUPM): openupm install com.gustavopsantos.reflex Or UPM Git URL: https://github.com/gustavopsantos/reflex.git?path=/Assets/Reflex/
RootScope (app lifetime — singletons, core services)
+-- SceneScope (scene lifetime — per-scene services, controllers)
+-- Manual child scopes (gameplay round, level, etc.)Each feature module has one Installer that registers its bindings. The Installer lives inside the feature's .asmdef.
csharpusing 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); } } }
| 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 |
csharpusing 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).
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 dependencies (A depends on B, B depends on A) cause infinite resolution loops. Prevent them with:
uw-scriptable-object-arch event channels.{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.
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:
Start() method — everything else is initialized via async InitEntryPoint().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.
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));
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.
uw-unity-feature-scaffold to create feature modules with their own .asmdef — each feature gets its own Installer.uw-state-machine for game flow states, with states receiving dependencies via constructor injection.uw-unity-test-runner — interface-based DI makes testing easy: construct classes with mock dependencies, no container needed in tests.uw-scriptable-object-arch for game data assets that get injected into services via AddInstance.ProjectConfig.yaml -> architecture_pattern: di-first.[Inject] attribute for MonoBehaviours..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.Awaitable with CancellationToken for async operations (per CODING_STANDARDS.md).Start().ProjectConfig.yaml -> mcp.unity_mcp is true, call refresh_unity after creating files.Other measured skills in the registry, with their headline benchmark lift.