| name | vcontainer |
| description | VContainer dependency injection for Unity — LifetimeScope hierarchy, registration patterns, constructor injection for plain C#, [Inject] for MonoBehaviours. Lightweight alternative to Zenject. |
| globs | ["**/VContainer*","**/*LifetimeScope*.cs","**/*Installer*.cs","**/Container*.cs"] |
VContainer — Dependency Injection for Unity
VContainer is a lightweight, fast DI framework for Unity by hadashiA. It provides constructor injection for plain C# classes, method injection for MonoBehaviours, hierarchical scoping, and lifecycle management without the complexity of Zenject.
Why Dependency Injection in Unity
- Decouple systems: Components depend on interfaces, not concrete types
- Testability: Swap real implementations for mocks in tests
- No singletons: Avoid static state and its hidden coupling
- Configurable composition: Change wiring without changing code
- Explicit dependencies: Constructor parameters document what a class needs
LifetimeScope Hierarchy
VContainer uses LifetimeScope MonoBehaviours as composition roots. They form a parent-child hierarchy for dependency resolution.
RootLifetimeScope (DontDestroyOnLoad)
|- AudioService (Singleton)
|- SaveSystem (Singleton)
|- AnalyticsService (Singleton)
+- ISettingsProvider (Singleton)
|
|- MainMenuLifetimeScope (MainMenu scene)
| |- MainMenuController
| +- LeaderboardService
|
+- GameLifetimeScope (Game scene)
|- GameManager
|- SpawnSystem
+- ScoreSystem
Root Scope (Project-Wide Services)
using VContainer;
using VContainer.Unity;
public class RootLifetimeScope : LifetimeScope
{
[SerializeField] private AudioSettings m_AudioSettings;
protected override void Configure(IContainerBuilder builder)
{
builder.Register<AudioService>(Lifetime.Singleton).As<IAudioService>();
builder.Register<SaveSystem>(Lifetime.Singleton).As<ISaveSystem>();
builder.Register<AnalyticsService>(Lifetime.Singleton).As<IAnalyticsService>();
builder.RegisterInstance(m_AudioSettings);
}
}
Scene Scope (Scene-Specific)
public class GameLifetimeScope : LifetimeScope
{
[SerializeField] private LevelConfig m_LevelConfig;
protected override void Configure(IContainerBuilder builder)
{
builder.Register<ScoreSystem>(Lifetime.Scoped);
builder.Register<WaveSpawner>(Lifetime.Scoped);
builder.RegisterEntryPoint<GameFlowController>();
builder.RegisterComponentInHierarchy<PlayerController>();
builder.RegisterComponentInHierarchy<HUDManager>();
builder.RegisterInstance(m_LevelConfig);
}
}
Parent-Child Auto-Resolution
Child scopes automatically resolve dependencies from their parent. A GameLifetimeScope can inject IAudioService registered in RootLifetimeScope without explicit wiring.
public class GameFlowController : IStartable, ITickable, IDisposable
{
private readonly IAudioService m_Audio;
private readonly ScoreSystem m_Score;
public GameFlowController(IAudioService audio, ScoreSystem score)
{
m_Audio = audio;
m_Score = score;
}
}
Registration Patterns
Plain C# Classes — Constructor Injection (Preferred)
builder.Register<ScoreSystem>(Lifetime.Singleton);
public class ScoreSystem
{
private readonly IAudioService m_Audio;
private readonly ISaveSystem m_Save;
public ScoreSystem(IAudioService audio, ISaveSystem save)
{
m_Audio = audio;
m_Save = save;
}
public void AddScore(int points)
{
m_Audio.PlaySfx("score");
m_Save.SetInt("score", points);
}
}
Interface Binding
builder.Register<AudioService>(Lifetime.Singleton).As<IAudioService>();
builder.Register<NetworkManager>(Lifetime.Singleton)
.As<INetworkSender>()
.As<INetworkReceiver>();
builder.Register<GameManager>(Lifetime.Singleton)
.AsSelf()
.As<IGameStateProvider>();
MonoBehaviour Registration
MonoBehaviours cannot use constructor injection. Use [Inject] method injection.
builder.RegisterComponentInHierarchy<PlayerController>();
builder.RegisterComponentOnNewGameObject<HUDManager>(
Lifetime.Scoped,
"HUDManager"
);
[SerializeField] private PlayerController m_Player;
builder.RegisterComponent(m_Player);
public class PlayerController : MonoBehaviour
{
private IAudioService m_Audio;
private IInputService m_Input;
[Inject]
public void Construct(IAudioService audio, IInputService input)
{
m_Audio = audio;
m_Input = input;
}
private void Update()
{
if (m_Input.JumpPressed)
{
Jump();
m_Audio.PlaySfx("jump");
}
}
}
Entry Points — Lifecycle Without MonoBehaviour
Entry points implement lifecycle interfaces and run without needing a GameObject.
builder.RegisterEntryPoint<GameFlowController>();
public class GameFlowController : IStartable, ITickable, IFixedTickable, IDisposable
{
private readonly ScoreSystem m_Score;
public GameFlowController(ScoreSystem score) => m_Score = score;
public void Start()
{
Debug.Log("Game started");
}
public void Tick()
{
}
public void FixedTick()
{
}
public void Dispose()
{
Debug.Log("Game ended");
}
}
Available lifecycle interfaces:
IStartable — Start() called once after construction
ITickable — Tick() called every Update
IPostTickable — PostTick() called every LateUpdate
IFixedTickable — FixedTick() called every FixedUpdate
IDisposable — Dispose() called when scope is destroyed
IAsyncStartable — async UniTask StartAsync(CancellationToken ct)
Factory Registration
For objects that need runtime creation with injected dependencies.
builder.Register<EnemyFactory>(Lifetime.Scoped);
public class EnemyFactory
{
private readonly IObjectResolver m_Container;
private readonly EnemySettings m_Settings;
public EnemyFactory(IObjectResolver container, EnemySettings settings)
{
m_Container = container;
m_Settings = settings;
}
public Enemy Create(EnemyType type)
{
GameObject prefab = m_Settings.GetPrefab(type);
GameObject instance = Object.Instantiate(prefab);
m_Container.InjectGameObject(instance);
return instance.GetComponent<Enemy>();
}
}
Func Factory (Lightweight)
builder.RegisterFactory<Vector3, Bullet>(container =>
{
var pool = container.Resolve<BulletPool>();
return position => pool.Get(position);
}, Lifetime.Scoped);
public class Weapon
{
private readonly Func<Vector3, Bullet> m_CreateBullet;
public Weapon(Func<Vector3, Bullet> createBullet)
{
m_CreateBullet = createBullet;
}
public void Fire(Vector3 muzzlePos)
{
Bullet b = m_CreateBullet(muzzlePos);
}
}
Lifetime Options
| Lifetime | Behavior | Use For |
|---|
Singleton | One instance for the entire scope lifetime | Services, managers, shared state |
Transient | New instance every time it is resolved | Stateless utilities, value objects |
Scoped | One instance per scope (child scopes get their own) | Per-scene services, per-context state |
builder.Register<AudioService>(Lifetime.Singleton);
builder.Register<DamageCalculator>(Lifetime.Transient);
builder.Register<ScoreTracker>(Lifetime.Scoped);
ScriptableObject and Asset Registration
public class GameLifetimeScope : LifetimeScope
{
[SerializeField] private GameConfig m_GameConfig;
[SerializeField] private EnemyDatabase m_EnemyDatabase;
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterInstance(m_GameConfig);
builder.RegisterInstance(m_EnemyDatabase).As<IEnemyDatabase>();
}
}
Child Scope Creation at Runtime
public class RoomManager
{
private readonly LifetimeScope m_ParentScope;
public RoomManager(LifetimeScope parentScope) => m_ParentScope = parentScope;
public LifetimeScope CreateRoomScope(RoomConfig config)
{
return m_ParentScope.CreateChild(builder =>
{
builder.RegisterInstance(config);
builder.Register<RoomController>(Lifetime.Scoped);
builder.RegisterEntryPoint<RoomLogic>();
});
}
}
Common Mistakes
GameContext / Service Locator Anti-Pattern
public class GameContext
{
public PlayerModel Player { get; }
public ScoreSystem Score { get; }
public IAudioService Audio { get; }
}
public sealed class ScoreView : MonoBehaviour
{
[Inject]
public void Construct(ScoreModel model) { ... }
}
Do NOT create intermediary container classes. Let VContainer resolve dependencies directly into each consumer's constructor or [Inject] Construct method. The LifetimeScope is the single wiring point.
Circular Dependencies
public class A { public A(B b) { } }
public class B { public B(A a) { } }
public class A { public A(IEventBus bus) { } }
public class B { public B(IEventBus bus) { } }
Registering MonoBehaviours as Transient
builder.RegisterComponentOnNewGameObject<Player>(Lifetime.Transient);
builder.RegisterComponentOnNewGameObject<Player>(Lifetime.Scoped);
builder.RegisterComponentInHierarchy<Player>();
Forgetting to Register Dependencies
Using [Inject] on Fields (Avoid)
public class BadComponent : MonoBehaviour
{
[Inject] private IAudioService m_Audio;
}
public class GoodComponent : MonoBehaviour
{
private IAudioService m_Audio;
[Inject]
public void Construct(IAudioService audio) => m_Audio = audio;
}
Testing with VContainer
[Test]
public void ScoreSystem_AddsScore()
{
var mockAudio = new MockAudioService();
var mockSave = new MockSaveSystem();
var score = new ScoreSystem(mockAudio, mockSave);
score.AddScore(100);
Assert.AreEqual(100, score.CurrentScore);
Assert.IsTrue(mockAudio.PlayedSfx.Contains("score"));
}
Project Structure Convention
Assets/
Scripts/
Installers/ (or Scopes/)
RootLifetimeScope.cs
GameLifetimeScope.cs
MainMenuLifetimeScope.cs
Services/
IAudioService.cs
AudioService.cs
Game/
GameFlowController.cs
ScoreSystem.cs