はじめに
今回はUnityにおける「Model-View-(Reactive)Presenter
パターン」とは何なのかについて解説します。
対象読者
- Unity開発者
- UniRxを使うことができる
- UnityにおけるGUI周りの実装に困っている
GUI周りの設計パターン
Model-View-(Reactive)Presenter
パターン(略してMV(R)Pパターン)とは、UnityにおけるGUI周りの設計パターンの一種です。
「GUI」とはいわゆる「ユーザインターフェース」のことで、ゲーム中における「画面上に表示される情報」や「メニュー」や「ボタン」といったものを指します。
(ざっくりいえば、uGUI
のことだと思って下さい)
GUI周りの実装手法というものはUnityに限らず、複雑になりがちな難しい部分です。
そのためいろいろな設計パターンが考案されてきました。
代表的なもので言えばMVC
やMVVM
などがあげられます。
その中でもMVP
パターンというものがあり、Model-View-(Reactive)Presenter
パターンはこれをもとにした設計パターンです。
なぜ設計パターンが必要なのか
GUI周りの実装時にこのような複数の設計パターンがなぜ登場するかというと、GUI周りをキレイに実装することがムズカシイからです。
GUI周りはその用途から、非常に複雑になりがちです。
- 画面上のオブジェクトに表示するデータは、その裏側で相互に連動している
- 人間に対してリアルタイムに情報を表示する必要がある
- 人間からGUI経由でデータ処理に干渉されることがある
GUI周りは、ただでさえデータ構造が複雑になりがちな上に、リアルタイム性、インタラクション性が必要とされる場所なのです。
そのため適当に実装してしまうと「相互参照」「循環参照」「ビジーウェイト」「イベントの無限ループ」などが発生する恐れが非常に高い場所なのです。
このように複雑になりやすいGUI周りを、「責務を分離したオブジェクト同士の相互作用で構築すれば多少はマシにできるのではないか」という発想のもとで提唱されてきたものがMVC
やMVVM
やMVP
といった設計パターンなのです。
「やばい」実装例
設計パターンを用いずに、愚直にuGUI
を使うとこうなりますよという例です。
雑に作ると、次のような「ゲーム中で用いる重要な数値」と「uGUI
にアクセスする部分」が同じクラスの中にそのまま書かれてしまったり。
using UnityEngine;
using UnityEngine.UI;
namespace Yabai
{
/// <summary>
/// UIとデータとごっちゃになったクラス
/// </summary>
public class Data : MonoBehaviour
{
[SerializeField] private Slider _slider;
// 外から読み書きされたりする
public float CurrentValue { get; set; }
private void Update()
{
// Updateで無駄に値のチェックを毎フレームやってて最悪
if (_slider.value != CurrentValue)
{
_slider.value = CurrentValue;
}
}
// コード上では参照無いが、Unity上から呼び出されるpublicメソッド
public void OnSliderValueChanged()
{
CurrentValue = _slider.value;
}
}
}
Unity上でオブジェクトの相互参照が発生して、どちらかが欠けるとエラーが発生してゲームが動かなくなったりしています。
こういう状況になるのを避ける目的として、Unityでよく使われるのがModel-View-(Reactive)Presenter
パターンです。
ではModel-View-(Reactive)Presenter
パターンを話す前に、これのベースとなっている「Model-View-Presenter
パターン」を先に解説します。
Model-View-Presenter
パターン
Model-View-Presenter
(MVP)パターンとは、GUIの設計パターンのうち「Presenter
」という概念を用いたものです。
GUI周りの構成要素を次の3つに分けて考えます。
- Model - データの実体。GUIとは直接関係ないアプリケーション本体の要素部分。
- View - GUIを制御する部分。データを画面に表示したり、逆にユーザからの操作を受け付ける部分。
- Presenter - ModelとViewをつなげる存在。仲介役。
MVPパターンで重要な点は1つです。
「Presenterが存在しなければ、ViewとModelは完全に独立した状態になる」という点です。
View
とModel
をつなげる存在はPresenter
のみであるため、Presenter
を排除するとこの2つは完全に独立することになります。
つまり「ViewはModelを完全に知らない」「ModelはViewを完全に知らない」ということになります。
さて、このMVPパターンですが、実際にコードとして実装しようとすると問題がでてきます。
それは「ModelとViewをリアルタイムに連動させるにはどうしたらいいか」です。
「Modelの変化をViewにすぐに反映する」「ユーザからのView入力をModelへ即座に伝える」という、リアルタイムな動作を何らかの方法を用いて実現する必要があるわけです。
このように「リアルタイムな動作をするモノ」を別途用意する必要があるのがMVPパターンの欠点でした。
MVPからMV(R)Pへ
「リアルタイムな動作をするモノ」の用意が必須だったModel-View-Presenter
パターンに、UniRxを加えることでその欠点を補ったものが「Model-View-(Reactive)Presenter
パターン」です。
UniRxのもつリアクティブなオブジェクト、とくにReactiveProperty
の利便性に着目しMVPパターンへと応用したものが「MV(R)P
パターン」です。
Model-View-(Reactive)Presenterパターン解説
改めて「Model-View-(Reactive)Presenter
パターン(MV(R)P
パターン)」の解説をします。
概要
Model-View-(Reactive)Presenter
パターンとは、MVPパターンとUniRxを併用したUnityにおけるGUI周りの実装手法です。
GUIの構成要素をModel
、View
、Presenter
の3つに分解し、それらをUniRx(のReactiveProperty
)を用いて連携させる手法となっています。
(Model
の変化をPresenter
経由でView
に反映。View
の変化をPresenter
経由でModel
に反映。その橋渡しにUniRxを使う。)
MV(R)P
パターンが言っていること/言ってないこと
こういう設計パターンを用いる際に気をつけるべきこととして、「この設計パターンは何を語っているのか」です。
たまに深読して、本来の意味とは全く違うことを豪語する人が居たりするので注意が必要です。
MV(R)P
パターンが言っていること、言ってないことを次にまとめたのでこれらを念頭に入れた上で読んで下さい。
MV(R)P
パターンが言っていること
- GUI(とくに
uGUI
)周りの実装パターンである -
View
とModel
を「Presenter
」という薄いレイヤでつなごう - 各オブジェクトの連結には
Observable
(ReactiveProperty
)を活用しよう
MV(R)P
パターンが言ってないこと
- Unityの
GameObject
などはすべてViewである(←言ってない。Model
がGameObject
を使ってても別にいい) -
MV(R)P
パターンはUnity依存レイヤと非Unity依存レイヤを分離するものである(←言ってない) -
MV(R)P
パターンを使えばゲームを何でもキレイに実装できる(←言ってない。) - インゲームに
MV(R)P
パターンを使えば設計が上手にできる(←言ってない。クリーンアーキテクチャとかと話が混ざってない?)
MV(R)P
パターンは「GUI周りをこう作るといいよ」というシンプルなことしか語っていません。
それ以上のことは「深読みのしすぎ」であり、MV(R)Pパターンに過度の期待を持ちすぎです。
各種オブジェクトの役割
MV(R)P
パターンには次の3つのオブジェクトが登場します。
Model
View
Presenter
抑えておくとよいポイントとして、これらのオブジェクトは実装者の視点によって相対的に決定するものだということです。
つまり「これからModel
を作るぞ!」という役割を決めてからオブジェクトを作るのではなく、「このオブジェクトはこのGUI
にとってはModel
として振る舞うな」と決める形が正しいです。
要するに何がModelで何がViewなのかなどは厳密な定義があるのではなく、システム全体の流れをみて自分で決めてよいということです。
Modelとは
- GUIに対して表示するデータの実体を持つ部分
MV(R)P
パターンにおけるModel
とは、「データの実体をもつ部分」を指します。
言い換えると「GUIに表示するデータの実体をもつ」部分です。
アクションゲームを例に挙げると、「プレイヤの座標」「プレイヤの体力」「敵の体力」「現在のステージ情報」「残り制限時間」などが挙げられます。
もう少し踏み込んだ表現をするならば、Model
は「ゲームの構成に必要不可欠な情報(ドメイン)」を扱っているオブジェクトです。
そしてMV(R)P
パターンにおけるModel
で重要なのは、ReactiveProperty
を用いてPresenter
からのアクセスを可能にしているという点です。
ReactiveProperty
を公開することで、Model
の内部状態が変化したときにそれがObservable
として外部に通知できるようになっています。
Modelの実装方法
あるオブジェクトを、MV(R)P
パターンのModel
として扱うために必要な手順は次です。
- 状態を
IObservable<T>
またはReactiveProperty<T>
として公開する - (必要ならば)
Model
の内部状態を書き換えるプロパティやメソッドを用意する
要するにUniRxを使って状態を公開するだけです。
たとえば、次のPlayer
は「体力」をReactiveProperty
として公開しています。
(また同時に、Enemy
に衝突したら体力が減る)
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// アクションゲームにおけるプレイヤー
/// 「画面に体力を出す」という視点からみるとModel
/// </summary>
public sealed class Player : MonoBehaviour
{
/// <summary>
/// 体力
/// ReactivePropertyとして外部に状態をReadOnlyで公開
/// </summary>
public IReadOnlyReactiveProperty<int> Health => _health;
private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
/// <summary>
/// 衝突イベント
/// </summary>
private void OnCollisionEnter(Collision collision)
{
// Enemyに触れたら体力を減らす
if (collision.gameObject.TryGetComponent<Enemy>(out var _))
{
_health.Value -= 10;
}
}
private void OnDestroy()
{
_health.Dispose();
}
}
}
なお、生のReactiveProperty
をpublic
として公開し値のRead/Write
を直接可能にするか、Read
専用にIReadonlyReactiveProperty<T>
のみを公開しWrite
は別メソッドを経由させるかはどっちでも構いません。
このあたりはチームの設計ルールや個人の好みなどで決めて大丈夫です。
Viewとは
MV(R)P
パターンにおけるView
とは、「ユーザ(人間)に情報を提示したり、ユーザ入力を受け付ける部分」を指します。
もっと簡単にいえば、uGUIを使ってる部分だと思って下さい(もちろんuGUI
以外を使ってもOKです)
シンプルに、uGUI
の「Text」「Button」「Slider」コンポーネントをそのままView
として扱うこともあります。
またTween
を追加するなどUI要素がアニメーションする場合は、そのアニメーションを管理するコンポーネントもView
とみなして扱ってもOKです。
Viewの実装方法
あるオブジェクトをView
として扱えるようにするために必要なことは、「Presenter
から参照可能にする」だけでOKです。
uGUI
のコンポーネントを直接View
とみなすのであれば、Presenter
からuGUI
コンポーネントを直接参照させるだけです。
何か自前のコンポーネントをPresenter
に参照させるのであれば、Presenter
から見える位置にそのコンポーネントを配置する必要があります。
たとえば次のコンポーネントは、uGUI
のSlider
を制御するコンポーネントです。
このコンポーネントは紛れもなくView
の要素であり、Presenter
から参照しても問題ありません。
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// uGUIのSliderをアニメーションさせるコンポーネント(View)
/// </summary>
public sealed class AnimationSlider : MonoBehaviour
{
[SerializeField] private Slider _slider;
public void SetValue(float value)
{
// アニメーションしながらSliderを動かす
DOTween.To(() => _slider.value,
n => _slider.value = n,
value,
duration: 1.0f);
}
}
}
(スライダーをアニメーションしながら変化させる)
Presenterとは
MV(R)P
パターンにおけるPresenter
とは、Model
とView
の橋渡しをするオブジェクトです。
-
Model
->View
:Model
の内部状態の変化をReactiveProperty
やObservable
を通じて検知し、それをView
に反映する -
View
->Model
:View
の状態変化を検知して、それをModel
に反映する -
View
とModel
の間で必要なデータの変換(string
->int
などの型変換や、値の表現範囲の調整をしたり)
といった責務を持ちます。
(場合によってはModel
->View
の一方通行で終わる場合も多々あります)
Presenter
はデータ変換程度のロジックしか持たない、非常に薄いレイヤにするべきです。
Presenter
にはゲームロジックやViewの管理ロジックをもたせるべきではなく、基本的にデータの受け渡しと簡単なデータ変換のみを行うべきです。
Presenterの実装方法
Presenter
は紐付けたいModel
とView
の両方を参照する必要があります。
そのためこれら2つが参照できるレイヤやモジュールに定義し、それぞれにアクセスさせます。
実装としてはModel
のObservable
やReactiveProperty
をSubscribe()
し、その状態変化をView
に伝える。
View
の状態変化を同様にModel
に伝える。
このような処理を実装するだけです。
次の実装は、さきほどのPlayer
とAnimationSlider
の両者を結ぶPresenter
です。
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Presenters
{
/// <summary>
/// Playerの体力をViewに反映するPresenter
/// </summary>
public sealed class PlayerHealthPresenter : MonoBehaviour
{
// Model
[SerializeField] private Player _player;
// View
[SerializeField] private AnimationSlider _animationSlider;
private void Start()
{
// PlayerのHealthを監視
_player.Health
.Subscribe(x =>
{
// Viewに反映
_animationSlider.SetValue((float)x / _player.MaxHealth);
}).AddTo(this);
}
}
}
Presenter
を上手く作るコツとしては、「Presenter
に複雑なロジックを書かない」「Presenter
に状態を持たせない」です。
Presenter
はあくまでModel
とView
をつなぐだけの存在であり、Presenterの有無がゲームの動作に影響してはいけません。
(Presenter
を配置しないとModel
の挙動が変わったり、エラーを出して動作しないといった作りはよくないです)
「Presenter
とView
は最悪未実装でも、Model
さえあればゲーム自体はエラーを出さずに動作する(それで正しく遊べるとは言ってない)」と思って下さい。
サンプルの動作例
先程あげたPlayer
とAnimationSlider
の組み合わせですが、実際に動作させるとこのようになります。
(Player
(カプセル)にEnemy
(箱)が衝突するたびに体力が減り、それがSliderに反映される)
サンプルのシーン構成
Presenter
というGameObject
に、PlayerHealthPresenter
がアタッチされています。
これがUnityのヒエラルキーウィンドウ上で、Player
とAnimationSlider
を直接参照しています。
別例:Model->View / View->Model の相互のやり取りがある例
先程の例ではModel
->View
の一方通行でした。
こちらはView
->Model
の経路もあるパターンです。
(スライダの操作がModelに反映され、Modelの操作がスライダとTextに反映される)
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// データの実体を持つ
/// </summary>
public sealed class SampleModel : MonoBehaviour
{
// Count値
public IReadOnlyReactiveProperty<int> Count => _count;
// 最大値
public readonly int MaxCount = 100;
private readonly IntReactiveProperty _count = new IntReactiveProperty(0);
public void SetCount(int value)
{
// 数値の範囲を補正
value = Mathf.Clamp(value, 0, MaxCount);
_count.Value = value;
}
private void OnDestroy()
{
_count.Dispose();
}
}
}
using MVRP.Models;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Presenters
{
/// <summary>
/// Countを表示するPresenter
/// </summary>
public sealed class CountPresenter : MonoBehaviour
{
// Views
// uGUIコンポーネントをダイレクトに参照
[SerializeField] private Slider _slider;
[SerializeField] private Text _text;
// Model
[SerializeField] private SampleModel _model;
private void Start()
{
// Model -> View
_model.Count.Subscribe(v =>
{
// Slider
_slider.value = ((float)v / _model.MaxCount);
// Text
_text.text = $"{v}%";
})
.AddTo(this);
// View -> Model
_slider.OnValueChangedAsObservable()
.Subscribe(x =>
{
// Sliderは 0.0~1.0 なので補正
// こういうModel-View間での値の範囲補正も
// Presenterの責務
var value = (int)(100 * x);
// Modelに反映
_model.SetCount(value);
})
.AddTo(this);
}
}
}
MV(R)Pパターンのよくある質問とその答え
MV(R)P
パターンがわからない、という人から寄せられた質問とその解答をまとめてみました。
「MV(R)P
パターンって、MonoBehaviourは使っていいんですか」
MV(R)P
パターンは「MonoBehaviour
の有無について語っていません」。
MonoBehaviour
はすべてのオブジェクトで使ってもいいですし使わなくてもいいです。つまり、どっちでもいいです。
たまに「Model
はUnity非依存にするべき」とか語っている人がいますが、そんなことはないです。
そもそもMV(R)P
パターンは「GUIをキレイに実装すること」を目的にした設計パターンです。そのため「GUIさえキレイに実装できるなら細かい部分は割とどうでもいい」です。
たとえばアクションゲームにおいて、「ゲーム中に存在するキャラクタのステータスを画面に出す」といったパターンは頻出します。この場合はゲーム中のキャラクタもGameObject
の上に載せて動いている場合が多いでしょう。
このとき、MV(R)P
パターンが果たしたいことは「キャラクタのステータスをGUIに表示したい」です。
(プレイヤの体力バーを出す、とかよくやるけどこのプレイヤってGameObject
でしょ?)
もしここで「Model
にMonoBehaviour
を使ってはいけない」という縛りを追加してしまうと、「GUIに情報を出したいだけなのに、キャラクタをMonoBehaviour
非依存に根本から作りから直さないといけない」という状況になってしまいます。
これでは話が大きくなりすぎてしまい「GUI周りの扱いをキレイにしたい」という目的から外れていってしまいます。
何度も言います。MV(R)P
パターンの目的は「GUI周りをキレイに実装すること」が目的です。
MonoBehaviour
の有無や、何がUnityに依存してる/してないという議論はナンセンスです。
「各オブジェクトの初期化の方法がわからない」
Model
、View
、Presenter
、これらの初期化は頭を悩める問題です。
いろいろやり方はあるのですが、最終的に「 Presenter
が View
と Model
を参照している」という形が作れれば何でもよいです。
ぱっと思いつく限りで3パターンほどありますので、参考にしてください。
初期化方法A:インスペクターウィンドウ上で紐付けておく
Model
、View
、Presenter
のすべてが最初からシーンに配置されている場合に使えるパターンです。
動的に生成されたり削除されることが無いのであれば、Unityのインスペクターウィンドウ上で最初から紐付けておけばOKです。
(インスペクター上でPresenter
にあらかじめ紐付けておくのが一番かんたん)
初期化方法B:DIContainerを使う
方法Aとあまり変わらないですが、動的にオブジェクトが生成/削除されないのであればDIContainer(Zenject
やVContainer
)経由でオブジェクトをPresenter
に渡してしまうというやり方もあります。
初期化方法C:動的にバインドする
これが一番ややこしいパターンです。Model
があとから動的に追加されたり、削除されたりすることをどう扱うかです。
自分がよくやるのは別途Dispatcher
というオブジェクトを配置し、それがModel
の生成/削除をイベントを購読するというやり方です。
例として、次のようなものを考えてみます。
-
Player
に体力バーを表示する -
Player
は動的にあとから増える
このようなパターンを、Dispatcher
を使って実装してみます。
Model
(さっきのPlayerとおなじ)
using System;
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
/// <summary>
/// アクションゲームにおけるプレイヤー
/// 「画面に体力を出す」という視点からみるとModel
/// </summary>
public sealed class Player : MonoBehaviour
{
/// <summary>
/// 体力
/// ReactivePropertyとして外部に状態をReadOnlyで公開
/// </summary>
public IReadOnlyReactiveProperty<int> Health => _health;
// 体力の最大値
public readonly int MaxHealth = 100;
private readonly IntReactiveProperty _health = new IntReactiveProperty(100);
private void Start()
{
_health.Value = MaxHealth;
}
/// <summary>
/// 衝突イベント
/// </summary>
private void OnCollisionEnter(Collision collision)
{
// Enemyに触れたら体力を減らす
if (collision.gameObject.TryGetComponent<Enemy>(out var _))
{
_health.Value -= 10;
}
}
private void OnDestroy()
{
_health.Dispose();
}
}
}
Modelを動的に生成するManager
Player
を動的に生成するManager
がいて、こいつがPlayer
生成イベントを発行しているとします。
using System;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using Random = UnityEngine.Random;
namespace MVRP.Models
{
public sealed class PlayerManager : MonoBehaviour
{
/// <summary>
/// プレイヤ一覧
/// ReactiveCollectionのため、Player数が増えると通知される
/// </summary>
public IReactiveCollection<Player> Players => _players;
private readonly ReactiveCollection<Player> _players = new ReactiveCollection<Player>();
// PlayerのPrefab
[SerializeField] private Player _playerPrefab;
private async UniTaskVoid Start()
{
// 初期化処理
// Playerを時間差で4体作成
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(1));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(2));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(3));
await UniTask.Delay(TimeSpan.FromSeconds(1));
_players.Add(CreatePlayer(4));
}
private Player CreatePlayer(int id)
{
// Playerの生成
var player = Instantiate(_playerPrefab, Vector3.right * Random.Range(1f, 2f), Quaternion.identity);
player.name = $"Player{id}";
return player;
}
}
}
Presenter
Presenter
は引数で外からPlayer
とView
の組み合わせをうけて、バインドできるようしておきます。
今回は単一のPresenter
を使いまわして複数のPlayer
とView
を扱わせます。
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Presenters
{
// PlayerのPresenter
public sealed class PlayerPresenter : MonoBehaviour
{
// Playerが生成されたらBindする
public void OnCreatePlayer(Player player, PlayerView view)
{
view.SetName(player.name);
// PlayerのHealthを監視
player.Health
.Subscribe(x =>
{
// Viewに反映
view.SetHealth((float)x / player.MaxHealth);
}).AddTo(this);
}
}
}
View
今回はView
として「ネームプレート」と「体力バー」があったとします。
このUIを扱うCanvas
と、各要素を制御するコンポーネントを用意してPrefab
化しておきます。
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// Playerの状態を表示するView
/// </summary>
public sealed class PlayerView : MonoBehaviour
{
[SerializeField] private Text _nameplate;
[SerializeField] private Slider _healthSlider;
public void SetName(string name)
{
_nameplate.text = name;
}
public void SetHealth(float value)
{
// アニメーションしながらSliderを動かす
DOTween.To(() => _healthSlider.value,
n => _healthSlider.value = n,
value,
duration: 1.0f);
}
}
}
Dispatcher
PlayerManager
のイベントを検知して、View
を生成してPresenter
にバインドするDispatcher
を用意します。
今回はこのDispatcher
をシーンにあらかじめ配置していますが、DIContainerなどで扱ってもOKです。
using MVRP.Models;
using MVRP.Presenters;
using MVRP.Views;
using UniRx;
using UnityEngine;
namespace MVRP.Dispatchers
{
/// <summary>
/// Playerの生成を検知して、Viewを割り当てる
/// </summary>
public sealed class PlayerDispatcher : MonoBehaviour
{
// Modelを提供するManager
[SerializeField] private PlayerManager _playerManager;
// PlayerのPresenter
[SerializeField] private PlayerPresenter _presenter;
// ViewのPrefab
[SerializeField] private PlayerView _viewPrefab;
private void Start()
{
// 今リストにあるやつをDispatch
foreach (var p in _playerManager.Players)
{
Dispatch(p);
}
// 以降新規作成されたものをDispatch
_playerManager.Players.ObserveAdd().Subscribe(x => Dispatch(x.Value)).AddTo(this);
}
private void Dispatch(Player player)
{
// Playerの子要素としてViewを作成
var view = Instantiate(_viewPrefab, player.transform, true);
// 位置を調整
view.transform.localPosition = Vector3.up * 1.5f;
// Presenterに組み合わせて通知
_presenter.OnCreatePlayer(player, view);
}
}
}
(シーンにDispatcher
を配置して、PlayerManager
、Presenter
、ViewのPrefab
を参照させる)
今回はPresenter
をあらかじめシーンに配置していますが、Presenter
もまたprefab
にしてしまい都度生成する形にしても問題ありません(やりやすいようにやればOK)
動作の様子
(Player
が動的に生成されても体力バーがそれぞれに表示されており、それぞれの体力が反映されている)
「Presenterってどこにおけばいいんですか」
Presenter
をシーン上のどこに配置するのか。もしくはMonoBehaviour
を使わずにピュアクラスとしてDIContainer
に管理するべきなのか。という質問です。
答えは「自由にやってよい」です。
管理できてるならどこにPresenter
を配置してもよいです。uGUI
のCanvas
のGameObject
にPresenter
を貼り付けてもいいですし、空のGameObject
を作ってそこに貼り付けてもよいです。
自分が管理さえできているのであれば、どこにどう置いてもよいです。
「PresenterとViewの規模感がわからない」
これは「1つのPresenter
が複数のView
を管理していいのか」「View
の内部で状態を持っていてよいのか」といった質問と同じです。
Presenterの規模感
1つのPresenter
がどうView
を管理するかですが、これも答えは「自分が管理できるなら自由にやってよい」です。
- 1つの
GameObject
に1つのPresenter
だけ割り当てるやり方 - 1つの
GameObject
に複数のPresenter
を割り当てるやり方 - 1つの
Presenter
で複数のModel
やView
を割り当てるやり方
いくつかパターンは挙げられますが、どれも好きにやってOKです。
Viewの規模感
「View
の内部で状態を持ってよいか」「View
を管理するコンポーネントもView
として扱ってよいか」ですが、答えは「はい」です。
View
自身が状態をもち、View
が自分自身を管理するのはMV(R)P
パターン的には問題がありません。
たとえば、次のような「RGBの三色のスライダー」があったとします。
これには「3つのスライダーを管理するコンポーネント」がアタッチされているわけですが、コンポーネントはView
とみなして問題ないです。
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Views
{
/// <summary>
/// RGBのスライダーを管理するViewコンポーネント
/// </summary>
public sealed class ColorSlider : MonoBehaviour
{
/// <summary>
/// 今のスライダーが指し示す色
/// </summary>
public readonly ColorReactiveProperty Color
= new ColorReactiveProperty(UnityEngine.Color.white);
[SerializeField] private Slider _red;
[SerializeField] private Slider _green;
[SerializeField] private Slider _blue;
private void Start()
{
Color.Subscribe(c =>
{
_red.value = c.r;
_green.value = c.g;
_blue.value = c.b;
})
.AddTo(this);
// スライダーがどれか1つでも変化したら反映
Observable.Merge(
_red.OnValueChangedAsObservable(),
_green.OnValueChangedAsObservable(),
_blue.OnValueChangedAsObservable())
.Subscribe(_ =>
{
Color.Value = new Color(_red.value, _green.value, _blue.value);
})
.AddTo(this);
}
}
}
using MVRP.Models;
using MVRP.Views;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace MVRP.Presenters
{
// Presenter
public class ColorPresenter : MonoBehaviour
{
[SerializeField] private ColorModel _model;
[SerializeField] private ColorSlider _colorSlider;
[SerializeField] private Text _text;
private void Start()
{
// Model -> View
_model.Color.Subscribe(x =>
{
_text.text = x.ToString();
_text.color = x;
_colorSlider.Color.Value = x;
})
.AddTo(this);
// View -> Model
_colorSlider.Color.Subscribe(x =>
{
_model.Color.Value = x;
})
.AddTo(this);
}
}
}
using UniRx;
using UnityEngine;
namespace MVRP.Models
{
// Model
public class ColorModel : MonoBehaviour
{
public readonly ColorReactiveProperty Color
= new ColorReactiveProperty();
}
}
この値ってViewかModelのどちらがもつべき状態ですか
この質問は、たとえば「クリックされたら色の変わるボタンがあったときに、この"色"の情報はView
かModel
のどちらがもつのですか」みたいなやつです。
(クリックされたら色の変わるボタン。この「色」情報はModel
かView
のどっちがもつ?)
答えは「その色情報がゲームにおけるドメイン要素にあたるかどうか」で決まります。
(「ドメイン」とは「ゲームのコア要素」みたいな意味だと思って下さい)
その「色情報」がゲームの成立に必要不可欠な要素であるならば、それはModel
で管理するべき情報となります。
一方でゲームの成立に直接関係なく、あくまで演出の目的としてボタンの色を変えている程度であるならばそれはView
で管理するべき情報となります。
たとえば「3色リバーシ」などは色情報がゲームの成立に絶対必須な要素です。
そのため3色リバーシをMV(R)P
パターンで構築するのであれば、「色の情報はModel
が管理するべき」となります。
一方で「ちょっと見た目がリッチなトグルボタン」という扱いであるならば、これはただの演出でしかなく、最悪「ボタンの色の変化」無くてもゲームとして成立します。
こういった場合はView
で管理するべきとなります。
Model要素になるパターン
- その情報(データ)がゲームの成立に必要不可欠である
- ゲームの進行や状態管理に必須である
View要素になるパターン
- その情報(データ)が無くてもゲームとして成立する
- あると見た目がリッチになるが最悪無くてもなんとかなる、みたいなの
View->Presenter->Model->Presenter->View って値がループしたりしないんですか
-
View
の変化を検知してPresenter
がModel
に書き込む -
Model
の状態が変化する -
Model
の状態が変化したことを検知してPresenter
がView
に反映する -
View
の状態が変化する -
View
の変化を検知してPresenter
がModel
に書き込む - (以下無限ループ)
という現象が起きないかどうかという質問です。
答えは「ReactiveProperty
を使っている場合においてはループしません」。
ReactiveProperty
は「直前と同じ値が書き込まれた場合、イベントを発行しない」という仕組みになっています。そのためイベントが一巡はしますが、2周目のループには入らないようになっています。
ただし、ReactiveProperty.SetValueAndForceNotify()
を使っていた場合は強制的にメッセージ発行が実行されるため無限ループを引き起こす可能性があります。
Model/View/PresenterごとにAssembly Definition Filesを分けたほうがいいですか
答えは「ご自由にどうぞ」です。
厳密にやるために分けてもいいですし、面倒くさいから分けないというのも手です。
この辺は本当にご自由にどうぞ。
依存関係逆転則を使って、依存関係をView
->Presenter
->Model
にしたほうがいいですか
参照関係は「View
← Presenter
→ Model
」だが、モジュールとしての依存関係は「 View
→ Presenter
→ Model
」にしたほうがいいんですかという質問です。
意味がわからない人はこのスライドでちょっと説明してます。
結論は「冗長なのでやる必要はなし」だと自分は考えます。
厳密にやりたいならやってもいいですが、手間の割にリターンが見合わないと思います。
「MVPパターンを使うとUnity非依存でキレイに作れるって聞きました」
答えとしては「できなくはないが、それ以前に考えることがある」です。
MVP
パターン(MV(R)P
パターン)はもともと「GUI周り向け」の設計パターンです。
View
(GUI)をどう管理すればキレイに実装できるか、という話でした。
しかし、このView
という概念を拡大解釈し、「View
(Unity)とModel
(Unity非依存)を分けてキレイに作るための設計パターンだ」と勘違いしている人がたまにいます。
- 「
MVP
パターンを使えば、GameObject
を使わずにゲームロジックが組める」 - 「
MVP
パターンを使えば、アクションゲームをキレイに作れる」 - 「
MVP
パターンを使えば、設計がすべてキレイになる」
これらはすべてMVPパターンに過度の信頼をおきすぎです。
何回も言いますが、「MVP
パターンがいうView
とはGUIのこと」であり、GameObject
やMonoBehaviour
のことではありません。MVP
パターンは「GUIを実装するためのパターン」です。
「フレームワーク(Unity)」と「ドメイン(ゲームのコアロジック)」を分離した設計はたしかに理想ではあります。
ですがこれを実現するためには「MVP
パターン」だけでは手駒が足りず、他の設計パターンやアーキテクチャの知識も必要となってきます。(たとえば、巷で話題の「クリーンアーキテクチャ」は、まさに「複雑化するドメインからフレームワーク依存をどう分離するか」を語っているアーキテクチャとなっています。)
(参考: Unityにおける「設計レベル」を定義してみた)
ではMVP
パターンを拡大解釈して、「フレームワークとドメインの分離」に用いるのがダメなのかというと、別にそうではありません。シチュエーションによってはMVP
パターンが使える場面もあったりします。
たとえば、「サーバサイドにロジックとデータがある」「UnityはそのViewerとして使う」みたいにハッキリ区分ができている場合はMVP
パターンを使っても上手くいく可能性があります。
ですが一方で、「アクションゲーム」みたいな「Unityにべったり依存した仕組みの上で動くゲーム」などはそもそも「フレームワークとドメインの分離」自体が難しかったりします。こういったそもそも分離が難しい類のものを無理やり分離して作るのは非常に筋が悪く、上手くいかない可能性が高いです。
なので「フレームワークとドメインの分離」をやる場合は、まずそもそもそれをやる意義があるのかを考える必要があります。「フレームワークとドメインの分離」は確かに設計上はキレイになりそうですが、実際は実施コストが高く、また開発速度も遅くなるというデメリットがあります。
- 作ろうとしているゲームの規模感
- 開発者の人数
- スケジュール
- ゲームのジャンル
- システムのアーキテクチャ(クライアント完結なのか、サーバ連携するのかなど)
このあたりを判断材料として、「そもそもフレームワークとドメインを完全に分離して作ることに本当にメリットがあるのか」「やる場合はどういうアーキテクチャを採用するのか」を考える必要があります。
というわけで、答えは「(MVPパターンを使ってUnity非依存な実装は)できなくはないが、それ以前に考えることがある」です。
口酸っぱくいいますが、「MVP
パターンはもともとGUI設計のためのパターン」です。
これを応用して、「フレームワークとドメインの分離」に使ってみること自体は悪いことではありません。ですが、それをやるには設計についての他の知識や、それ相応の覚悟が必要となります。
そしてそれを初心者に求めるのは酷なので、MVPパターンは素直にGUI周りにだけ使っておいた方が安全という話になります。
まとめ
-
MV(R)P
パターンはUnityにおける「GUI周り」の設計パターン -
Model
とView
を、Presenter
という薄いレイヤでつなごう -
Model
とView
の連動には、UniRxを活用しよう(ReactiveProperty
が便利) - 上記以上のことについては、割と自由にやってよい
- 絶対の正解は無いので、やりやすいように解釈して使ってOK
- ただし「あくまでGUI周り用の設計パターンである」ということは念頭におくこと
(追記)
あとで「スマホ画面から移動操作などのInputを受け付けるときの考え方」みたいなのを追記します
機会があれば書くかも…。