VContainerを触ってみた。その1,HelloWorld (Unity初心者)

 はじめに

最近ゲーム作りにおけるデザインパターンなるものを気にしながらゲームプログラムを書いている。その延長上でDependency Injection(DI)なるデザインパターンがあり、Unity上で動作するDIフレームワークのVContainerを使ってみる。



Dependency Injection(DI)とは

直訳すると依存性注入。
DIとは、プログラミングにおけるデザインパターン(設計思想)の一種で、オブジェクトを成立させるために必要となるコードを実行時に注入(Inject)してゆくという概念のことである。

参考:https://www.weblio.jp/content/Dependency+Injection 



なぜVContainerなのか

もう一つUnityのDIフレームワークで有名なZenjectというものがあり、最初そちらを触ってみたのだが、どうやら調べてみるとVContainerの方がパフォーマンスがいいらしい。

ぶっちゃけ初心者にパフォーマンスの違いとかわからんが、まぁいいと言われているものを使った方がいいだろうということで。

あと、日本語のリファレンスがあって、何か詰まったら調べやすい気がした。

VContainer日本語リファレンス


導入

新しいプロジェクトを作成するか、既存の導入したいプロジェクトを開く。

Packages/manifest.jsonファイルを開く

"dependencies": { }内に以下のコードを追記(古くなっている可能性があるので、最新版を上のドキュメントURLからコピーした方がいいです)


"jp.hadashikick.vcontainer":"https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.15.3"

これでインポートされるはず。




チュートリアル

公式リファレンスのHello Worldをやっていく。

(以下はリファレンス通りにやっているだけなんで飛ばしてOK)

自分なりに噛み砕きながら進めていきます。


①サービスクラスの作成


    public class HelloWorldService
    {
        public void Hello()
        {
            UnityEngine.Debug.Log("Hello world");
        }
    }

デバッグログで"Hello world"と出力するだけのクラス(MVPパターンにするならModelにあたるのかな?)


②DIコンテナに登録

プロジェクトでC#スクリプトを作成して名前をGameLifetimeScopeにする。(このとき、名前はなんでもいいけど、語尾をLifetimeScopeにすると自動的にLifetimeScopeを継承したクラスのテンプレートを作成してくれる)

生成されたテンプレートのConfigureメソッドをOverrideしてここで先ほどのHelloWorldServiceを登録する。


using VContainer;
using VContainer.Unity;

    public class GameLifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
           builder.Register<HelloWorldService>(Lifetime.Singleton);
        }
    }

Configure関数内でクラスを登録します。今回はHelloWorldServiceを登録。

引数のLifetimeはとりあえずSingletonでOK

Lifetimeについて詳しくはhttps://qiita.com/sakano/items/3a009019e279024fda19#lifetime%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6


③GameObjectを作成して②で作ったLifetimeScopeをアタッチ

アタッチされたLifetimeScopeのコンポーネントのインスペクターは一旦なにもいじらないで大丈夫。


④①で作ったHelloWorldServiceクラスを呼び出す。

GamePresenterクラスを作成。


    public class GamePresenter
    {
        readonly HelloWorldService helloWorldService;

        public GamePresenter(HelloWorldService helloWorldService)
        {
            this.helloWorldService = helloWorldService;
        }
    }

HelloWorldServiceクラスの参照を持つ。

コンストラクトで参照を受け取る。

DIコンテナに登録するために以下のコードをGameLifetimeScopeに追加。


builder.Register<HelloWorldService>(Lifetime.Singleton);
builder.Register<GamePresenter>(Lifetime.Singleton);//追記

これでGamePresenterクラスが持つHelloWorldServiceクラスにちゃんと注入されているはず。


⑤UnityのPlayerLoopSystem(Start()とかUpdate())で処理を実行する。

UnityのMonoBehaviorを継承したクラスは基本的にStart()やUpdate()で処理を書いたりするけど、そういったライフサイクルイベントで処理が行われるようにする。

VContainerでEntryPointと呼ぶらしい。

先ほど作ったGamePresenterクラスにライフサイクルイベントを持ったインターフェイスを継承させる(今回はITickableインターフェイス(Update()と同じイベント持つ))


    public class GamePresenter : ITickable //ITickable追加
     {
        readonly HelloWorldService helloWorldService;

        public GamePresenter(HelloWorldService helloWorldService)
        {
             this.helloWorldService = helloWorldService;
        }

        void ITickable.Tick() //ITickableのメソッド(これがUpdate()の代わり)
        {
            helloWorldService.Hello(); //HelloWorldServiceクラスのHello()を実行
        }
     }

※どうしてもUpdate()で処理させる必要があるならMonoBehaviorでいんじゃね?と思ったけど、それだとMonoBehaviorを継承してコンストラクタを作成できないし、余計な依存関係を作ってしまってせっかくVContainerを使っているのに本末転倒だし、せっかくインスペクターにオブジェクトを持ってやらなくてもピュアC#で書けていいよね。今まではMVPパターンで何かを作ったときにPresenterはGameObjectにアタッチして使ってViewとかを参照を持たせてたけど、このやり方だとわざわざそうしなくてもいいのがいいなと思った。

GameLifetimeScopeクラスでDIコンテナへの登録方法を変える

Untiyのライフサイクルイベントで処理を実行するには登録する際に以下のように登録方法を変える。


builder.RegisterEntryPoint<GamePresenter>(); //Register<GamePresenter>()から変更

これで実行してみるとUpdateで"Hello world"のログが出てくる。


⑥ボタンを作成して押した時に処理を実行(Viewとの分離)

以下のようなViewのクラス(HelloScreenクラス)を作成。

コード

まぁ何も考えず処理を直書きするならこのクラスでHelloWorldServiceクラスを作成させて処理を実行したり、違う処理をAddListenerで入れたりできるが、そうしてしまうとViewであるHelloScreenクラスが処理の実行にも影響されてしまうため、何か別の処理を追記したり、変更したり、条件によって処理を変えたいことが出てきたりするとどんどんViewの負担が大きくなっていわゆる神クラス的になってしまうので、Viewはボタンコンポーネントや必要なUIコンポーネントだけを持った方がいいよね。

GamePresenterクラスにViewの参照を持たせる。そこにGameLifetimeScopeクラスでViewの参照を注入する。


    public class GamePresenter : ITickable, IStartable //IStartableを追記
    {
        readonly HelloWorldService helloWorldService;
        readonly HelloScreen helloScreen; //注入してもらうView

        public GamePresenter(
            HelloWorldService helloWorldService,
            HelloScreen helloScreen)
        {
            this.helloWorldService = helloWorldService;
            this.helloScreen = helloScreen;
        }

        void IStartable.Start() //Unityライフサイクルイベントで言うところのStart()
        {
            helloScreen.HelloButton.onClick.AddListener(() => helloWorldService.Hello());
        }
     }    

IStartableはUnityのライフサイクルイベントのStart()関数と同じライフサイクルイベントを実行する。

GamePresenterでViewのボタンに注入されたModel(HelloWorldService)の処理をAddListenerで入れている。


   public class GameLifetimeScope : LifetimeScope
   {
       [SerializeField]
       HelloScreen helloScreen; //Viewはインスペクターでアタッチ

       protected override void Configure(IContainerBuilder builder)
       {
           builder.RegisterEntryPoint<GamePresenter>();
           builder.Register<HelloWorldService>(Lifetime.Singleton);
           builder.RegisterComponent(helloScreen); //追記。インスペクターで持っているやつはRegisterComponent()で登録できる
       }
   }

GameLifetimeScopeはシーン上のGameObjectにアタッチしているので、[SerializeField]で参照を取得できる。View(HelloScreen)もボタンなどの持つ必要があるのでシーン内にGameObjectにアタッチする必要がある。このViewをGameLifetimeScopeにアタッチする。

これでボタンを押すと"Hello world"とログが出力されるはず。


以上。

なんとなくはVContainerの感じは掴んだので、自分なりに独自のクラスを作りながらもっと試したいと思う。

長くなったので、自分なり試した処理は別で書きます。


次回以降

VContainerを触ってみた。その2,MVPパターン (Unity初心者)

VContainerを触ってみた。その3,注入するInputの変更 (Unity初心者)



参考にさせていただいた記事等🙏

公式リファレンス


VContainer入門(1) - IContainerBuilderとIObjectResolver


【C#】Dependency Injection(依存性の注入)とは






コメント