【Unity】MV(R)Pパターンのすすめ



こんにちは。Annulus Gamesです。

今回はUnityにおけるUIの設計について。UnityでUIを実装するとき、全ての処理をUpdate関数に詰め込んでしまうことはありませんか?

この記事では、UniRxを用いた「MV(R)Pパターン」と呼ばれる設計パターンを用いて、UnityのGUI周りを実装する方法について解説していきます。


MV(R)Pパターンって何?

実装の解説を始める前に、まず「MV(R)Pパターンとは何か」についてを軽く説明します。

「Model-View-(Reactive)Presenterパターン」とはUnityにおけるGUI周りの実装に用いられる設計パターンです。

Unityに限った話ではないですが、GUI周りの実装は複雑になりがちです。多くのデータを扱う必要があるだけでなく、それらが相互に連動し、さらにはユーザーから干渉されることもあります。

そのため、そういった部分をわかりやすく実装するために、MVCやMVVMなど、様々な設計パターンが考案されてきました。その中のMVPパターンをもとに、UniRxを用いて設計されたものが「MV(R)Pパターン」です。


パターンなんかなくてもいいんじゃね?

とは言いましたが、「そもそも設計パターンなんてなくても書けるじゃん?」「全部Updateに書くことの何がダメなの?」という疑問もあるかと思います。

確かに、シンプルなゲームのUIであれば「プレイヤーのHPを表示する」「コインの総数を表示する」など、一つ一つの実装はさして複雑でない場合が多いです。

しかし、それらの要素が数百個あり、それら全てを表示しなければならない場合はどうでしょう。それらをすべてUpdate関数に詰め込めば、訳がわからなくなってしまいます。

例えばこのようなRPGのUIがあったとします。










(ものすごく既視感のあるUIですが、気にしないでおきましょう)

このUIを一つのクラスで実装してみると、こんな感じになります。

  using UnityEngine;
  using UnityEngine.UI;
  using TMPro;

  public class StatusView : MonoBehaviour
  {
      [SerializeField] private TextMeshProUGUI playerNameText;
      [SerializeField] private TextMeshProUGUI levelText;
      [SerializeField] private TextMeshProUGUI hpText;
      [SerializeField] private Slider hpSlider;

      //外から読み書きされる値
      public string PlayerName { get; set; }
      public int Level { get; set; } = 1;
      public int HP { get; set; } = 10;
      public int HPMax { get; set; } = 10;

      void Update()
      {
          //変更がなくても毎フレーム更新を行う
          playerNameText.text = PlayerName;
          levelText.text = Level.ToString();
          hpText.text = HP.ToString() + "/" + HPMax.ToString();

          hpSlider.maxValue = HPMax;
          hpSlider.value = HP;
      }
  }

まあ、なんというか...ぐちゃぐちゃですね。uGUIの管理とデータの管理を同じクラスが行なっているし、毎フレーム無駄な更新処理が走るし...

まだ処理が少ないだけマシですが、より複雑な処理を扱おうとすると、さらにクラスが肥大化していきます。

このように、設計パターンを考えずにUIの処理を実装していくと、無駄が多く読みづらいコードが出来上がってしまうのです。


ModelとViewの分離

ではここからが本題。このUIをMV(R)Pパターンを用いて実装していきます。
...が、その前に。まずはMVPパターン、すなわちModel-View-Presenterパターンについて説明します。


MVPパターンにおいてはまず、GUI周りの要素を以下の三つに分類します。

先ほどの例で言うと、

・HPやレベルなどの値を「Model」

・TextやSliderなどのuGUIを「View」

として扱うことができるでしょう。




ここで重要なのは「ModelとViewがお互いを知らないということです。

両者をつなぐのは「Presenter」のみなので、Presenterが存在しなければModelとViewは完全に独立した状態になります。

これにより各クラスの責務が明確になる他、ViewとModelが互いに依存していない(疎結合である)ため、どちらかを差し替えることが簡単にできるようになります。


Model-View-(Reactive)Presenter

長くなりましたが、ようやくここからMV(R)Pパターンの説明に入ります。

MV(R)Pパターンでは、先ほどのMVPパターンと同様にModel、View、Presenterに分け、それらをUniRxの機能を使って連携させます。


UniRxには、ReactivePropertyという非常に便利な機能が存在します。

例えば、「ある値が変更された時に何らかの処理を行いたい」とします。これをReactivePropertyを使って実装するとこんな感じです。


  using UnityEngine;
  using UniRx;

  public class SampleA : MonoBehaviour
  {
      //値を読み取り専用にして公開する
      public IReadOnlyReactiveProperty<int> Hoge => hoge;
      private ReactiveProperty<int> hoge = new ReactiveProperty<int>(0);

      void Start()
      {
          //普通の変数のように値を変更できる
          hoge.Value = 10;
      }

      void OnDestroy()
      {
          //Disposeを忘れずに
          hoge.Dispose();
      }
  }

  public class SampleB : MonoBehaviour
  {
      [SerializeField] private SampleA sampleA;

      void Awake()
      {
          //処理を登録(購読)
          sampleA.Hoge.Subscribe(value => OnValueChanged(value))
              .AddTo(this);
      }

      void OnValueChanged(float value)
      {
          //なんらかの処理
          //値を表示してみたり
          Debug.Log(value);
      }
  }
  

このように、ReactivePropertyを使うと、hogeの値の変更を外部に通知することができます。

ReactivePropertyを利用することによって「ModelやViewの変更を通知し」「Presenterを介してもう一方に状態を反映する」

これがMV(R)Pパターンの基本的な考え方になります。


MR(R)Pパターンを使ってみよう

それでは、先ほどのRPGのUIをMV(R)Pパターンを用いて作り直してみましょう。









まずは「Model」を用意します。今回はPlayerクラスに値を保持させ、それらをReactivePropertyとして公開することでModelとして扱います。

  using UnityEngine;
  using UniRx;

  public class Player : MonoBehaviour
  {
      public IReadOnlyReactiveProperty<int> Level => level;
      public IReadOnlyReactiveProperty<string> PlayerName => playerName;
      public IReadOnlyReactiveProperty<int> HP => hp;
      public IReadOnlyReactiveProperty<int> HPMax => hpMax;

      private ReactiveProperty<int> level = new ReactiveProperty<int>(1);
      private ReactiveProperty<string> playerName = new ReactiveProperty<string>("Chara");
      private ReactiveProperty<int> hp = new ReactiveProperty<int>(10);
      private ReactiveProperty<int> hpMax = new ReactiveProperty<int>(10);

      //ダメージを与える処理とか
      public void Damage(int value)
      {
          hp.Value -= value;
      }

      ...

      void OnDestroy()
      {
          //Disposeを忘れずに
          level.Dispose();
          playerName.Dispose();
          hp.Dispose();
          hpMax.Dispose();
      }
  }

こんな感じでしょうか。


次に「View」ですが、今回はUnityのuGUIをそのままViewとして扱います。







ViewはPresenterから参照できればなんでもいいので、uGUIでも良いし、複数のテキストやボタンなどを管理するコンポーネントでも構いません。

ただし、Viewがゲームの構成に必要な値(HPやレベルなど)を持たないようにしてください。重要なのはModelとViewを分離することです。


最後に「Presenter」

Presenterでは、ViewとModel間でのデータの変換や、状態の反映を行います。PresenterはあくまでViewとModelを繋ぐだけの役割なので、それ以外のロジックは持たせないようにしましょう。

実装はこんな感じ。

  using UnityEngine;
  using UnityEngine.UI;
  using TMPro;
  using UniRx;

  public class Presenter : MonoBehaviour
  {
      //Model
      [SerializeField] private Player player;

      //View
      [SerializeField] private TextMeshProUGUI playerNameText;
      [SerializeField] private TextMeshProUGUI levelText;
      [SerializeField] private TextMeshProUGUI hpText;
      [SerializeField] private Slider hpSlider;

      void Start()
      {
          //値を監視
          player.PlayerName
              .Subscribe(value =>
              {
                  //値をViewに反映させる
                  playerNameText.text = value;
              })
              .AddTo(this);

          player.Level
              .Subscribe(value =>
              {
                  levelText.text = value.ToString();
              })
              .AddTo(this);

          player.HP
              .Subscribe(value =>
              {
                  hpText.text = value + "/" + player.HPMax.Value;
                  hpSlider.value = value;
              })
              .AddTo(this);

          player.HPMax
              .Subscribe(value =>
              {
                  hpText.text = player.HP.Value + "/" + value;
                  hpSlider.maxValue = value;
              })
              .AddTo(this);
      }
  }
  

あとは、Presenterを適当なオブジェクト(Canvasなど)にアタッチしてInspectorからModel(Player)とView(uGUI)を割り当てることで、実装は完了です。

どうでしょうか。最初のコードと比べると、それぞれのクラスの役割が明確になり、わかりやすい設計になったのではないでしょうか。

また、今回はModelの値をViewに反映させるのみ(Model→Presenter→View)でしたが、それとは逆にViewの値をModelに反映させたい場合にも、同じようにPresenterを介して実装することができます。


まとめ

・MV(R)PパターンとはUnityにおけるGUI周りの設計パターン

・ModelとViewを分離し、Presenterで繋ぐ

・変更の通知にUniRxを用いる


いかがだったでしょうか。ModelとViewの分離は慣れるまで少々難しいかもしれませんが、複雑なUIを組む上ではほぼ必須となる手法です。ぜひ覚えて、使いこなしていきましょう。


コメント