LoginSignup
308
241

【Unity】Model-View-(Reactive)Presenterパターンとは何なのか

Last updated at Posted at 2022-03-01

はじめに

今回はUnityにおける「Model-View-(Reactive)Presenterパターン」とは何なのかについて解説します。

対象読者

  • Unity開発者
  • UniRxを使うことができる
  • UnityにおけるGUI周りの実装に困っている

GUI周りの設計パターン

Model-View-(Reactive)Presenterパターン(略してMV(R)Pパターン)とは、UnityにおけるGUI周りの設計パターンの一種です。
「GUI」とはいわゆる「ユーザインターフェース」のことで、ゲーム中における「画面上に表示される情報」や「メニュー」や「ボタン」といったものを指します。
(ざっくりいえば、uGUIのことだと思って下さい)

GUI周りの実装手法というものはUnityに限らず、複雑になりがちな難しい部分です。
そのためいろいろな設計パターンが考案されてきました。
代表的なもので言えばMVCMVVMなどがあげられます。

その中でもMVPパターンというものがあり、Model-View-(Reactive)Presenterパターンはこれをもとにした設計パターンです。

なぜ設計パターンが必要なのか

GUI周りの実装時にこのような複数の設計パターンがなぜ登場するかというと、GUI周りをキレイに実装することがムズカシイからです。
GUI周りはその用途から、非常に複雑になりがちです。

  • 画面上のオブジェクトに表示するデータは、その裏側で相互に連動している
  • 人間に対してリアルタイムに情報を表示する必要がある
  • 人間からGUI経由でデータ処理に干渉されることがある

GUI.jpg

GUI周りは、ただでさえデータ構造が複雑になりがちな上に、リアルタイム性、インタラクション性が必要とされる場所なのです。
そのため適当に実装してしまうと「相互参照」「循環参照」「ビジーウェイト」「イベントの無限ループ」などが発生する恐れが非常に高い場所なのです。

このように複雑になりやすいGUI周りを、「責務を分離したオブジェクト同士の相互作用で構築すれば多少はマシにできるのではないか」という発想のもとで提唱されてきたものがMVCMVVMMVPといった設計パターンなのです。

「やばい」実装例

設計パターンを用いずに、愚直に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;
        }
    }
}

Yabai.jpg

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は完全に独立した状態になる」という点です。

ViewModelをつなげる存在は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の構成要素をModelViewPresenterの3つに分解し、それらをUniRx(のReactiveProperty)を用いて連携させる手法となっています。

MVPPattern.jpg

(Modelの変化をPresenter経由でViewに反映。Viewの変化をPresenter経由でModelに反映。その橋渡しにUniRxを使う。)

MV(R)Pパターンが言っていること/言ってないこと

こういう設計パターンを用いる際に気をつけるべきこととして、「この設計パターンは何を語っているのか」です。
たまに深読して、本来の意味とは全く違うことを豪語する人が居たりするので注意が必要です。

MV(R)Pパターンが言っていること、言ってないことを次にまとめたのでこれらを念頭に入れた上で読んで下さい。


MV(R)Pパターンが言っていること

  • GUI(とくにuGUI)周りの実装パターンである
  • ViewModelを「Presenter」という薄いレイヤでつなごう
  • 各オブジェクトの連結にはObservable(ReactiveProperty)を活用しよう

MV(R)Pパターンが言ってないこと

  • UnityのGameObjectなどはすべてViewである(←言ってない。ModelGameObjectを使ってても別にいい)
  • 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();
        }
    }
}

なお、生のReactivePropertypublicとして公開し値の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から見える位置にそのコンポーネントを配置する必要があります。

たとえば次のコンポーネントは、uGUISliderを制御するコンポーネントです。
このコンポーネントは紛れもなく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とは、ModelViewの橋渡しをするオブジェクトです。

  • Model -> ViewModelの内部状態の変化をReactivePropertyObservableを通じて検知し、それをViewに反映する
  • View -> ModelViewの状態変化を検知して、それをModelに反映する
  • ViewModelの間で必要なデータの変換(string->intなどの型変換や、値の表現範囲の調整をしたり)

といった責務を持ちます。
(場合によってはModel->Viewの一方通行で終わる場合も多々あります)

Presenterはデータ変換程度のロジックしか持たない、非常に薄いレイヤにするべきです。
PresenterにはゲームロジックやViewの管理ロジックをもたせるべきではなく、基本的にデータの受け渡しと簡単なデータ変換のみを行うべきです。

Presenterの実装方法

Presenterは紐付けたいModelViewの両方を参照する必要があります。
そのためこれら2つが参照できるレイヤやモジュールに定義し、それぞれにアクセスさせます。

実装としてはModelObservableReactivePropertySubscribe()し、その状態変化をViewに伝える。
Viewの状態変化を同様にModelに伝える。
このような処理を実装するだけです。

次の実装は、さきほどのPlayerAnimationSliderの両者を結ぶ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はあくまでModelViewをつなぐだけの存在であり、Presenterの有無がゲームの動作に影響してはいけません。
Presenterを配置しないとModelの挙動が変わったり、エラーを出して動作しないといった作りはよくないです)

PresenterViewは最悪未実装でも、Modelさえあればゲーム自体はエラーを出さずに動作する(それで正しく遊べるとは言ってない)」と思って下さい。

サンプルの動作例

先程あげたPlayerAnimationSliderの組み合わせですが、実際に動作させるとこのようになります。

MVRP_Sample.gif
Player(カプセル)にEnemy(箱)が衝突するたびに体力が減り、それがSliderに反映される)

サンプルのシーン構成

PresenterUnity.png

PresenterというGameObjectに、PlayerHealthPresenterがアタッチされています。
これがUnityのヒエラルキーウィンドウ上で、PlayerAnimationSliderを直接参照しています。

別例:Model->View / View->Model の相互のやり取りがある例

先程の例ではModel->Viewの一方通行でした。
こちらはView->Modelの経路もあるパターンです。

MVRP_Slider.gif

(スライダの操作が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に表示したい」です。

Reimu.png

(プレイヤの体力バーを出す、とかよくやるけどこのプレイヤってGameObjectでしょ?)

もしここで「ModelMonoBehaviourを使ってはいけない」という縛りを追加してしまうと、「GUIに情報を出したいだけなのに、キャラクタをMonoBehaviour非依存に根本から作りから直さないといけない」という状況になってしまいます。
これでは話が大きくなりすぎてしまい「GUI周りの扱いをキレイにしたい」という目的から外れていってしまいます。

何度も言います。MV(R)Pパターンの目的は「GUI周りをキレイに実装すること」が目的です。
MonoBehaviourの有無や、何がUnityに依存してる/してないという議論はナンセンスです。

「各オブジェクトの初期化の方法がわからない」

ModelViewPresenter、これらの初期化は頭を悩める問題です。

いろいろやり方はあるのですが、最終的に「 PresenterViewModelを参照している」という形が作れれば何でもよいです。

ぱっと思いつく限りで3パターンほどありますので、参考にしてください。

初期化方法A:インスペクターウィンドウ上で紐付けておく

ModelViewPresenterのすべてが最初からシーンに配置されている場合に使えるパターンです。
動的に生成されたり削除されることが無いのであれば、Unityのインスペクターウィンドウ上で最初から紐付けておけばOKです。

PresenterInitialize.png
(インスペクター上でPresenterにあらかじめ紐付けておくのが一番かんたん)

初期化方法B:DIContainerを使う

方法Aとあまり変わらないですが、動的にオブジェクトが生成/削除されないのであればDIContainer(ZenjectVContainer)経由でオブジェクトを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は引数で外からPlayerViewの組み合わせをうけて、バインドできるようしておきます。
今回は単一のPresenterを使いまわして複数のPlayerViewを扱わせます。

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);
        }
    }
}

image.png
ViewPrefab

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);
        }
    }
}

image.png
(シーンにDispatcherを配置して、PlayerManagerPresenterViewのPrefabを参照させる)

今回はPresenterをあらかじめシーンに配置していますが、Presenterもまたprefabにしてしまい都度生成する形にしても問題ありません(やりやすいようにやればOK)

動作の様子

MultiPresenter.gif

Playerが動的に生成されても体力バーがそれぞれに表示されており、それぞれの体力が反映されている)

「Presenterってどこにおけばいいんですか」

Presenterをシーン上のどこに配置するのか。もしくはMonoBehaviourを使わずにピュアクラスとしてDIContainerに管理するべきなのか。という質問です。

答えは「自由にやってよい」です。
管理できてるならどこにPresenterを配置してもよいです。uGUICanvasGameObjectPresenterを貼り付けてもいいですし、空のGameObjectを作ってそこに貼り付けてもよいです。

自分が管理さえできているのであれば、どこにどう置いてもよいです。

「PresenterとViewの規模感がわからない」

これは「1つのPresenterが複数のViewを管理していいのか」「Viewの内部で状態を持っていてよいのか」といった質問と同じです。

Presenterの規模感

1つのPresenterがどうViewを管理するかですが、これも答えは「自分が管理できるなら自由にやってよい」です。

  • 1つのGameObjectに1つのPresenterだけ割り当てるやり方
  • 1つのGameObjectに複数のPresenterを割り当てるやり方
  • 1つのPresenterで複数のModelViewを割り当てるやり方

PresenterPatterns1.jpg

PresenterPatterns2.jpg

PresenterPatterns3.jpg
(どうやってもOK)

いくつかパターンは挙げられますが、どれも好きにやってOKです。

Viewの規模感

Viewの内部で状態を持ってよいか」「Viewを管理するコンポーネントもViewとして扱ってよいか」ですが、答えは「はい」です。
View自身が状態をもち、Viewが自分自身を管理するのはMV(R)Pパターン的には問題がありません。

たとえば、次のような「RGBの三色のスライダー」があったとします。

ColorSlider.gif

これには「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のどちらがもつべき状態ですか

この質問は、たとえば「クリックされたら色の変わるボタンがあったときに、この"色"の情報はViewModelのどちらがもつのですか」みたいなやつです。

ChanageColorButton.gif
(クリックされたら色の変わるボタン。この「色」情報はModelViewのどっちがもつ?)

答えは「その色情報がゲームにおけるドメイン要素にあたるかどうか」で決まります。
(「ドメイン」とは「ゲームのコア要素」みたいな意味だと思って下さい)

その「色情報」がゲームの成立に必要不可欠な要素であるならば、それはModelで管理するべき情報となります。
一方でゲームの成立に直接関係なく、あくまで演出の目的としてボタンの色を変えている程度であるならばそれはViewで管理するべき情報となります。

たとえば「3色リバーシ」などは色情報がゲームの成立に絶対必須な要素です。
そのため3色リバーシをMV(R)Pパターンで構築するのであれば、「色の情報はModelが管理するべき」となります。

一方で「ちょっと見た目がリッチなトグルボタン」という扱いであるならば、これはただの演出でしかなく、最悪「ボタンの色の変化」無くてもゲームとして成立します。
こういった場合はViewで管理するべきとなります。

Model要素になるパターン
  • その情報(データ)がゲームの成立に必要不可欠である
    • ゲームの進行や状態管理に必須である
View要素になるパターン
  • その情報(データ)が無くてもゲームとして成立する
    • あると見た目がリッチになるが最悪無くてもなんとかなる、みたいなの

View->Presenter->Model->Presenter->View って値がループしたりしないんですか

  1. Viewの変化を検知してPresenterModelに書き込む
  2. Modelの状態が変化する
  3. Modelの状態が変化したことを検知してPresenterViewに反映する
  4. Viewの状態が変化する
  5. Viewの変化を検知してPresenterModelに書き込む
  6. (以下無限ループ)

という現象が起きないかどうかという質問です。
答えは「ReactivePropertyを使っている場合においてはループしません」。

ReactivePropertyは「直前と同じ値が書き込まれた場合、イベントを発行しない」という仕組みになっています。そのためイベントが一巡はしますが、2周目のループには入らないようになっています。

ただし、ReactiveProperty.SetValueAndForceNotify()を使っていた場合は強制的にメッセージ発行が実行されるため無限ループを引き起こす可能性があります。

Model/View/PresenterごとにAssembly Definition Filesを分けたほうがいいですか

答えは「ご自由にどうぞ」です。
厳密にやるために分けてもいいですし、面倒くさいから分けないというのも手です。
この辺は本当にご自由にどうぞ。

依存関係逆転則を使って、依存関係をView->Presenter->Modelにしたほうがいいですか

参照関係は「ViewPresenterModel」だが、モジュールとしての依存関係は「 ViewPresenterModel」にしたほうがいいんですかという質問です。

意味がわからない人はこのスライドでちょっと説明してます。

Unityにおける設計パターン(56ページ)

結論は「冗長なのでやる必要はなし」だと自分は考えます。
厳密にやりたいならやってもいいですが、手間の割にリターンが見合わないと思います。

「MVPパターンを使うとUnity非依存でキレイに作れるって聞きました」

答えとしては「できなくはないが、それ以前に考えることがある」です。

MVPパターン(MV(R)Pパターン)はもともと「GUI周り向け」の設計パターンです。
View(GUI)をどう管理すればキレイに実装できるか、という話でした。

しかし、このViewという概念を拡大解釈し、「View(Unity)とModel(Unity非依存)を分けてキレイに作るための設計パターンだ」と勘違いしている人がたまにいます。

  • MVPパターンを使えば、GameObjectを使わずにゲームロジックが組める」
  • MVPパターンを使えば、アクションゲームをキレイに作れる」
  • MVPパターンを使えば、設計がすべてキレイになる」

これらはすべてMVPパターンに過度の信頼をおきすぎです。

何回も言いますが、「MVPパターンがいうViewとはGUIのこと」であり、GameObjectMonoBehaviourのことではありません。MVPパターンは「GUIを実装するためのパターン」です。

「フレームワーク(Unity)」と「ドメイン(ゲームのコアロジック)」を分離した設計はたしかに理想ではあります。
ですがこれを実現するためには「MVPパターン」だけでは手駒が足りず、他の設計パターンやアーキテクチャの知識も必要となってきます。(たとえば、巷で話題の「クリーンアーキテクチャ」は、まさに「複雑化するドメインからフレームワーク依存をどう分離するか」を語っているアーキテクチャとなっています。)

(参考: Unityにおける「設計レベル」を定義してみた

ではMVPパターンを拡大解釈して、「フレームワークとドメインの分離」に用いるのがダメなのかというと、別にそうではありません。シチュエーションによってはMVPパターンが使える場面もあったりします。

たとえば、「サーバサイドにロジックとデータがある」「UnityはそのViewerとして使う」みたいにハッキリ区分ができている場合はMVPパターンを使っても上手くいく可能性があります。

ですが一方で、「アクションゲーム」みたいな「Unityにべったり依存した仕組みの上で動くゲーム」などはそもそも「フレームワークとドメインの分離」自体が難しかったりします。こういったそもそも分離が難しい類のものを無理やり分離して作るのは非常に筋が悪く、上手くいかない可能性が高いです。

なので「フレームワークとドメインの分離」をやる場合は、まずそもそもそれをやる意義があるのかを考える必要があります。「フレームワークとドメインの分離」は確かに設計上はキレイになりそうですが、実際は実施コストが高く、また開発速度も遅くなるというデメリットがあります。

  • 作ろうとしているゲームの規模感
  • 開発者の人数
  • スケジュール
  • ゲームのジャンル
  • システムのアーキテクチャ(クライアント完結なのか、サーバ連携するのかなど)

このあたりを判断材料として、「そもそもフレームワークとドメインを完全に分離して作ることに本当にメリットがあるのか」「やる場合はどういうアーキテクチャを採用するのか」を考える必要があります。

というわけで、答えは「(MVPパターンを使ってUnity非依存な実装は)できなくはないが、それ以前に考えることがある」です。

口酸っぱくいいますが、「MVPパターンはもともとGUI設計のためのパターン」です。

これを応用して、「フレームワークとドメインの分離」に使ってみること自体は悪いことではありません。ですが、それをやるには設計についての他の知識や、それ相応の覚悟が必要となります

そしてそれを初心者に求めるのは酷なので、MVPパターンは素直にGUI周りにだけ使っておいた方が安全という話になります。

まとめ

  • MV(R)PパターンはUnityにおける「GUI周り」の設計パターン
  • ModelViewを、Presenterという薄いレイヤでつなごう
  • ModelViewの連動には、UniRxを活用しよう(ReactivePropertyが便利)
  • 上記以上のことについては、割と自由にやってよい
    • 絶対の正解は無いので、やりやすいように解釈して使ってOK
    • ただし「あくまでGUI周り用の設計パターンである」ということは念頭におくこと

(追記)

あとで「スマホ画面から移動操作などのInputを受け付けるときの考え方」みたいなのを追記します
機会があれば書くかも…。

308
241
1

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
  3. You can use dark theme
What you can do with signing up
308
241