| name | tools-unity-vcontainer |
| description | VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls. |
VContainer Dependency Injection
Overview
VContainer is a lightweight, fast DI container for Unity. This skill covers registration patterns, lifecycle management, and common pitfalls that cause crashes.
When to Use
- Setting up dependency injection in Unity
- Creating scoped containers for features/states
- Debugging VContainerException crashes
- Writing testable Unity code
- Managing service lifetimes
Installation
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.15.4"
Core Concepts
Lifetime Types
| Lifetime | Description | Use Case |
|---|
Singleton | One instance per container | Global services, managers |
Scoped | One instance per scope | Feature-specific services |
Transient | New instance every resolve | Factories, short-lived objects |
Critical: Singleton in a child scope is actually scoped to that child, not truly global!
Registration Patterns
Basic Registration
public class GameInstaller : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<IPlayerService, PlayerService>(Lifetime.Singleton);
builder.Register<GameManager>(Lifetime.Singleton);
builder.Register<AudioSystem>(Lifetime.Singleton)
.AsImplementedInterfaces()
.AsSelf();
}
}
Instance Registration
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterInstance(existingService);
var config = Resources.Load<GameConfig>("GameConfig");
builder.RegisterInstance(config);
builder.RegisterComponent(GetComponent<AudioSource>());
}
Factory Registration
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterFactory<Enemy>(() => new Enemy(), Lifetime.Scoped);
builder.RegisterFactory<string, IWeapon>(name =>
new Weapon(name), Lifetime.Transient);
builder.Register<IEnemyFactory, EnemyFactory>(Lifetime.Singleton);
}
public class EnemyFactory : IEnemyFactory
{
private readonly IObjectResolver _resolver;
public EnemyFactory(IObjectResolver resolver)
{
_resolver = resolver;
}
public Enemy Create(EnemyConfig config)
{
var enemy = new Enemy(config);
_resolver.Inject(enemy);
return enemy;
}
}
EntryPoint Registration
protected override void Configure(IContainerBuilder builder)
{
builder.RegisterEntryPoint<GameInitializer>();
builder.RegisterEntryPoint<GameLoop>();
builder.RegisterEntryPoint<AsyncGameInitializer>();
builder.Register<GameManager>(Lifetime.Singleton)
.AsImplementedInterfaces();
}
public class GameInitializer : IStartable
{
public void Start()
{
}
}
public class AsyncGameInitializer : IAsyncStartable
{
public async UniTask StartAsync(CancellationToken ct)
{
await LoadGameDataAsync(ct);
}
}
Injection Patterns
Constructor Injection (Preferred)
public class PlayerController
{
private readonly IInputService _input;
private readonly IPlayerService _player;
public PlayerController(IInputService input, IPlayerService player)
{
_input = input;
_player = player;
}
}
Method Injection
public class EnemyBehavior : MonoBehaviour
{
private ITargetingService _targeting;
[Inject]
public void Construct(ITargetingService targeting)
{
_targeting = targeting;
}
}
Property Injection (Use Sparingly)
public class DebugManager
{
[Inject]
public IAnalyticsService Analytics { get; set; }
}
Field Injection (Avoid in Production)
public class LegacyComponent : MonoBehaviour
{
[Inject] private IService _service;
}
Scoped Containers (Child Scopes)
Creating Child Scopes
public class StateNavigationController
{
private readonly LifetimeScope _rootScope;
private readonly List<LifetimeScope> _stateStack = new();
public async UniTask NavigateTo(IStateTransitionData data)
{
var stateScope = _rootScope.CreateChild(builder =>
{
builder.Register<IState, WorldState>(Lifetime.Singleton);
builder.Register<WorldController>(Lifetime.Singleton);
});
_stateStack.Add(stateScope);
var state = stateScope.Container.Resolve<IState>();
await state.Enter(data);
}
public async UniTask ExitState()
{
var scope = _stateStack[^1];
_stateStack.RemoveAt(_stateStack.Count - 1);
var state = scope.Container.Resolve<IState>();
await state.Exit();
await UniTask.Yield();
scope.Dispose();
}
}
Using IInstaller
public class WorldStateInstaller : IInstaller
{
public void Install(IContainerBuilder builder)
{
builder.Register<IState, WorldState>(Lifetime.Singleton);
builder.Register<WorldController>(Lifetime.Singleton);
builder.Register<WorldView>(Lifetime.Singleton);
}
}
var scope = parentScope.CreateChild(new WorldStateInstaller());
Enqueue Pattern for Segment Loading
using (LifetimeScope.Enqueue(segmentCache))
{
var stateScope = _rootScope.CreateChild(installer);
}
Common Crash Patterns & Fixes
Problem 1: Resolving After Disposal
await state.Exit();
scope.Dispose();
await state.Exit();
await UniTask.Yield();
scope.Dispose();
Problem 2: Singleton Referencing Scoped
builder.Register<GlobalManager>(Lifetime.Singleton);
builder.Register<FeatureService>(Lifetime.Scoped);
public class GlobalManager
{
public GlobalManager(FeatureService feature) { }
}
public class GlobalManager
{
private readonly IObjectResolver _resolver;
public GlobalManager(IObjectResolver resolver)
{
_resolver = resolver;
}
public FeatureService GetFeature()
{
return _resolver.Resolve<FeatureService>();
}
}
Problem 3: Fire-and-Forget Without Guard
public async UniTaskVoid DoAsyncWork()
{
await UniTask.Delay(5000);
_service.DoSomething();
}
private CancellationTokenSource _cts;
public void Initialize()
{
_cts = new CancellationTokenSource();
}
public async UniTask DoAsyncWork()
{
await UniTask.Delay(5000, cancellationToken: _cts.Token);
_service.DoSomething();
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
Problem 4: Missing Registration
builder.Register<IService, ServiceImpl>(Lifetime.Singleton);
Problem 5: Circular Dependency
public class ServiceA
{
public ServiceA(ServiceB b) { }
}
public class ServiceB
{
public ServiceB(ServiceA a) { }
}
public class ServiceA
{
private readonly Lazy<ServiceB> _b;
public ServiceA(Lazy<ServiceB> b) => _b = b;
}
Safe Resolution Patterns
TryResolve Extension
public static class VContainerExtensions
{
public static bool TryResolve<T>(this IObjectResolver resolver, out T result)
{
result = default;
try
{
if (resolver == null) return false;
result = resolver.Resolve<T>();
return true;
}
catch (VContainerException)
{
return false;
}
}
public static T ResolveOrDefault<T>(this IObjectResolver resolver, T defaultValue = default)
{
return resolver.TryResolve<T>(out var result) ? result : defaultValue;
}
}
if (scope.Container.TryResolve<IState>(out var state))
{
await state.Exit();
}
Scope Alive Check
public static bool IsAlive(this LifetimeScope scope)
{
try
{
return scope != null && scope.Container != null;
}
catch
{
return false;
}
}
Testing with VContainer
Mocking Dependencies
[TestFixture]
public class PlayerControllerTests
{
private LifetimeScope _testScope;
[SetUp]
public void SetUp()
{
_testScope = LifetimeScope.Create(builder =>
{
var mockInput = Substitute.For<IInputService>();
builder.RegisterInstance(mockInput);
builder.Register<PlayerController>(Lifetime.Singleton);
});
}
[TearDown]
public void TearDown()
{
_testScope?.Dispose();
}
[Test]
public void PlayerController_Move_UpdatesPosition()
{
var controller = _testScope.Container.Resolve<PlayerController>();
}
}
Testing Installers
[Test]
public void Installer_RegistersAllDependencies()
{
using var scope = LifetimeScope.Create(new GameInstaller());
Assert.DoesNotThrow(() => scope.Container.Resolve<IPlayerService>());
Assert.DoesNotThrow(() => scope.Container.Resolve<IInputService>());
}
Editor Cleanup (Critical for Unity Editor)
public static class EditorLifetimeScopeCleanup
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ClearGlobalInstallers()
{
var type = typeof(LifetimeScope);
var field = type.GetField("GlobalExtraInstallers",
BindingFlags.NonPublic | BindingFlags.Static);
if (field?.GetValue(null) is IList list)
{
list.Clear();
}
}
}
Best Practices
- Prefer constructor injection - Explicit dependencies, easier testing
- Use interfaces - Enables mocking and swapping implementations
- Scope appropriately - Don't make everything Singleton
- Dispose child scopes - Prevent memory leaks
- Use CancellationToken - Graceful async cleanup
- Avoid property/field injection - Except for optional dependencies
- Test your installers - Catch missing registrations early
- Guard against disposed scopes - Use TryResolve in edge cases
Troubleshooting
| Error | Cause | Solution |
|---|
Unable to resolve | Not registered | Add registration |
Circular dependency | A→B→A | Use Lazy or factory |
Object disposed | Scope disposed | Check lifecycle, add guards |
Multiple registrations | Duplicate registration | Remove duplicate |
Invalid cast | Wrong type registered | Check As() calls |