- 投稿日:2022-02-28T22:43:45+09:00
【Unity】Model-View-(Reactive)Presenterパターンとは何なのか
はじめに 今回は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); } } } (ViewのPrefab) 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) いくつかパターンは挙げられますが、どれも好きにやって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」にしたほうがいいんですかという質問です。 意味がわからない人はこのスライドでちょっと説明してます。 Unityにおける設計パターン(56ページ) 結論は「冗長なのでやる必要はなし」だと自分は考えます。 厳密にやりたいならやってもいいですが、手間の割にリターンが見合わないと思います。 「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を受け付けるときの考え方」みたいなのを追記します
- 投稿日:2022-02-28T18:24:35+09:00
VRMモデルを使っているけど、どうしてもFBXでUnityに入れたいとき用(BlenderでのBlendshapeを使いたいとき)
BlenderでVRMをFBXで出力後にLook Atを適用する方法 Blenderの設定 fbx の出力時に、 内容からアーマチュアとメッシュを選択。 アーマチュアからリーフボーンを追加のチェックを外す。 アニメーションをベイクのチェックを外す。 そのあとで、FBXをエクスポートをクリック FBXの設定 肝のところ! Model の Bake Axis Conversion のチェックをつける Rig の Animation Type を Humanoid に設定 VRN Look At Headの変更 VRMLookAtHead.cs [SerializeField] public float yawDiff; //追加 [SerializeField] public float pitchDiff; //追加 ... public void LookWorldPosition(Vector3 targetPosition, out float yaw, out float pitch) { var localPosition = Head.worldToLocalMatrix.MultiplyPoint(targetPosition); Matrix4x4.identity.CalcYawPitch(localPosition, out yaw, out pitch); yaw -= yawDiff; //追加 pitch -= pitchDiff; //追加 RaiseYawPitchChanged(yaw, pitch); } Gizumoでデバッグしてyawとpitchの分のずれを補正する. Modelの設定 あとは通常通りに、 別にimportしたVRMモデルからマテリアルの設定 VRM Look At Head と VRM Look at Bone Applyer を設定する! 以上!