ゲーム開発
Unity
UnrealEngine
C++
Blender
Houdini
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • Houdini
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20220914_01

      【Unity】DIコンテナの概要とVContainerの使い方について

      Unityソフトウェア設計VContainerDIコンテナ
      2022-09-14

      マイケル
      マイケル
      みなさんこんにちは!
      マイケルです!!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      今日はUnityのDIフレームワークである VContainer について
      使い方を紹介していくぜ!
      エレキベア
      エレキベア
      最近はUnity界隈でDIが流行ってるクマね〜〜〜
      マイケル
      マイケル
      今回の記事を書くにあたって、下記の公式ページとスライドを参考にさせていただきました!
      リファリンスも分かりやすくまとまっているのでおすすめです。

      About | VContainer
      ↑VContainerの公式ページ

      Unity専用最速DIコンテナVContainer と、UnityにおけるDIの勘所
      ↑作者本人の説明スライド

      エレキベア
      エレキベア
      シンプルで使いやすそうクマね〜〜〜
      マイケル
      マイケル
      いきなりDIと言われても分からない方もいると思うので、
      最初に 依存関係やDIについて説明した後、
      ライブラリの使い方を説明
      していこうかと思います!

      DI(依存性の注入)とは

      マイケル
      マイケル
      まずはDIとは何か?という部分について解説していきます。
      以前、SOLID原則について紹介しましたが、この中で出てくる
      「DIP(依存性逆転の原則)」を実現するための手段としてDIが用いられます。
      エレキベア
      エレキベア
      確か 依存関係を逆転させて、抽象に依存させるべき
      という原則だったクマね
      ↑抽象に依存している状態
      マイケル
      マイケル
      その通り!
      依存関係を制御するための手段として、「DI」「DIコンテナ」というものが使われているんだ。
      まずはイメージを掴むために簡単なDIの例を見てみよう!

      依存関係とDI

      マイケル
      マイケル
      まず、下記のようなマイケルクラスがあったとします。
      この中では所持モンスターを変数として持っています。
      /// <summary>
      /// マイケルクラス
      /// </summary>
      public class Michael
      {
          /// <summary>
          /// 所持モンスター
          /// </summary>
          private ElekiBear _haveMonster;
          public Michael(ElekiBear monster)
          {
              _haveMonster = monster;
          }
      }
      
      ↑マイケルクラスの実装
      マイケル
      マイケル
      この実装はよく見ると、エレキベアクラスをそのまま使っているため、
      エレキベア以外のモンスターを所持することはできません
      エレキベア
      エレキベア
      誰が所持モンスターになんてなるかクマ
      マイケル
      マイケル
      この状態のことをマイケルクラスはエレキベアクラスに依存していると言われます。
      UML図で書くと下記のようになります。依存の向きが矢印の向きですね。

      ↑マイケルクラスがエレキベアクラスに依存している
      エレキベア
      エレキベア
      エレキベア以外のモンスターはダメということクマね
      マイケル
      マイケル
      これを解決するにはインターフェイスを使用します。
      下記のようにIMonster型で定義し直すと、IMonsterをimplementしているクラスであればどれでも所持モンスターとして設定できるようになります。
      /// <summary>
      /// マイケルクラス
      /// </summary>
      public class Michael
      {
          /// <summary>
          /// 所持モンスター
          /// </summary>
          private IMonster _haveMonster;
          public Michael(IMonster monster)
          {
              _haveMonster = monster;
          }
      }
      
      ↑インターフェイスで定義しなおした状態
      マイケル
      マイケル
      これをUML図で表すとこんな感じ!
      どのクラスもインターフェイスに依存しており、向きが逆転していることが分かります!

      ↑矢印の向きが逆転している!
      エレキベア
      エレキベア
      浮気クマか・・・
      ゴロヤン
      ゴロヤン
      ゴロ〜〜〜(モンスター呼ばわりは心外です)
      マイケル
      マイケル
      これが冒頭で話した 抽象に依存している状態になります!
      設定する側はこんな感じです!
      /// <summary>
      /// マイケル管理クラス
      /// </summary>
      public class MichaelManager
      {
          public void Initialize()
          {
              var michael = new Michael(new ElekiBear());
              michael = new Michael(new Goloyan()); // 別モンスターにも設定できる
          }
      }
      
      ↑注入している例
      マイケル
      マイケル
      依存性(オブジェクト)を引き渡していますが、
      このことを依存性の注入(DI)と言います。
      エレキベア
      エレキベア
      注入する依存性というのは渡すオブジェクトへの依存のことだったクマね
      マイケル
      マイケル
      ただ見ても分かる通り、今度は注入するクラス(MichaelManager)の方が
      各モンスタークラスに依存
      するようになっています。
      これを解決するために、依存関係を定義するクラスをDIコンテナという形で分けることで
      DIを実現しやすくする
      、というわけです。
      public class SampleLifetimeScope : LifetimeScope
      {
          protected override void Configure(IContainerBuilder builder)
          {
              // インターフェースを登録
              builder.Register<IMonster, ElekiBear>(Lifetime.Singleton);
          }
      }
      
      ↑DIコンテナの例(VContainerでの実装。IMonsterとElekiBearを紐づけている)
      エレキベア
      エレキベア
      なるほどクマ
      DIコンテナは依存性の注入(DI)をしやすくするためのものだったクマね
      マイケル
      マイケル
      その通り!
      各用語の意味をまとめると下記のようになります!

      • DIP(依存性逆転の原則)
        依存の向きを逆転させて抽象に依存すべきである、という原則。
      • DI(依存性の注入)
        外側からオブジェクトの参照を渡すことで依存関係を取り除く手法。
      • DIコンテナ
        依存関係を別途定義するコンテナを作成し、DIを行いやすくするもの。

      エレキベア
      エレキベア
      用語がややこしいクマが、なんとなくイメージは分かったクマね
      マイケル
      マイケル
      DIやDIコンテナのイメージが湧いてきたところで、
      VContainerの使い方について見ていこう!

      VContainerの使い方

      マイケル
      マイケル
      VContainerは下記の特徴を持つUnityのDIフレームワークです!

      • Unityに最適化されたDIフレームワーク
      • MonoBehaviourを制御することを思想としている
      • 類似ライブラリのZenjectと比べてシンプルで高速

      マイケル
      マイケル
      DIについてやZenjectとの比較については、
      下記にまとまっているのでご参照ください!

      Comparing to Zenject | VContainer

      DIって何? | VContainer

      エレキベア
      エレキベア
      リファレンスも分かりやすいクマ〜〜〜

      導入方法

      マイケル
      マイケル
      導入はunitypackageかUPM経由でインストールしましょう!

      インストール | VContainer

      ↑PackageManagerからインストール

      基本の使い方

      マイケル
      マイケル
      まずはDIコンテナとなるLifetimeScopeを定義します。
      ここでは処理の起点となるエントリーポイントとシングルトンのクラスを登録してみます。
      using VContainer;
      using VContainer.Unity;
      public class GameLifetimeScope : LifetimeScope
      {
          protected override void Configure(IContainerBuilder builder)
          {
              // シングルトン登録
              builder.Register<GameManager>(Lifetime.Singleton);
              // エントリーポイント登録
              builder.RegisterEntryPoint<GameEntryPoint>();
          }
      }
      
      ↑エントリーポイントとシングルトンを登録
      マイケル
      マイケル
      それぞれの処理は下記の通り!
      コンストラクタに[Inject]を指定することで登録したクラスが渡されるようになります!
      using VContainer.Unity;
      /// <summary>
      /// エントリーポイント
      /// </summary>
      public class GameEntryPoint : IStartable, ITickable
      {
          private readonly GameManager _gameManager;
          [Inject]
          public GameEntryPoint(GameManager gameManager)
          {
              _gameManager = gameManager;
          }
          public void Start()
          {
              _gameManager.OnStart();
          }
          public void Tick()
          {
              _gameManager.OnUpdate();
          }
      }
      
      ↑エントリーポイント
      /// <summary>
      /// ゲーム管理クラス
      /// </summary>
      public class GameManager
      {
          public void OnStart()
          {
              // TODO
          }
          public void OnUpdate()
          {
              // TODO
          }
      }
      
      ↑シングルトン
      マイケル
      マイケル
      VContainerに用意されているIStartable、ITickableといったライフサイクルのインターフェイスを使用することで、MonoBehaviourと同様の挙動を行うことができます。
      GameManagerクラスを見て分かる通り、何にも依存していないクリーンなコードになっていることが分かります。
      エレキベア
      エレキベア
      MonoBehaviour以外を起点にすることができるクマね
      マイケル
      マイケル
      次はインタフェースと紐づけて登録してみます。
      using VContainer;
      using VContainer.Unity;
      
      public class GameLifetimeScope : LifetimeScope
      {
          protected override void Configure(IContainerBuilder builder)
          {
      ・・・略・・・
      
              // サービス登録
              builder.Register<IHelloService, HelloService>(Lifetime.Singleton);
      
      ・・・略・・・
          }
      }
      
      マイケル
      マイケル
      こうすることで下記のようにコンストラクタに注入されます。
      using UnityEngine;
      
      /// <summary>
      /// ゲーム管理クラス
      /// </summary>
      public class GameManager
      {
          private readonly IHelloService _helloService;
      
          [Inject]
          public GameManager(IHelloService helloService)
          {
              _helloService = helloService;
          }
      
          public void OnStart()
          {
              var message = _helloService.GetHelloMessage();
              Debug.Log(message);
          }
      
          public void OnUpdate()
          {
          }
      }
      
      エレキベア
      エレキベア
      最初に見ていたDIの形クマね
      マイケル
      マイケル
      あとはLifetimeScopeクラスをオブジェクトにアタッチして実行すれば、
      正常に実行できることが確認できます。
      ↑LifetimeScopeクラスをアタッチ
      ↑ログが出力された
      エレキベア
      エレキベア
      シンプルに使えるクマね

      MVPの実装

      マイケル
      マイケル
      次は簡単なMVPの実装をしてみます。
      MonoBehaviourクラスはRegisterComponent関数で登録します。
      また、エントリーポイントは複数登録することもできます。
      using UnityEngine;
      using VContainer;
      using VContainer.Unity;
      public class GameLifetimeScope : LifetimeScope
      {
          [SerializeField] private GameView gameView;
          protected override void Configure(IContainerBuilder builder)
          {
              // シングルトン登録
              builder.Register<GameManager>(Lifetime.Singleton);
              // サービス登録
              builder.Register<IHelloService, HelloService>(Lifetime.Singleton);
              // コンポーネント登録
              builder.RegisterComponent(gameView); // UIを登録
              // エントリーポイント登録
              builder.RegisterEntryPoint<GameEntryPoint>();
              builder.RegisterEntryPoint<GamePresenter>(); // 複数登録できる
          }
      }
      
      ↑ViewとPresenterの登録
      マイケル
      マイケル
      それぞれの実装は下記の通り!
      PresenterがViewクラスを受け取る形になっていることが確認できます。
      using VContainer;
      using VContainer.Unity;
      
      public class GamePresenter : IStartable
      {
          private readonly IHelloService _helloService;
          private readonly GameView _gameView;
      
          [Inject]
          public GamePresenter(IHelloService helloService, GameView gameView)
          {
              _helloService = helloService;
              _gameView = gameView;
          }
      
          public void Start()
          {
              var message = _helloService.GetHelloMessage();
              _gameView.SetSampleText(message);
          }
      }
      
      using UnityEngine;
      using UnityEngine.UI;
      
      public class GameView : MonoBehaviour
      {
          [SerializeField] private Text sampleText;
          public void SetSampleText(string text)
          {
              sampleText.text = text;
          }
      }
      
      マイケル
      マイケル
      処理を実行すると、正常に表示されることが確認できました!
      エレキベア
      エレキベア
      MVP実装もスマートに実装できるクマね〜〜

      その他の使い方

      マイケル
      マイケル
      基本的な使い方は以上になりますが、
      それ以外の機能や使い方についてもいくつか紹介します!

      Injectの方法

      マイケル
      マイケル
      先ほどはコンストラクタにInjectしましたが、
      関数、変数に対してもInjectすることができます。
      public class MonsterBehaviour : MonoBehaviour
      {
          // 変数にインジェクションする方法
          [Inject] private IHelloService _helloService;
      
          // 関数にインジェクションする方法
          private IHelloService2 _helloService2;
          [Inject]
          private void Construct(IHelloService2 helloService2)
          {
              _helloService2 = helloService2;
          }
          
      }
      
      マイケル
      マイケル
      ただし、下記のような理由からコンストラクタ経由でのInjectが推奨されています。
      • インスタンス化された時点で依存オブジェクトが揃っていることが保証される
      • 依存関係の見通しがよくなり、責務の把握がしやすくなる
      マイケル
      マイケル
      そのため、MonoBehaviourクラス以外は
      基本的にコンストラクタでInjectする方がよいと思います。
      エレキベア
      エレキベア
      責務が把握できてるとクラス分割の目安にもなるクマね〜〜

      LifetimeScopeでの登録方法

      マイケル
      マイケル
      LifetimeScopeでの登録方法には他にもいくつかあります。
      public class GameLifetimeScope : LifetimeScope
      {
          [SerializeField] private MonsterData monsterData; // ScriptableObject
          [SerializeField] private GameView gameView;       // MonoBehaviour
      
          protected override void Configure(IContainerBuilder builder)
          {
              // スコープの種類
              builder.Register<GameManager>(Lifetime.Singleton); // 親か自身のコンテナがオブジェクトを生成して保持する
              builder.Register<GameManager>(Lifetime.Scoped);    // 自身のコンテナがオブジェクトを生成して保持する
              builder.Register<GameManager>(Lifetime.Transient); // 毎回インスタンスを作成する
      
              // クラスの登録
              // 使用された時にインスタンス化する
              builder.Register<GameManager>(Lifetime.Singleton); // 具象クラス登録
              builder.Register<IHelloService, HelloService>(Lifetime.Singleton); // インターフェイスと紐づけて登録
      
              // インスタンス登録
              // 既に存在するインスタンスを登録する(ScriptableObjectなど)
              builder.RegisterInstance(monsterData);
      
              // コンポーネント登録
              // MonoBehaviourなど
              builder.RegisterComponent(gameView); // UIを登録
      
              // エントリーポイント登録
              builder.RegisterEntryPoint<GameEntryPoint>();
              builder.RegisterEntryPoint<GamePresenter>();
          }
      }
      
      マイケル
      マイケル
      詳細は公式のリファレンスをご参照ください!

      Lifetime Overview | VContainer

      エレキベア
      エレキベア
      オブジェクトのスコープ定義やインスタンス登録もできるクマね

      LifetimeScopeの親子階層定義

      マイケル
      マイケル
      また、LifetimeScopeは階層構造を定義することもできます。
      LifetimeScopeオブジェクトのParentに設定することで親に指定できます。
      マイケル
      マイケル
      また、VContainerの設定ファイルを作成して Root Lifetime Scope に指定することで、
      全てのLifetimeScopeの親に指定することもできます。
      ↑VContainerの設定ファイルを作成
      ↑RootとなるLifetimeScopeを定義
      エレキベア
      エレキベア
      階層構造を定義することで
      依存関係を更に整理することができるクマね

      Container API

      マイケル
      マイケル
      最後はCotaninerAPIの使い方についてです!
      IObjectResolver型で受け取ることでコンテナに直接アクセスすることができます。

      Container API | VContainer

      マイケル
      マイケル
      使い方は下記のようになります。
      コンテナ経由でInstantiateすることで動的に生成したオブジェクトにInjectしたり、サービスロケータ的な使い方をすることもできます。
      using UnityEngine;
      using VContainer;
      using VContainer.Unity;
      
      /// <summary>
      /// ゲーム管理クラス
      /// </summary>
      public class GameManager
      {
          private readonly IObjectResolver _objectResolver;
          private readonly MonsterBehaviour _monsterBehaviour;
          private readonly IHelloService _helloService;
          public GameManager(IObjectResolver objectResolver, MonsterBehaviour monsterBehaviour, IHelloService helloService)
          {
              _objectResolver = objectResolver;
              _monsterBehaviour = monsterBehaviour;
              _helloService = helloService;
          }
      
          public void OnStart()
          {
              // コンテナ経由でInstantiateする
              var monster = _objectResolver.Instantiate(_monsterBehaviour);
      
              // サービスロケータ的な使い方もできる
              var service = _objectResolver.Resolve<IHelloService>();
              var message = service.GetHelloMessage();
              Debug.Log(message);
          }
      }
      エレキベア
      エレキベア
      これで動的なオブジェクト生成も安心クマね

      おわりに

      マイケル
      マイケル
      というわけで今回はDIコンテナの概要とVContainerの使い方についてでした!
      どうだったかな??
      エレキベア
      エレキベア
      DIコンテナと聞くと難しそうだったクマが、
      シンプルで使いやすかったクマね
      マイケル
      マイケル
      使い方は簡単だけど、うやむやに依存関係を定義すると逆に複雑になってしまうから
      なるべくシンプルな依存関係になるよう整理しながら使用するように気をつけよう!
      マイケル
      マイケル
      それでは今日はこの辺で!
      アデューー!!
      エレキベア
      エレキベア
      クマ〜〜〜

      【Unity】DIコンテナの概要とVContainerの使い方について 〜完〜


      Unityソフトウェア設計VContainerDIコンテナ
      2022-09-14

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
      2024-04-14
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28
      【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜
      2024-03-28
      【Unity】サウンドミドルウェアに依存しない設計を考える【CRI ADX・Wwise】
      2024-03-27