LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

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

PONOS Advent Calendar 2020 24日目の記事です。
昨日は@ackylaさんGoogleCloudShellのteachmeコマンドが便利でした。

はじめに

VContainerについての記事は以前に書いているので、そもそもVContainerってなんだろうと思った方はこちらを読んでみてください。VContainerとはどういうものなのかについて触れています。

サンプルについて

j4qf0-vk3x1.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同様にコンストラクタは依存解決時に自動的に呼び出されてIViewIModelが引数に渡されます。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で自動的に行われる処理と同じようなタイミングで走る処理の総称と思って間違いなと思っています。
詳しくはこちらをご確認いただければと思います。

まとめ

前回も書きましたがZenjectとコードの書き方が似ているので移行するにしてもそこまで難しくないかなと考えています。
まだまだプロジェクトへの導入はありませんがこれから増えていって欲しいなと思います。そしてどんどん記事も増えて情報も増えていって欲しいですね。
最後に公式のサイトがオープンしたらしいのでこちらで色々と使い方をみてみてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2