tools-unity-vcontainer
VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.
Mobile-specific Unity optimization patterns for memory, battery, thermal, and performance.
End-of-story completion review. Reads the story file, verifies each acceptance criterion against the implementation, checks for GDD/ADR deviations, prompts code review, updates story status to Complete, and surfaces the next ready story from the sprint.
Unity Addressables patterns for asset loading, memory management, reference counting, and remote content delivery.
Unity animation patterns including Animancer, state machines, and performance optimization.
Behavior Designer patterns for AI behavior trees including task creation, shared variables, conditionals, and debugging.
| name | tools-unity-vcontainer |
| description | VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls. |
VContainer is a lightweight, fast DI container for Unity. This skill covers registration patterns, lifecycle management, and common pitfalls that cause crashes.
// Package Manager
// Add to manifest.json:
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.15.4"
| 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!
public class GameInstaller : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// Interface to implementation
builder.Register<IPlayerService, PlayerService>(Lifetime.Singleton);
// Concrete type
builder.Register<GameManager>(Lifetime.Singleton);
// With interfaces
builder.Register<AudioSystem>(Lifetime.Singleton)
.AsImplementedInterfaces()
.AsSelf();
}
}
protected override void Configure(IContainerBuilder builder)
{
// Pre-existing instance
builder.RegisterInstance(existingService);
// ScriptableObject from Resources
var config = Resources.Load<GameConfig>("GameConfig");
builder.RegisterInstance(config);
// Component on same GameObject
builder.RegisterComponent(GetComponent<AudioSource>());
}
protected override void Configure(IContainerBuilder builder)
{
// Simple factory
builder.RegisterFactory<Enemy>(() => new Enemy(), Lifetime.Scoped);
// Factory with parameters
builder.RegisterFactory<string, IWeapon>(name =>
new Weapon(name), Lifetime.Transient);
// Async factory
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;
}
}
protected override void Configure(IContainerBuilder builder)
{
// IStartable - called once after container built
builder.RegisterEntryPoint<GameInitializer>();
// ITickable - called every frame
builder.RegisterEntryPoint<GameLoop>();
// IAsyncStartable - async initialization
builder.RegisterEntryPoint<AsyncGameInitializer>();
// Multiple interfaces
builder.Register<GameManager>(Lifetime.Singleton)
.AsImplementedInterfaces();
}
public class GameInitializer : IStartable
{
public void Start()
{
// Called after container is built
}
}
public class AsyncGameInitializer : IAsyncStartable
{
public async UniTask StartAsync(CancellationToken ct)
{
await LoadGameDataAsync(ct);
}
}
public class PlayerController
{
private readonly IInputService _input;
private readonly IPlayerService _player;
// Dependencies injected via constructor
public PlayerController(IInputService input, IPlayerService player)
{
_input = input;
_player = player;
}
}
public class EnemyBehavior : MonoBehaviour
{
private ITargetingService _targeting;
[Inject]
public void Construct(ITargetingService targeting)
{
_targeting = targeting;
}
}
public class DebugManager
{
// Optional dependency
[Inject]
public IAnalyticsService Analytics { get; set; }
}
public class LegacyComponent : MonoBehaviour
{
[Inject] private IService _service; // Avoid - harder to test
}
public class StateNavigationController
{
private readonly LifetimeScope _rootScope;
private readonly List<LifetimeScope> _stateStack = new();
public async UniTask NavigateTo(IStateTransitionData data)
{
// Create child scope for new state
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();
// CRITICAL: Dispose scope AFTER exit completes
await UniTask.Yield(); // Allow pending callbacks
scope.Dispose();
}
}
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);
}
}
// Usage
var scope = parentScope.CreateChild(new WorldStateInstaller());
using (LifetimeScope.Enqueue(segmentCache))
{
var stateScope = _rootScope.CreateChild(installer);
// segmentCache registrations included
}
// BAD: Scope disposed while async operation pending
await state.Exit();
scope.Dispose(); // Crash if Exit has pending callbacks
// GOOD: Wait for pending operations
await state.Exit();
await UniTask.Yield(); // Allow callbacks to complete
scope.Dispose();
// BAD: Global singleton holds scoped reference
builder.Register<GlobalManager>(Lifetime.Singleton); // Root scope
builder.Register<FeatureService>(Lifetime.Scoped); // Child scope
public class GlobalManager
{
// This will break when child scope disposes!
public GlobalManager(FeatureService feature) { }
}
// GOOD: Use factory or lazy resolution
public class GlobalManager
{
private readonly IObjectResolver _resolver;
public GlobalManager(IObjectResolver resolver)
{
_resolver = resolver;
}
public FeatureService GetFeature()
{
return _resolver.Resolve<FeatureService>();
}
}
// BAD: No cancellation when scope disposes
public async UniTaskVoid DoAsyncWork()
{
await UniTask.Delay(5000);
_service.DoSomething(); // May crash if disposed
}
// GOOD: Use CancellationToken
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();
}
// VContainerException: Unable to resolve IService
// Check 1: Is it registered?
builder.Register<IService, ServiceImpl>(Lifetime.Singleton);
// Check 2: Is the assembly referenced?
// Add to asmdef references
// Check 3: Is the scope correct?
// Child scope can't resolve from parent without proper hierarchy
// VContainerException: Circular dependency detected
// BAD
public class ServiceA
{
public ServiceA(ServiceB b) { }
}
public class ServiceB
{
public ServiceB(ServiceA a) { }
}
// GOOD: Break with Lazy<T> or factory
public class ServiceA
{
private readonly Lazy<ServiceB> _b;
public ServiceA(Lazy<ServiceB> b) => _b = b;
}
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;
}
}
// Usage
if (scope.Container.TryResolve<IState>(out var state))
{
await state.Exit();
}
public static bool IsAlive(this LifetimeScope scope)
{
try
{
return scope != null && scope.Container != null;
}
catch
{
return false;
}
}
[TestFixture]
public class PlayerControllerTests
{
private LifetimeScope _testScope;
[SetUp]
public void SetUp()
{
_testScope = LifetimeScope.Create(builder =>
{
// Register mocks
var mockInput = Substitute.For<IInputService>();
builder.RegisterInstance(mockInput);
// Register real implementation under test
builder.Register<PlayerController>(Lifetime.Singleton);
});
}
[TearDown]
public void TearDown()
{
_testScope?.Dispose();
}
[Test]
public void PlayerController_Move_UpdatesPosition()
{
var controller = _testScope.Container.Resolve<PlayerController>();
// Test...
}
}
[Test]
public void Installer_RegistersAllDependencies()
{
using var scope = LifetimeScope.Create(new GameInstaller());
Assert.DoesNotThrow(() => scope.Container.Resolve<IPlayerService>());
Assert.DoesNotThrow(() => scope.Container.Resolve<IInputService>());
}
// Prevent VContainerException when exiting Play Mode
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();
}
}
}
| 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 |