VContainer で動的に GameObject を生成する

# はじめに

[VContainer](https://github.com/hadashiA/VContainer) の入門記事は結構あるのですが、動的に `GameObject` を生成する方法がイマイチよく分からなかったので、サンプルを作成しました。

## ソースコード

以下に記載しているプログラムは GitHub にも公開しています。

https://github.com/akiojin/VContainer

ファクトリメソッドを使用する方法を記載していましたが、もっと簡単に書ける方法がありましたので、修正しました。

具体的には `IObjectResolver` をインジェクトすることで、ファクトリメソッドを介さずに、直接依存注入する形式です。

### GameLifetimeScope

```C#
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField]
    EnemySpawner EnemySpawner;

    protected override void Configure(IContainerBuilder builder)
    {
        // Enemy に依存注入する Timer クラス
        builder.RegisterComponentOnNewGameObject<Timer>(Lifetime.Singleton);
        // 実際に敵を生成するクラス
        builder.RegisterComponentInNewPrefab(EnemySpawner, Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<EnemySpawnTrigger>();

        // ヒエラルキーに配置済みの Enemy クラスに依存注入
        foreach (var enemy in FindObjectsByType<Enemy>(FindObjectsSortMode.None)) {
            builder.RegisterBuildCallback(resolver => {
                resolver.Inject(enemy);
            });
        }
    }
}
```

### EnemySpawner

```C#
using UnityEngine;
using VContainer;

public class EnemySpawner : MonoBehaviour
{
    [Inject]
    IObjectResolver Resolver;

    [SerializeField]
    Enemy EnemyPrefab;

    public Enemy Generate(string ID)
    {
        Debug.Log($"Generate ID: {ID}");

        // 今回は Instantiate を呼び出しているが、
        // ObjectPool などで、インスタンスを生成して返すことも可能。
        // 例) var enemy = ObjectPool.Get(ID).GetCompnent<Enemy>();
        var enemy = Instantiate(EnemyPrefab);

        // 動的生成されたインスタンスに対して依存注入
        Resolver.Inject(enemy);

        return enemy;
    }
}
```

### Enemy

```C#
using UnityEngine;
using VContainer;

public class Enemy : MonoBehaviour
{
    [Inject]
    Timer timer;

    [SerializeField]
    float Duration = 10f;

    float lifetime;

    void Update()
    {
        if ((lifetime += Time.deltaTime) >= Duration) {
            Destroy(gameObject);
        }

        if (timer.Timeout) {
            Destroy(gameObject);
        }
    }
}
```

### EnemySpawnTrigger

```C#
using System;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using Random = UnityEngine.Random;

public class EnemySpawnTrigger : MonoBehaviour
{
    [SerializeField]
    Button EnemySpawnButton;

    [Inject]
    EnemySpawner EnemySpawner;

    [Inject]
    Timer timer;

    void Start()
        => EnemySpawnButton.onClick.AddListener(OnClicked);

    void Update()
    {
        if (timer.Timeout) {
            EnemySpawnButton.onClick.RemoveListener(OnClicked);
        }
    }

    void OnClicked()
    {
        var ID = Guid.NewGuid().ToString();
        var enemy = EnemySpawner.Generate(ID);

        enemy.transform.position = new(Random.Range(0f, 320f), Random.Range(0f, 180f), 0f);
    }
}
```

### Timer

```C#
using UnityEngine;

public class Timer : MonoBehaviour
{
    float TimeoutSeconds = 10f;

    public bool Timeout => !enabled;

    void Update()
    {
        if ((TimeoutSeconds -= Time.deltaTime) <= 0) {
            Debug.Log("Timeout");
            enabled = false;
        }
    }
}
```

<details><summary>古いコード</summary>
### GameLifetimeScope

```C#
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField]
    EnemyGenerator EnemyGenerator;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponentOnNewGameObject<Timer>(Lifetime.Singleton);
        builder.RegisterComponentInNewPrefab(EnemyGenerator, Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<EnemySpawner>();
        builder.RegisterFactory<string, Enemy>(container => (ID) => {
            return container.Resolve<EnemyGenerator>().Generate(container, ID);
        }, Lifetime.Scoped);
    }
}
```

### Timer

```C#
using UnityEngine;

public class Timer : MonoBehaviour
{
    float TimeoutSeconds = 10f;

    public bool Timeout => !enabled;

    void Update()
    {
        if ((TimeoutSeconds -= Time.deltaTime) <= 0) {
            Debug.Log("Timeout");
            enabled = false;
        }
    }
}
```

### Enemy

```C#
using UnityEngine;
using VContainer;

public class Enemy : MonoBehaviour
{
    [Inject]
    Timer timer;

    [SerializeField]
    float Duration = 10f;

    float lifetime;

    void Update()
    {
        if ((lifetime += Time.deltaTime) >= Duration) {
            Destroy(gameObject);
        }

        if (timer.Timeout) {
            Destroy(gameObject);
        }
    }
}
```

### EnemyGenerator

```C#
using UnityEngine;
using VContainer;

public class EnemyGenerator : MonoBehaviour
{
    [SerializeField]
    GameObject EnemyPrefab;

    public Enemy Generate(IObjectResolver resolver, string ID)
    {
        Debug.Log($"Generate ID: {ID}");

        // 今回は Instantiate を呼び出しているが、
        // ObjectPool などで、インスタンスを生成して返すことも可能。
        // 例) var enemy = ObjectPool.Get(ID).GetCompnent<Enemy>();
        var enemy = Instantiate(EnemyPrefab).GetComponent<Enemy>();

        // 動的生成されたインスタンスに対して DI 注入
        resolver.Inject(enemy);

        return enemy;
    }
}
```

### EnemySpawnTrigger

```C#
using System;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using Random = UnityEngine.Random;

public class EnemySpawnTrigger : MonoBehaviour
{
    [SerializeField]
    Button EnemySpawnButton;

    /// <summary>
    /// RegisterFactory で登録されたファクトリメソッドを依存注入する
    /// </summary>
    [Inject]
    Func<string, Enemy> GenerateEnemy;

    [Inject]
    Timer timer;

    void Start()
        => EnemySpawnButton.onClick.AddListener(OnClicked);

    void Update()
    {
        if (timer.Timeout) {
            EnemySpawnButton.onClick.RemoveListener(OnClicked);
        }
    }

    void OnClicked()
    {
        var ID = Guid.NewGuid().ToString();
        var enemy = GenerateEnemy(ID);

        enemy.transform.position = new(Random.Range(0f, 320f), Random.Range(0f, 180f), 0f);
    }
}
```
</details>