【Unity】VContainerで動的に生成したGameObjectにDIする方法

VContainerで動的に生成したGameObjectにDIする方法についてまとめました。

VContainer 1.15.3

はじめに

本記事では VContainer を使って動的に生成したGameObjectにDIする方法についてまとめます。

例として、ゲーム中に動的に多数の敵が生成され、それぞれの敵は自身が行動したイベントを他に伝えるためのイベントバスを持つものとします。
まずイベントバス以下のように定義します(説明用なのでログを出力するだけ)。

using UnityEngine;

public sealed class EventBus
{
    public void Publish<T>(T message)
    {
        Debug.Log($"Publish: {message}");
    }
}

敵のクラスは以下の通りです。
このMonoBehaviourをアタッチしたPrefabを動的に生成し、IEventBusをDIするケースを考えます。

using UnityEngine;

public sealed class Enemy : MonoBehaviour
{
    private EventBus _eventBus;

    public void Initialize(EventBus eventBus)
    {
        _eventBus = eventBus;
        Debug.Log($"Enemy.Initialize: {eventBus}");
    }
}

なおVContainerの基本的な使い方については以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

パターン1: Enemyを生成するクラス(Spawner)にDIする場合

上述のように多数の敵を生成するケースでは、敵の生成を管理するクラスが普通はありそうなので、まずこのケースについて考えます。
以下のように敵の生成を管理するクラスを作成します。

using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;
using VContainer;

public sealed class EnemySpawner : MonoBehaviour
{
    [SerializeField] private Enemy prefab;
    [SerializeField] private Button spawnButton;
    private EventBus _eventBus;

    // EventBusをVContainerにより注入する
    [Inject]
    public void Construct(EventBus eventBus)
    {
        _eventBus = eventBus;
    }

    private void Start()
    {
        // ボタンを押したら敵がスポーンされる
        spawnButton.onClick.AddListener(() =>
        {
            // イベントバスが注入されてなかったらエラー
            Assert.IsNotNull(_eventBus);
            
            var enemy = Instantiate(prefab);
            // イベントバスを渡す
            enemy.Initialize(_eventBus);
        });
    }
}

コメントに書いた通り、VContainerにはこのMonoBehaviorを登録してEventBusをDIします。
このクラスが生成したEnemyにはそれを受け渡すだけです。
DIは以下のように行います。

using VContainer;
using VContainer.Unity;

public sealed class CompositionRoot : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<EventBus>(Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<EnemySpawner>();
    }
}

EnemySpawnerCompositionRootをシーン上の適当なGameObjectにアタッチして、EnemySpawnerEnemyのPrefabとButtonを持たせて実行すれば動作確認できます。

パターン2: Spawnerを介しつつEnemyにDIしたい場合

前節のようにEnemySpawnerにDIするのではなく、EnemyにDIしたいんだ、というケースもあると思います。

using UnityEngine;
using VContainer;

public sealed class Enemy : MonoBehaviour
{
    private EventBus _eventBus;

        // Enemyに対してDIする
    [Inject]
    public void Initialize(EventBus eventBus)
    {
        _eventBus = eventBus;
        Debug.Log($"Enemy.Initialize: {eventBus}");
    }
}

あまりDIするポイントを細かくしすぎるとコードがカオスになっていきますし(よくある)、今回のケースでは必要ありませんが、あくまで例としてこういうケースがあるとします。
このような場合にはEnemySpawnerにDIコンテナ自体をDIしてIObjectResolver.Instantiate を使ってEnemyインスタンス化することで依存性を注入できます。

using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;
using VContainer;
using VContainer.Unity;

public sealed class EnemySpawner : MonoBehaviour
{
    [SerializeField] private Enemy prefab;
    [SerializeField] private Button spawnButton;
    private IObjectResolver _diContainer;

    // DIコンテナを注入
    [Inject]
    public void Initialize(IObjectResolver container)
    {
        _diContainer = container;
    }

    private void Start()
    {
        spawnButton.onClick.AddListener(() =>
        {
            Assert.IsNotNull(_diContainer);
            
            // IObjectResolver.Instantiateでインスタンスを生成
            _diContainer.Instantiate(prefab);
        });
    }
}

パターン3: ファクトリメソッドをDIする場合

ちなみに以下のようにファクトリメソッドをDIすることもできます。

using System;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;
using VContainer;

public sealed class EnemySpawner : MonoBehaviour
{
    [SerializeField] private Button spawnButton;

    // Enemyのファクトリメソッド
    private Func<Enemy> _enemyFactory;

    // ファクトリメソッドをDIする
    [Inject]
    public void Initialize(Func<Enemy> enemyFactory)
    {
        _enemyFactory = enemyFactory;
    }

    private void Start()
    {
        spawnButton.onClick.AddListener(() =>
        {
            Assert.IsNotNull(_enemyFactory);

            // ファクトリメソッドを呼び出してEnemyを生成
            _enemyFactory();
        });
    }
}

CompositionRoot は以下の通りです。
(あくまで例で、設計としてはかなりダメなので悪しからず)

using System;
using UnityEngine;
using VContainer;
using VContainer.Unity;

public sealed class CompositionRoot : LifetimeScope
{
    [SerializeField] private Enemy enemyPrefab;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<EventBus>(Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<EnemySpawner>();

        // ファクトリを登録する
        builder.RegisterFactory(container =>
            {
                Func<Enemy> factory = () => container.Instantiate(enemyPrefab);
                return factory;
            },
            Lifetime.Singleton);
    }
}

参考

vcontainer.hadashikick.jp

vcontainer.hadashikick.jp