Skip to content
cccskills
FreePublished on GitHub

tools-unity-vcontainer

VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md12.2 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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

// Package Manager
// Add to manifest.json:
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.15.4"

Core Concepts

Lifetime Types

LifetimeDescriptionUse Case
SingletonOne instance per containerGlobal services, managers
ScopedOne instance per scopeFeature-specific services
TransientNew instance every resolveFactories, 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)
    {
        // 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();
    }
}

Instance Registration

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>());
}

Factory Registration

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;
    }
}

EntryPoint Registration

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);
    }
}

Injection Patterns

Constructor Injection (Preferred)

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;
    }
}

Method Injection

public class EnemyBehavior : MonoBehaviour
{
    private ITargetingService _targeting;
    
    [Inject]
    public void Construct(ITargetingService targeting)
    {
        _targeting = targeting;
    }
}

Property Injection (Use Sparingly)

public class DebugManager
{
    // Optional dependency
    [Inject]
    public IAnalyticsService Analytics { get; set; }
}

Field Injection (Avoid in Production)

public class LegacyComponent : MonoBehaviour
{
    [Inject] private IService _service; // Avoid - harder to test
}

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)
    {
        // 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();
    }
}

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);
    }
}

// Usage
var scope = parentScope.CreateChild(new WorldStateInstaller());

Enqueue Pattern for Segment Loading

using (LifetimeScope.Enqueue(segmentCache))
{
    var stateScope = _rootScope.CreateChild(installer);
    // segmentCache registrations included
}

Common Crash Patterns & Fixes

Problem 1: Resolving After Disposal

// 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();

Problem 2: Singleton Referencing Scoped

// 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>();
    }
}

Problem 3: Fire-and-Forget Without Guard

// 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();
}

Problem 4: Missing Registration

// 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

Problem 5: Circular Dependency

// 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;
}

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;
    }
}

// Usage
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 =>
        {
            // 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...
    }
}

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)

// 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();
        }
    }
}

Best Practices

  1. Prefer constructor injection - Explicit dependencies, easier testing
  2. Use interfaces - Enables mocking and swapping implementations
  3. Scope appropriately - Don't make everything Singleton
  4. Dispose child scopes - Prevent memory leaks
  5. Use CancellationToken - Graceful async cleanup
  6. Avoid property/field injection - Except for optional dependencies
  7. Test your installers - Catch missing registrations early
  8. Guard against disposed scopes - Use TryResolve in edge cases

Troubleshooting

ErrorCauseSolution
Unable to resolveNot registeredAdd registration
Circular dependencyA→B→AUse Lazy<T> or factory
Object disposedScope disposedCheck lifecycle, add guards
Multiple registrationsDuplicate registrationRemove duplicate
Invalid castWrong type registeredCheck As<T>() calls

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

adopt

Free

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

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.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

The English summary is being prepared. Showing the original description.

IdoCohen560/claude-unity-game-studio22Updated Jul 8, 2026

See all skills by IdoCohen560

Report a problem with this skill