2021/04/25 追記
こちらの記事見返してて思ったんですが、
Sliderの値をReactivePropertyに代入して公開するのではなくて、
Sliderそのものを公開すればいいのでは?ってなりました。
以下の記事ではそうしてます。
【参考リンク】:【Unity(C#)】UniRxとExtenjectでMV(R)Pやってみる
##デモ
早速ですがデモです。
Sliderの値を変更するとCubeがそれぞれの軸を基準に回転します。
このコード自体は簡単ですが、一つのコードにべた書きするとロジックやUIが絡み合ってくるので
クラスが肥大化し、それぞれの役割を差し替えたりテストしたりが難しくなります。
そこでMV(R)Pパターンを使います。
##MV(R)Pとは
UniRxを使ったデザインパターンです。
Model、View、Presenterがそれぞれ役割を持ちます。
Model | View | Presenter |
---|---|---|
ロジック | 見た目 | ロジックと見た目を繋ぐ |
詳細な説明は最後に参考リンクを貼ってます。
PresenterはModelとViewを繋ぎ、
ModelとViewがお互いに依存関係のない状態を作り出せます。
正確には、ViewとModelの変更がReactiveに互いに反映されるつくりのことを指すようですが、
今回は下記のような一方通行の変更通知パターンを実際に作りながら学びます。
・Viewの変更→Presenterに通知→Modelに反映
逆に下記のようなパターンは参考リンクの図がイメージしやすいと思います。
・Modelの変更→Presenterに通知→Viewに反映
【参考リンク】:プログラミング MVPパターン ~理論編①~
バージョン情報
Unity 2019.4.8f1
UniRx 7.1.0
##Model
まずはModelを作ります。Cubeを回転させる というロジックを持ちます。
Viewの存在は知りません。
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の存在は知りません。
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の存在を知っています。
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
続きとして、MonoBehaviourを継承していないクラス間でのMVPパターンを
Extenjectと合わせて作ってみようと思ってます。
2021/04/25 追記
やってみました。
【参考リンク】:【Unity(C#)】UniRxとExtenjectでMV(R)Pやってみる
##参考リンク
UnityにおけるMVPパターンについて
Web出身のUnityエンジニアによる大規模ゲームの基盤設計
UniRxでMV(R)Pパターンをやってみた
UniRxとZenjectを使ってMV(R)Pで南京錠を作る
Unityで学ぶMVPパターン ~ UniRxを使って体力Barを作成する ~