VContainerを組み込んだゲームサンプル

PONOS Advent Calendar 2020 24日目の記事です。
昨日は[@ackylaさん](https://qiita.com/ackyla)の[GoogleCloudShellのteachmeコマンドが便利](https://qiita.com/ackyla/private/6505cb13a4990d8b2599)でした。

## はじめに
VContainerについての記事は以前に書いているので、そもそもVContainerってなんだろうと思った方は[こちら](https://qiita.com/kenta-san/items/59bc9d9300b4d1b7af9a)を読んでみてください。VContainerとはどういうものなのかについて触れています。

## サンプルについて
![j4qf0-vk3x1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/914167/86729583-2bf4-5812-aaec-9fb9a3372948.gif)
今回はこのように表示されている文字を小文字から大文字へと変換するサンプルになっています。`MonoBehaviour`を継承した一つのクラスでも問題はないのですが、サンプルとしての有用性を高めるためにMVRPとして作成しています。またイベントのやりとりについては`UniRx`を使用しています。

## コード
# GameSampleLifetimeScope
```GameSampleLifetimeScope.cs
using VContainer;
using VContainer.Unity;
using UnityEngine;

namespace GameSample
{
    public class GameSampleLifetimeScope : LifetimeScope
    {
        [SerializeField] View view;
        [SerializeField] MessageData data;

        protected override void Configure(IContainerBuilder builder)
        {
            builder.RegisterComponent<IView>(view);
            builder.Register<ToUpperModel>(Lifetime.Scoped).WithParameter<MessageData>(data).As<IModel>();
            builder.RegisterEntryPoint<Presenter>(Lifetime.Scoped);
        }
    }
}
```
`LifetimeScope`はこのようになっています。
ViewとModelに関してはインターフェースとして登録しています。
また、Presenterについては`EntryPoint`に登録をしています。

Modelについては`WithParameter`を使用して`LifetimeScope`に登録されている`MessageData`をコンストラクタの時に渡してあげるようにしています。

# MVP
```ToUpperModel.cs
using UniRx;

namespace GameSample
{
    public class ToUpperModel : IModel
    {
        ReactiveProperty<string> message = new ReactiveProperty<string>();
        public ReadOnlyReactiveProperty<string> Message => message.ToReadOnlyReactiveProperty();

        public ToUpperModel(MessageData msg)
        {
            message.Value = msg.Message;
        }

        public void Modify(string msg)
        {
            message.Value = msg.ToUpper();
        }
    }
}
```

```View.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using UniRx;

namespace GameSample
{
    public class View : MonoBehaviour, IView
    {
        [SerializeField] Text text;
        [SerializeField] Button modify;

        Subject<string> modifySubject = new Subject<string>();
        public IObservable<string> OnClickModify => modifySubject;

        private void Start()
        {
            modify.OnClickAsObservable().Subscribe(_ => modifySubject.OnNext(text.text)).AddTo(this);
        }

        public void RefreshMessage(string msg)
        {
            text.text = msg;
        }
    }
}
```

```Presenter.cs
using VContainer.Unity;
using System;
using UniRx;

namespace GameSample
{
    public interface IView
    {
        IObservable<string> OnClickModify { get; }
        void RefreshMessage(string msg);
    }

    public interface IModel
    {
        ReadOnlyReactiveProperty<string> Message { get; }
        void Modify(string msg);
    }

    public class Presenter: IDisposable, IInitializable
    {
        CompositeDisposable disposables;

        readonly IView view;
        readonly IModel model;

        public Presenter(IView view, IModel model)
        {
            this.view = view;
            this.model = model;
            disposables = new CompositeDisposable();
        }

        public void Initialize()
        {
            view.OnClickModify.Subscribe(model.Modify).AddTo(disposables);
            model.Message.Subscribe(view.RefreshMessage).AddTo(disposables);
        }

        public void Dispose()
        {
            disposables.Dispose();
        }
    }
}
```

Model、View、Presenterについてはこのようになっています。
Modelのコンストラクタに`MessageData`が引数で渡されていますが、こちらは先ほど述べたように`LifetimeScope`にて登録されていたデータが渡されます。このコンストラクタは依存解決時に自動的に呼び出されます。

今回の肝になっているのはPresenterになっています。
PresenterもModel同様にコンストラクタは依存解決時に自動的に呼び出されて`IView`と`IModel`が引数に渡されます。Presenterが呼び出されている箇所はもちろんMVRPなのでありませんが、Presenterに`IInitializable`を設定し、`EntryPoint`に登録することによって自動的に呼び出され、その時に依存解決されるのです。これにより紐づる式にModelも生成されます。
また`IDisposable`も継承していますが、こちらもインスタンス破棄のタイミングで自動的に呼ばれる仕組みになっています。
DIの使い方としてインターフェースを渡すことにより、修正コストを少なくして別の処理に置き換えることができます。
ToUpperModelをToLowerModelという小文字に変換するModelへと置き換えた場合の修正コストは以下になります。

# ToLowerModel
```ToLowerModel.cs
using UniRx;

namespace GameSample
{
    public class ToLowerModel : IModel
    {
        ReactiveProperty<string> message = new ReactiveProperty<string>();
        public ReadOnlyReactiveProperty<string> Message => message.ToReadOnlyReactiveProperty();

        public ToLowerModel(MessageData msg)
        {
            message.Value = msg.Message;
        }

        public void Modify(string msg)
        {
            message.Value = msg.ToLower();
        }
    }
}
```
# 既存コードの修正箇所
```GameSampleLifetimeScope.cs(修正箇所のみ)
builder.Register<ToLowerModel>(Lifetime.Scoped).WithParameter<MessageData>(data).As<IModel>();
```
デバッグ用の処理と、本番用の処理の切替が楽にすみますね！
DIの利点はこの修正コストの少なさだと私は思っています。

# MessageData
```MessageData.cs
using UnityEngine;

[CreateAssetMenu(menuName = "Create/Create Message")]
public class MessageData : ScriptableObject
{
    public string Message;
}
```
渡している`MessageData`の中身はこのようになっています。

## EntryPoint
先ほどのPresenterの登録について`EntryPoint`で行いました。
`EntryPoint`とは`PlayerLoop`および`MonoBehaviour`で自動的に行われる処理と同じようなタイミングで走る処理の総称と思って間違いなと思っています。
詳しくは[こちら](https://vcontainer.hadashikick.jp/integration/dispatching-unity-lifecycle-event)をご確認いただければと思います。
## まとめ
前回も書きましたがZenjectとコードの書き方が似ているので移行するにしてもそこまで難しくないかなと考えています。
まだまだプロジェクトへの導入はありませんがこれから増えていって欲しいなと思います。そしてどんどん記事も増えて情報も増えていって欲しいですね。
最後に[公式のサイト](https://vcontainer.hadashikick.jp/)がオープンしたらしいのでこちらで色々と使い方をみてみてください。