はじめに
こんにちは。今回はUnityにおける設計パターンの「MVPパターン」を実装してアウトプットという形で記事にしてみたいと思います。
この記事ではMVPの説明はメモ程度にとどめておきます。詳しいことを知りたい人は、参考にした記事を以下においておくので、そちらを参照ください。
ざっくりMVPとは
このパターンを用いることで、Unity内で複雑になりがちなGUI周りの処理が綺麗に書けるようになります。「Model」「View」「Presenter」の頭文字をとってMVPモデルです。今回はオブザーバーにUniRxの「ReactiveProperty」を使用するので、MV(R)Pとも表現できますね。$\tiny{最近だとUniRxの代わりにR3というライブラリが使われることもあるそうで。}$
さておき、それぞれの役割は以下の通りです。
- Model
GUIに表示させるデータを保持する部分。例えばint型の残り時間であるとか。
PresenterのこともViewのことも知らない。独立している。
- View
GUI。UnityだとButtonとかSliderとかTextMeshProとか。
PresenterのこともModelのことも知らない。独立している。
- Presenter
ModelとViewの橋渡し役。
Modelの情報をViewに反映させるし、Viewで変更が行われたらModelに反映させる。
MVPパターンの実装
BGM音量設定のUI部分をMVPで実装していこうと思います。
Unity内のオブジェクト
Unity内で適当にUI部分とその他を配置しました。ヒエラルキーはこんな感じです。
CanvasにViewスクリプトをアタッチし、空っぽのゲームオブジェクトScriptにPresenterスクリプトをアタッチしています。ModelはMonobihaviourを継承しないピュアクラスですので、Unityエディタ上ではインスタンス化しません。
画面内のUIはこんな感じです。
画像内の+ボタンを押したら音量が上がり、数字が増える。-ボタンを押したら音量が下がり、数字が減る。この簡単な処理をMVPパターンで実装していきます。
スクリプト
Model
Modelサンプルコード
using UniRx;
namespace MVP_pettern
{
public class Model
{
const int MAX_SPEED = 15;
const int MIN_SPEED = 0;
private ReactiveProperty<int> _speed;
public IReadOnlyReactiveProperty<int> Speed
{
get { return _speed; }
}
//コンストラクタ
public Model()
{
_speed = new ReactiveProperty<int>(0);
}
//スピードのセット
public void SetSpeed(int value)
{
value = Mathf.Clamp(value, MIN_SPEED, MAX_SPEED);
_speed.Value = value;
}
}
}
スピードはプロパティでgetだけできるようにしています。setはメソッドを通しています。ここはお好みでプロパティからそのままsetできるようにしてもいいと思います。
そして、getで公開しているIReadOnlyReactivePropertyは、interfaceの継承を追っていけば分かると思いますが、Subscribeが出来るようになっています。publicとして他クラスに公開することで、Presenterから購読と読み取りだけが出来るようになるってわけですね。
View
Viewサンプルコード
using UnityEngine;
using System;
using TMPro;
namespace MVP_pettern
{
public class View : MonoBehaviour
{
//テキスト
[SerializeField] TextMeshProUGUI _speedText;
//「+」ボタンのイベントリスナー
public Action OnPlusButtonClickedListener;
//「-」ボタンのイベントリスナー
public Action OnMinusButtonClickedListener;
//スピードが変わったときのメソッド
public void OnSpeedChanged(int speed)
{
_speedText.text = speed.ToString();
}
//プラスボタンがクリックされたとき
public void OnPlusButtonClicked()
{
if (OnPlusButtonClickedListener != null) { OnPlusButtonClickedListener(); }
}
//マイナスボタンがクリックされたとき
public void OnMinusButtonClicked()
{
if (OnMinusButtonClickedListener != null) { OnMinusButtonClickedListener(); }
}
}
}
それぞれのボタンが押されたときのActionをpublicで宣言しています。
このActionに、Presenterがボタンを押されたときの挙動(メソッド)を購読します。
Presenter
Presenterサンプルコード
using UnityEngine;
using UniRx;
namespace MVP_pettern
{
public class Presenter : MonoBehaviour
{
[SerializeField] View _view;
private Model _model;
private void Start()
{
_model = new Model();
SetEvents();
Bind();
}
//Viewにボタンが押されたときのメソッドを購読
private void SetEvents()
{
_view.OnPlusButtonClickedListener += OnPlusButtonClicked;
_view.OnMinusButtonClickedListener += OnMinusButtonClicked;
}
//Modelにスピードが変わったときのメソッドを購読
private void Bind()
{
_model.Speed
.Subscribe(_view.OnSpeedChanged)
.AddTo(this.gameObject);
}
private void OnPlusButtonClicked()
{
_model.SetSpeed(_model.Speed.Value + 1);
}
private void OnMinusButtonClicked()
{
_model.SetSpeed(_model.Speed.Value - 1);
}
}
}
Modelが公開しているIReadOnlyReactivePropertyにスピードが変わったときのViewのメソッドを購読します。
また、Viewが公開しているActionに、それぞれModelのスピードを変えるメソッドを購読しています。
MVPパターンを用いることで、ModelとViewが独立した状態を実装することが出来ました。ちょっと複雑な構造ですが、ゲーム内のGUI周りの処理がごちゃごちゃするのをある程度防いでくれます。便利ですね。