Unityにおける依存性注入(DI)とVContainerの基本解説

2024年12月9日

Unityは直感的で強力なゲームエンジンであり、多くの初心者や経験者が愛用しています。一方で、プロジェクトが大規模になると、コードの依存関係や構造が複雑化し、変更や保守が難しくなることがあります。例えば、GameObject.Find や [SerializeField] を多用していると、コードが特定のオブジェクトや名前に依存してしまい、結果として柔軟性やテストのしやすさが損なわれることがあります。

これらの課題を解決するために役立つのが、依存性注入(DI: Dependency Injection) という設計パターンです。Unityには、VContainer という軽量でシンプルなDIフレームワークがあり、プロジェクトの設計を改善し、保守性や柔軟性を向上させることができます。本資料では、DIの基本概念から、Unityでの実践方法、VContainerの使い方について、初心者でも分かりやすい形で解説します。

これを通じて、以下のような課題を解決できるようになります:

  • プロジェクトのコード構造がシンプルになり、変更に強い設計が可能になる。
  • テストが容易になり、モジュールごとの動作を検証できる。
  • Unityの標準的な開発手法から一歩進んだ設計が習得できる。

1. 依存性注入(DI)とは?

依存性注入(Dependency Injection、略してDI) とは、プログラムが依存する外部コンポーネントを直接生成せず、外部から注入される仕組みを指します。

DIのメリット

  • 疎結合な設計
    各クラスが特定の実装に依存しないため、変更に強く再利用性が高い。
  • テストのしやすさ
    モックやテスト用のオブジェクトを差し替えやすい。
  • 保守性の向上
    依存関係を明示的に管理することで、コードが読みやすくなり、保守が簡単。

Unityでの課題

Unityの標準的な参照方法(GameObject.Find[SerializeField])では、次のような課題があります:

  1. 結合度が高くなる(他のクラスや名前に依存)。
  2. テストや再利用が困難。
  3. 保守性が低下する。

これを解決するために、VContainer のようなDIフレームワークを導入することが推奨されます。


2. VContainerとは?

VContainer は Unity 用の軽量な DI フレームワークで、Zenject よりもシンプルな設計が特徴です。
依存関係の登録と解決、ライフサイクル管理などをサポートし、Unity の開発効率を向上させます。

VContainerの主な機能

  1. 依存関係の登録と解決
    クラス間の依存関係を自動的に管理します。
  2. ライフサイクル管理
    オブジェクトのライフタイム(Singleton, Transient, Scoped)を簡単に制御できます。
  3. Unityとの統合
    MonoBehaviour や ScriptableObject もサポート。

3. VContainerの使い方(IInputServiceを例に)

ステップ1: VContainerをインストール


ステップ2: インターフェースを設計する

IInputService インターフェースを作成して、入力処理を抽象化します。

public interface IInputService
{
    Vector3 GetMovementInput();
    bool GetJumpInput();
}

ステップ3: 実装クラスを作成する

  • KeyboardInputService
    キーボード入力を処理する実装。
using UnityEngine;

public class KeyboardInputService : IInputService
{
    public Vector3 GetMovementInput()
    {
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        return new Vector3(x, 0, z);
    }

    public bool GetJumpInput()
    {
        return Input.GetButtonDown("Jump");
    }
}
  • GamepadInputService
    ゲームパッド入力を処理する実装。
using UnityEngine;

public class GamepadInputService : IInputService
{
    public Vector3 GetMovementInput()
    {
        float x = Input.GetAxis("GamepadHorizontal");
        float z = Input.GetAxis("GamepadVertical");
        return new Vector3(x, 0, z);
    }

    public bool GetJumpInput()
    {
        return Input.GetButtonDown("GamepadJump");
    }
}

ステップ4: VContainerで依存関係を登録する

VContainerの LifetimeScope を拡張して依存関係を登録します。

using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // IInputService に対して KeyboardInputService を登録
        builder.Register<IInputService, KeyboardInputService>(Lifetime.Singleton);

        // 他の依存性を登録
        builder.Register<PlayerController>(Lifetime.Singleton);
    }
}

ステップ5: 依存関係を注入して使用する

  • PlayerControllerIInputService を注入して利用します。
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private IInputService inputService;

    [Inject]
    public void Construct(IInputService inputService)
    {
        this.inputService = inputService;
    }

    void Update()
    {
        // 移動入力を取得
        Vector3 movement = inputService.GetMovementInput();
        transform.Translate(movement * Time.deltaTime * 5.0f);

        // ジャンプ入力を処理
        if (inputService.GetJumpInput())
        {
            Debug.Log("Player Jumped!");
        }
    }
}

4. テスト用モックの作成

モックを利用してテスト用の IInputService を作成します。

public class MockInputService : IInputService
{
    public Vector3 GetMovementInput() => new Vector3(1, 0, 0); // 常に右に移動
    public bool GetJumpInput() => true; // 常にジャンプ
}

// テスト例
var mockInputService = new MockInputService();
var playerController = new PlayerController();
playerController.Construct(mockInputService);

// プレイヤーの移動とジャンプをテスト
playerController.Update();

5. ライフサイクル管理

VContainerでは、以下のライフサイクルを指定して依存関係を管理できます。

ライフタイム説明使用例
Singletonアプリ全体で1つのインスタンスを共有するゲーム設定、スコア管理など
Transient要求があるたびに新しいインスタンスを生成する弾丸やエフェクトなど
Scoped特定のスコープ内でインスタンスを共有する特定シーンの一時的なデータ

登録例:

builder.Register<GameSettings>(Lifetime.Singleton);
builder.Register<Bullet>(Lifetime.Transient);
builder.Register<LevelData>(Lifetime.Scoped);

6. 注意点

  1. インターフェースを活用
    抽象化を行うことで、柔軟性と再利用性を向上させます。
  2. 依存関係の適切な登録
    必要以上に依存関係を登録しすぎないよう注意してください。
  3. テストを意識した設計
    モックやスタブを活用し、テストしやすいコードを書く習慣を身につけましょう。

まとめ

  • VContainer は、UnityのDIを簡単かつ効率的に実現するフレームワークです。
  • 入力処理(IInputService)を抽象化することで、柔軟で再利用可能な設計を行えます。
  • モックやライフサイクル管理を活用し、テストや拡張性に優れたプロジェクトを構築できます。

VContainerを活用して、シンプルでモジュール化されたゲーム開発を始めてみましょう!