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 _audioSettings;
protected override void Configure(IContainerBuilder builder)
{
// Singletons survive scene loads
builder.Register<AudioService>(Lifetime.Singleton).As<IAudioService>();
builder.Register<SaveSystem>(Lifetime.Singleton).As<ISaveSystem>();
builder.Register<AnalyticsService>(Lifetime.Singleton).As<IAnalyticsService>();
// ScriptableObject instance
builder.RegisterInstance(_audioSettings);
}
}
Scene Scope (Scene-Specific)
public class GameLifetimeScope : LifetimeScope
{
[SerializeField] private LevelConfig _levelConfig;
protected override void Configure(IContainerBuilder builder)
{
// Scene-specific registrations
builder.Register<ScoreSystem>(Lifetime.Scoped);
builder.Register<WaveSpawner>(Lifetime.Scoped);
// Entry point with lifecycle
builder.RegisterEntryPoint<GameFlowController>();
// MonoBehaviour already in scene hierarchy
builder.RegisterComponentInHierarchy<PlayerController>();
builder.RegisterComponentInHierarchy<HUDManager>();
// Config data
builder.RegisterInstance(_levelConfig);
}
}
Parent-Child Auto-Resolution
Child scopes automatically resolve dependencies from their parent. A GameLifetimeScope can inject IAudioService registered in RootLifetimeScope without explicit wiring.
// GameFlowController receives IAudioService from Root + ScoreSystem from Game scope
public class GameFlowController : IStartable, ITickable, IDisposable
{
private readonly IAudioService _audio;
private readonly ScoreSystem _score;
public GameFlowController(IAudioService audio, ScoreSystem score)
{
_audio = audio;
_score = score;
}
}
Registration Patterns
Plain C# Classes — Constructor Injection (Preferred)
// Registration
builder.Register<ScoreSystem>(Lifetime.Singleton);
// The class — dependencies are constructor parameters
public class ScoreSystem
{
private readonly IAudioService _audio;
private readonly ISaveSystem _save;
public ScoreSystem(IAudioService audio, ISaveSystem save)
{
_audio = audio;
_save = save;
}
public void AddScore(int points)
{
// Use injected services
_audio.PlaySfx("score");
_save.SetInt("score", points);
}
}
Interface Binding
// Register concrete type, expose as interface
builder.Register<AudioService>(Lifetime.Singleton).As<IAudioService>();
// Multiple interfaces for same implementation
builder.Register<NetworkManager>(Lifetime.Singleton)
.As<INetworkSender>()
.As<INetworkReceiver>();
// Self + interface
builder.Register<GameManager>(Lifetime.Singleton)
.AsSelf()
.As<IGameStateProvider>();
MonoBehaviour Registration
MonoBehaviours cannot use constructor injection. Use [Inject] method injection.
// MonoBehaviour already placed in the scene
builder.RegisterComponentInHierarchy<PlayerController>();
// Create MonoBehaviour on a new GameObject
builder.RegisterComponentOnNewGameObject<HUDManager>(
Lifetime.Scoped,
"HUDManager" // Optional GameObject name
);
// Register existing component reference from LifetimeScope's serialized fields
[SerializeField] private PlayerController _player;
// In Configure:
builder.RegisterComponent(_player);
// MonoBehaviour with [Inject]
public class PlayerController : MonoBehaviour
{
private IAudioService _audio;
private IInputService _input;
[Inject]
public void Construct(IAudioService audio, IInputService input)
{
_audio = audio;
_input = input;
}
private void Update()
{
if (_input.JumpPressed)
{
Jump();
_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 _score;
public GameFlowController(ScoreSystem score) => _score = score;
public void Start()
{
// Called once after injection, like MonoBehaviour.Start
Debug.Log("Game started");
}
public void Tick()
{
// Called every frame, like MonoBehaviour.Update
}
public void FixedTick()
{
// Called every FixedUpdate
}
public void Dispose()
{
// Called when scope is destroyed, like MonoBehaviour.OnDestroy
Debug.Log("Game ended");
}
}
Available lifecycle interfaces:
IStartable—Start()called once after constructionITickable—Tick()called every UpdateIPostTickable—PostTick()called every LateUpdateIFixedTickable—FixedTick()called every FixedUpdateIDisposable—Dispose()called when scope is destroyedIAsyncStartable—async UniTask StartAsync(CancellationToken ct)
Factory Registration
For objects that need runtime creation with injected dependencies.
// Registration
builder.Register<EnemyFactory>(Lifetime.Scoped);
// Factory class
public class EnemyFactory
{
private readonly IObjectResolver _container;
private readonly EnemySettings _settings;
public EnemyFactory(IObjectResolver container, EnemySettings settings)
{
_container = container;
_settings = settings;
}
public Enemy Create(EnemyType type)
{
GameObject prefab = _settings.GetPrefab(type);
GameObject instance = Object.Instantiate(prefab);
_container.InjectGameObject(instance); // Inject into all [Inject] methods
return instance.GetComponent<Enemy>();
}
}
Func Factory (Lightweight)
// Register a factory delegate
builder.RegisterFactory<Vector3, Bullet>(container =>
{
var pool = container.Resolve<BulletPool>();
return position => pool.Get(position);
}, Lifetime.Scoped);
// Inject the factory
public class Weapon
{
private readonly Func<Vector3, Bullet> _createBullet;
public Weapon(Func<Vector3, Bullet> createBullet)
{
_createBullet = createBullet;
}
public void Fire(Vector3 muzzlePos)
{
Bullet b = _createBullet(muzzlePos);
}
}
Lifetime Options
| Lifetime | Behavior | Use For | |----------|----------|---------| | `