PONOS Advent Calendar 2020 24日目の記事です。
昨日は@ackylaさんのGoogleCloudShellのteachmeコマンドが便利でした。
はじめに
VContainerについての記事は以前に書いているので、そもそもVContainerってなんだろうと思った方はこちらを読んでみてください。VContainerとはどういうものなのかについて触れています。
サンプルについて
今回はこのように表示されている文字を小文字から大文字へと変換するサンプルになっています。MonoBehaviour
を継承した一つのクラスでも問題はないのですが、サンプルとしての有用性を高めるためにMVRPとして作成しています。またイベントのやりとりについてはUniRx
を使用しています。
コード
GameSampleLifetimeScope
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
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();
}
}
}
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;
}
}
}
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
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();
}
}
}
既存コードの修正箇所
builder.Register<ToLowerModel>(Lifetime.Scoped).WithParameter<MessageData>(data).As<IModel>();
デバッグ用の処理と、本番用の処理の切替が楽にすみますね!
DIの利点はこの修正コストの少なさだと私は思っています。
MessageData
using UnityEngine;
[CreateAssetMenu(menuName = "Create/Create Message")]
public class MessageData : ScriptableObject
{
public string Message;
}
渡しているMessageData
の中身はこのようになっています。
EntryPoint
先ほどのPresenterの登録についてEntryPoint
で行いました。
EntryPoint
とはPlayerLoop
およびMonoBehaviour
で自動的に行われる処理と同じようなタイミングで走る処理の総称と思って間違いなと思っています。
詳しくはこちらをご確認いただければと思います。
まとめ
前回も書きましたがZenjectとコードの書き方が似ているので移行するにしてもそこまで難しくないかなと考えています。
まだまだプロジェクトへの導入はありませんがこれから増えていって欲しいなと思います。そしてどんどん記事も増えて情報も増えていって欲しいですね。
最後に公式のサイトがオープンしたらしいのでこちらで色々と使い方をみてみてください。