「UnityソルジャーでもMVPパターンくらいはやっとけ」みたいな世間の風潮を感じるので、「別にそれくらいできるが?」というとこを見せつけてやりましょう。
話を分かりやすくするために、Unityソルジャーのタケル君という想像上のキャラクターを登場させましょう。
タケル「俺はタケル。Unityソルジャー 1st。俺が引き受けた仕事に失敗はあり得ない」
今回タケル君に作ってもらうのはこちら。
なんかこういう、プレイヤーのHP表示があって、ダメージボタンを押すとHPが減る感じの戦闘画面的なやつ。
タケル「引き受けよう。報酬は10万。翌月末までに振り込んでくれ」
ステップ1 なんも考えずに作る
タケル君が書いてきたスクリプトはこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//プレイヤー public class Player : MonoBehaviour { [SerializeField]Text textHp; //HP表示Text public int hp = 100; //プレイヤーのHP void Update() { //HPの表示更新 textHp.text = hp.ToString(); } } |
これがPlayerのスクリプト。
1 2 3 4 5 6 7 8 9 10 11 12 |
//ダメージボタン public class ButtonDamage : MonoBehaviour { [SerializeField] Player player; //Playerを参照 //インスペクタでButtonのOnClickにセット public void SetDamage() { //プレイヤーのHPを減らす player.hp = player.hp - Random.Range(1, 11); } } |
こっちがダメージボタンのスクリプト。
実行してみると、ちゃんとダメージボタン押すたびにダメージ受けてHPが減りますね。
たしかに、別にこれでも何の文句も無いけど、ちょっと修正してもらえるともっと良くなるかも。
タケル「要件は満たしてるが?」
うん。そうですね。別に問題ないっちゃないんだけど、Updateの中でHPの変更を毎フレーム監視する作りは、変えてもらえると嬉しいかも。
タケル「まあ、多少のサポートはあらかじめ料金に含まれるから修正に応じよう」
ステップ2 Update()を無くす
修正されたPlayerスクリプトがこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//プレイヤー public class Player : MonoBehaviour { [SerializeField]Text textHp; //HP表示Text int hp = 100; //プレイヤーのHP public int Hp { get { return hp; } set { if (hp != value)//変更があった時だけ { hp = value; textHp.text = hp.ToString(); //HP表示を更新 } } } } |
HPのプロパティを用意して、SetterでHPが変更された時だけtextに反映されるようになりました。これでUpdate()が消せたので負荷が減ります。
でも、欲を言えばMVPパターンでModelとViewを分割してくれた方がありがたいかも…。
タケル「MVPパターンって何だよ」
うん…MVPっていうのはUIに使われるアーキテクチャパターンの一種で、これを意識してコード書くと”比較的簡単にいい感じになって嬉しい”って感じだね。
MVPパターンではView(UI)をModel(アプリの状態)から分離して、Presenterで繋ぐっていう形になります。
タケル「何が言いたい?」
まあUnityで言えば、UIに関係ないクラスからUIを切り離すって事かな。現状だとModel(Playerクラス)がView(textHp)を持ってしまってるので。
タケル「元々繋がってたものをわざわざ切り離して繋ぎ直すなんて事をして何が嬉しいんだ?」
まず、UI部品単位で”View”として作っておけば、使い回しが効くから便利だよね。上の図の例で言えば、”バーゲージ”のViewをプレハブとして作っとけば、HPバーとMPバー両方で使えるから便利。これは分かりやすい。
逆に、モデル(上の例だとPlayerクラス)も一か所でしか使われないとは限らないんだよね。戦闘画面のHP表示だけならいいけど、メニュー画面のHP表示は場所も形も違うかもしれないし。戦闘画面は戦闘画面のビュー、メニュー画面はメニュー画面のビューがあるから、モデルはビューと分離しないと厄介な事になると分かる。
タケル「あらかじめ何が必要で何が不要になるか分からないから、実際にそういう複数画面が必要になってから、その時はじめてモデルとビューを分離とか考えればいいんじゃないのか?」
おっしゃる通り。なんだけど、まあ手間がかからない範囲で先読みして柔軟に作る分にはその方がいいんじゃないかって話なんですね。
ゲーム開発の終盤になって、ビューを分割しないとマズいって事に気付いてから初めて分割しようとしても、すでに大量の箇所で参照されちゃってて今さらリファクタリング不可能で後悔するかもしれないよね。
だから常にMVP的に作る習慣をつけといた方が、最終的にはトクする事が多いって事なんだよね。
タケル「なるほど。そこまで言うならMVP的に改修してみよう」
お願いします!
ステップ3 MVP的に作る
MVP的に改修されたスクリプトがこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//プレイヤーモデル public class PlayerModel : MonoBehaviour { public UnityAction<int> OnChangeHp; //HP変更時のアクション int hp = 100; //プレイヤーのHP public int Hp { get { return hp; } set { if (hp != value)//変更があった時だけ { hp = value; if (OnChangeHp != null) { OnChangeHp.Invoke(hp); //アクションを呼ぶ } } } } } |
PlayerのModelです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//戦闘画面のプレイヤーモデルのプレゼンター public class BattlePlayerPresenter : MonoBehaviour { public PlayerModel playerModel; //プレイヤーモデルを参照 [SerializeField]Text textHp; //HP表示テキスト [SerializeField]Button buttonDamage; //ダメージボタン void Start() { //HP表示テキスト playerModel.OnChangeHp = ( (hp) => { textHp.text = hp.ToString(); } ); //ダメージボタン buttonDamage.onClick.AddListener( () => { playerModel.Hp = playerModel.Hp - Random.Range(1, 11); }); } } |
Presenterです。戦闘画面でのPlayerModelに対するPresenterというイメージです。Presenterの粒度をどれくらいにするかは色々意見があるようですが、まあ画面ごとのModelごとくらいの粒度でもいいんじゃないでしょうか。
Viewについては、今回は単にUGUIのTextとかButtonをそのまま使うだけなので、クラスは作りませんでした。UGUIを組み合わせて自作のUI部品を作った時とかはそれがViewのクラスになるイメージ。
この改修で、PlayerModelが直接View(UI)を持たなくて済むようになりましたね。
こういう作りにしておけば、PlayerModelは全てのシーンで共通で持っておきつつ、戦闘画面ではBattlePlayerPresenter、メニュー画面に入ったらMenuPlayerPresenterがいい感じにModelをViewに接続してくれそうですね。
逆に、Viewを操作するとPresenterを介してModelが変更されるようにもなってます。ダメージボタンのやつです。(別にAddListenerじゃなくても普通にメソッド作ってButtonのインスペクタで参照する普通のUnityのやり方でも構わんと思う)
厳密なMVPになってるかどうか分かりませんが、まあ厳密なところは人によって解釈が異なるらしい(Viewひとつ毎にPresenter作れと言う人もいるらしい)ですし、適当でいいでしょう。
タケル「仕事は完了した。後で請求書を送る」
タケル君、ありがとうございました。
ステップ4 UniRxのReactivePropertyを使う
こういう作り方もあるよと言う話で。
私はReactというjavascriptのフレームワークを少し触ったことがありますが、Reactでは状態(state)が変更されると、それに応じて必要なUIが”自動的に反応して”反映されます。
自動的に反応(リアクション)するからReactというわけです。
こういう風に、状態が勝手にUIに反映される作りってカッコいいよね。という文化があるんですね。
で、UniRxというライブラリのReactivePropertyという機能を使うと、そんな感じで状態(変数)の変化に応じて自動的にUIとかに反映する的な作りが簡単に実現できます。
UniRxは機能が多彩過ぎて中々使いこなすのが難しいですが、とりあえずReactivePropertyだけを使うのもアリかもしれません。この機能はSubscribeするだけだから簡単。
ReactivePropertyを使うと先ほどのコードはこうなります。
1 2 3 4 5 6 7 |
//プレイヤーモデル public class PlayerModel : MonoBehaviour { //プレイヤーのHP public IntReactiveProperty Hp = new IntReactiveProperty(100); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//戦闘画面のプレイヤーモデルのプレゼンター public class BattlePlayerPresenter : MonoBehaviour { public PlayerModel playerModel; //プレイヤーモデル [SerializeField]Text textHp; //HP表示テキスト [SerializeField]Button buttonDamage; //ダメージボタン void Start() { //HP表示テキスト playerModel.Hp.Subscribe( (hp) => { textHp.text = hp.ToString(); } ); //ダメージボタン buttonDamage.onClick.AddListener( () => { playerModel.Hp.Value = playerModel.Hp.Value - Random.Range(1, 11); }); } } |
うわ…PlayerModelのコード量がえげつないくらい減りました。
さしものUnityソルジャーでもこれなら嬉しいですね。