【Unity(C#)】MV(R)Pパターンで作ってみる

2021/04/25 追記
こちらの記事見返してて思ったんですが、
Sliderの値をReactivePropertyに代入して公開するのではなくて、
Sliderそのものを公開すればいいのでは？ってなりました。
以下の記事ではそうしてます。
【参考リンク】：[【Unity(C#)】UniRxとExtenjectでMV(R)Pやってみる](https://qiita.com/OKsaiyowa/items/1e2477ace3c397b166aa)


##デモ
早速ですがデモです。
Sliderの値を変更するとCubeがそれぞれの軸を基準に回転します。

![MVP_Sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/e8d25571-40d6-3a1a-c158-e59599ee8d99.gif)

このコード自体は簡単ですが、一つのコードにべた書きするとロジックやUIが絡み合ってくるので
クラスが肥大化し、それぞれの役割を差し替えたりテストしたりが難しくなります。

そこでMV(R)Pパターンを使います。

##MV(R)Pとは

UniRxを使ったデザインパターンです。
**Model、View、Presenter**がそれぞれ役割を持ちます。

| Model | View | Presenter |
|:-----------|------------:|:------------:|
| ロジック       | 見た目        | ロジックと見た目を繋ぐ  |

詳細な説明は最後に参考リンクを貼ってます。

PresenterはModelとViewを繋ぎ、
**ModelとViewがお互いに依存関係のない状態**を作り出せます。

クラス図にすると下記です。
![MVP_Image.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/da434567-b3df-ade8-3820-d98dd6a6b946.png)

正確には、**ViewとModelの変更がReactiveに互いに反映されるつくり**のことを指すようですが、
今回は下記のような一方通行の変更通知パターンを実際に作りながら学びます。
**・Viewの変更→Presenterに通知→Modelに反映**

逆に下記のようなパターンは参考リンクの図がイメージしやすいと思います。
・Modelの変更→Presenterに通知→Viewに反映
【参考リンク】：[プログラミング MVPパターン ~理論編①~](https://hakase0274.hatenablog.com/entry/2019/03/17/235736)


## バージョン情報
Unity 2019.4.8f1
UniRx 7.1.0

##Model

まずはModelを作ります。Cubeを回転させる というロジックを持ちます。
Viewの存在は知りません。

```CS
using UnityEngine;

namespace Ono.MVP.Model
{
    /// <summary>
    /// ビジネスロジックを持つモデルクラス
    /// キューブを回転させる
    /// キューブにアタッチ
    /// </summary>
    public class CubeRotationModel : MonoBehaviour
    {
        /// <summary>
        /// インスタンス
        /// </summary>
        public static CubeRotationModel Instance;
        
        private void Awake()
        {
            Instance = this;
        }

        /// <summary>
        /// 与えられたパラメータに応じてX軸方向にキューブを回転
        /// </summary>
        /// <param name="x">X軸回転</param>
        public void SetRotationX(float x)
        {
            var rot = Quaternion.AngleAxis(x, Vector3.right);
            transform.rotation =  rot;
        }

        /// <summary>
        /// 与えられたパラメータに応じてY軸方向にキューブを回転
        /// </summary>
        /// <param name="y">X軸回転</param>
        public void SetRotationY(float y)
        {
            var rot = Quaternion.AngleAxis(y, Vector3.up);
            transform.rotation =  rot;
        }

        /// <summary>
        /// 与えられたパラメータに応じてZ軸方向にキューブを回転
        /// </summary>
        /// <param name="z">Z軸回転</param>
        public void SetRotationZ(float z)
        {
            var rot = Quaternion.AngleAxis(z, Vector3.forward);
            transform.rotation =  rot;
        }
    }
}
```

Model内の公開された機能(パブリックなメソッド)はPresenterにて呼び出します。
Model内では使いません。

## View

ViewはSliderの変更をPresenterに通知する役割を持ちます。
Modelの存在は知りません。

```CS
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace Ono.MVP.View
{
    /// <summary>
    /// SliderのViewを担うクラス
    /// </summary>
    public class SliderView : MonoBehaviour
    {
        [SerializeField] private Slider _sliderX, _sliderY, _sliderZ;
        [SerializeField] private Text _textX, _textY, _textZ;

        /// <summary>
        /// X軸操作のSlider
        /// 購読機能のみ外部に公開
        /// </summary>
        public IReadOnlyReactiveProperty<float> SliderValueRP_X => _floatReactivePropertyX;
        private readonly FloatReactiveProperty _floatReactivePropertyX = new FloatReactiveProperty();

        /// <summary>
        /// Y軸操作のSlider
        /// 購読機能のみ外部に公開
        /// </summary>
        public IReadOnlyReactiveProperty<float> SliderValueRP_Y => _floatReactivePropertyY;
        private readonly FloatReactiveProperty _floatReactivePropertyY = new FloatReactiveProperty();

        /// <summary>
        /// Z軸操作のSlider
        /// 購読機能のみ外部に公開
        /// </summary>
        public IReadOnlyReactiveProperty<float> SliderValueRP_Z => _floatReactivePropertyZ;
        private readonly FloatReactiveProperty _floatReactivePropertyZ = new FloatReactiveProperty();

        void Start()
        {
            //X軸操作用Sliderの値の変更を監視
            _sliderX.OnValueChangedAsObservable()
                .DistinctUntilChanged()
                .Subscribe(value => { OnValueChange(value, _floatReactivePropertyX, _textX); })
                .AddTo(this);

            //Y軸操作用Sliderの値の変更を監視
            _sliderY.OnValueChangedAsObservable()
                .DistinctUntilChanged()
                .Subscribe(value => { OnValueChange(value, _floatReactivePropertyY, _textY); })
                .AddTo(this);

            //Z軸操作用Sliderの値の変更を監視
            _sliderZ.OnValueChangedAsObservable()
                .DistinctUntilChanged()
                .Subscribe(value => { OnValueChange(value, _floatReactivePropertyZ, _textZ); })
                .AddTo(this);
        }

        /// <summary>
        /// Sliderの値変更時の処理
        /// </summary>
        /// <param name="value">Sliderの値</param>
        /// <param name="floatReactiveProperty">値を更新をしたいRP</param>
        /// <param name="valueText">更新するテキスト</param>
        private void OnValueChange(float value, FloatReactiveProperty floatReactiveProperty, Text valueText)
        {
            //値の整形
            var arrangeValue = Mathf.Floor((value - 0.5f) * 100) / 100 * 360;
            //値の更新
            floatReactiveProperty.Value = arrangeValue;
            //テキストに値を反映
            valueText.text = arrangeValue.ToString();
        }
    }
}
```

ReactivePropertyの購読機能のみ外部に公開します。
これによりPresenter側で通知を受け取ることができます。

##Presenter

ModelとViewを繋ぐPresenterです。
Model、Viewの存在を知っています。

```CS
using Ono.MVP.Model;
using Ono.MVP.View;
using UniRx;
using UnityEngine;

namespace Ono.MVP.Presenter
{
    /// <summary>
    /// ViewとModelを繋ぐPresenter
    /// </summary>
    public class CubeRotationPresenter : MonoBehaviour
    {
        [SerializeField] private SliderView _sliderView;
        
        void Start()
        {
            var cubeRotationLogic = CubeRotationModel.Instance;

            // ================================
            // Sliderの値の更新を監視
            // ================================
            
            _sliderView.SliderValueRP_X
                .Subscribe(value => { cubeRotationLogic.SetRotationX(value); }).AddTo(this);
            
            _sliderView.SliderValueRP_Y
                .Subscribe(value => { cubeRotationLogic.SetRotationY(value); }).AddTo(this);
            
            _sliderView.SliderValueRP_Z
                .Subscribe(value => { cubeRotationLogic.SetRotationZ(value); }).AddTo(this);
        }
    }
}
```

View側で公開されている購読機能の中でModel側で公開されている機能を利用します。

## 最後に

一応GitHubに置いときました。
[MVP_Demo](https://github.com/ForJobOk/MVP_Demo)

続きとして、MonoBehaviourを継承していないクラス間でのMVPパターンを
Extenjectと合わせて作ってみようと思ってます。

---

2021/04/25 追記
やってみました。
【参考リンク】：[【Unity(C#)】UniRxとExtenjectでMV(R)Pやってみる](https://qiita.com/OKsaiyowa/items/1e2477ace3c397b166aa)


##参考リンク
[UnityにおけるMVPパターンについて](https://virtualcast.jp/blog/2019/11/mvp_pattern_on_unity/)
[Web出身のUnityエンジニアによる大規模ゲームの基盤設計](https://developers.cyberagent.co.jp/blog/archives/4262/)
[UniRxでMV(R)Pパターンをやってみた](https://www.slideshare.net/torisoup/unirxmvrp)
[UniRxとZenjectを使ってMV(R)Pで南京錠を作る](https://ryo620.org/post/unirx-zenject-mvrp-padlock/)
[Unityで学ぶMVPパターン ~ UniRxを使って体力Barを作成する ~](https://qiita.com/Nakashima_Hibari/items/5e0c36c3b0df78110d32)