Unity で UI周りの実装が複雑になるため、設計パターンを試してみる

UniRx を利用した MVP パターンが良さそう。
参考:
-
https://qiita.com/wolfmagnate/items/4b1a1d0cf8db7ff7d66f
- MVP 実装例(分かりやすそう)
- サンプルが難しい上手く落とし込んでみる
- elapsedTime(経過時間)Modelを作ってみる
- 時間に応じて増えるスコアボードを作ってみる
- https://qiita.com/toRisouP/items/5365936fc14c7e7eabf9

elapsedTime(経過時間)Modelを作り、データの流れを実装してみる
参考プログラム:#リレーするだけのpresenter を見つつ、試しに以下のようなコードを組んでみた。
参考プログラムはデータの受け渡しと同時に、以下2点も同時に実現しており、初学者の自分としては理解がしづらかったため、できるだけ単純に実装する。
- 一部の変数を非公開にして、アクセスを制限すること
- 一部の型を静的に定義して、どこからでも(というよりはインスタンスを介さずに?)アクセスできるようすること
作成したコード1
値の受け渡し部分を実装してみる。
using System;
using UniRx;
using UnityEngine;
public class SampleView : MonoBehaviour
{
private TimerPresenter presenter;
void Start()
{
presenter = new TimerPresenter();
presenter._Subject.Subscribe(value =>
{
Debug.Log($"Timer value: {value}");
});
}
}
public class TimerModel
{
private int value = 0;
public Subject<int> _Subject = new Subject<int>();
public TimerModel()
{
Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ =>
{
value++;
_Subject.OnNext(value);
});
}
}
public class TimerPresenter
{
private TimerModel model = new TimerModel();
public Subject<int> _Subject = new Subject<int>();
public TimerPresenter()
{
model._Subject.Subscribe(value =>
{
_Subject.OnNext(value);
});
}
}
補足として、presenter を 定義と同時に初期化するとエラーが発生する。
Start() より前に presenter が初期化され、他にUnityのオブジェクトが null の状態となるため?
Exception: UniRx requires a MainThreadDispatcher component created on the main thread. Make sure it is added to the scene before calling UniRx from a worker thread.
NullReferenceException: Object reference not set to an instance of an object
SampleView.Start () (at Assets/Scripts/SampleView.cs:10)
作成したコード2
一部の変数を非公開にして、アクセスを制限してみる
using System;
using UniRx;
using UnityEngine;
public class SampleView : MonoBehaviour
{
private TimerPresenter presenter;
void Start()
{
presenter = new TimerPresenter();
presenter._Obserber.Subscribe(value =>
{
Debug.Log($"Timer value: {value}");
});
}
}
public class TimerModel
{
private int value = 0;
private Subject<int> _Subject = new Subject<int>();
public IObservable<int> _Obserber => _Subject;
public TimerModel()
{
Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ =>
{
value++;
_Subject.OnNext(value);
});
}
}
public class TimerPresenter
{
private TimerModel model = new TimerModel();
private Subject<int> _Subject = new Subject<int>();
public IObservable<int> _Obserber => _Subject;
public TimerPresenter()
{
model._Obserber.Subscribe(value => _Subject.OnNext(value));
}
}
作成したコード3
Modelを静的に定義してみる。
プログラムの構成・変数名等も少し更新。
using System;
using TMPro;
using UniRx;
using UnityEngine;
public class SampleView : MonoBehaviour
{
private TimerPresenter presenter;
// UI -> TextMeshPro をインスペクター上で設定する
public TextMeshProUGUI timerText;
void Start()
{
presenter = new TimerPresenter();
presenter.TimerValueStream.Subscribe(value =>
{
timerText.text = $"Timer value: {value}";
});
}
}
public class TimerModel
{
private int value = 0;
private Subject<int> timerValueSubject = new Subject<int>();
public IObservable<int> TimerValueStream => timerValueSubject;
public TimerModel()
{
Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ =>
{
value++;
timerValueSubject.OnNext(value);
});
}
}
public static class Model
{
static TimerModel _timerModel;
public static TimerModel TimerModel => _timerModel;
static Model()
{
_timerModel = new TimerModel();
}
}
public class TimerPresenter
{
private Subject<int> timerValueSubject = new Subject<int>();
public IObservable<int> TimerValueStream => timerValueSubject;
public TimerPresenter()
{
Model.TimerModel.TimerValueStream.Subscribe(SendValue);
}
void SendValue (int value)
{
timerValueSubject.OnNext(value);
}
}

↑のelapsedTime(経過時間)Modelの例は MVP っぽくないように思うので、別途作り直してみる。
下記の通り、良い感じに Model View Presenter を分離することができた。
ただし、Subject がどこからでも値発行(OnNext)出来るので、アクセスを制限する等をしたい。
using System;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
public class SampleView : MonoBehaviour
{
// UI -> TextMeshPro をインスペクター上で設定する
public TextMeshProUGUI LightText;
public Button button;
public LightPresenter LightPresenter;
void Start()
{
LightPresenter = new LightPresenter();
button.onClick.AddListener(() =>
{
LightPresenter.InputSubject.OnNext(Unit.Default);
}
);
LightPresenter.OutputStream.Subscribe(isOn =>
{
if (isOn) {
LightText.text = "ON";
} else
{
LightText.text = "OFF";
}
}
);
}
}
public class LightPresenter
{
public Subject<Unit> InputSubject = new Subject<Unit>();
public Subject<bool> OutputSubject = new Subject<bool>();
public IObservable<Unit> InputStream => InputSubject;
public IObservable<bool> OutputStream => OutputSubject;
public LightModel LightModel;
public LightPresenter()
{
LightModel = new LightModel();
InputStream.Subscribe(_ =>
{
LightModel.Toggle();
OutputSubject.OnNext(LightModel.GetState());
}
);
}
}
public class LightModel
{
private bool IsON = false;
public void Toggle()
{
IsON = !IsON;
}
public bool GetState()
{
return IsON;
}
}

↑について、下記の記事では View は Presenter を知らないようにしてるみたい。
最初に作成したプログラムみたいに Model 側で Reactive な変数を宣言しているが、Editorテストはできるのかな?
自分的にはしっくりきたので、こちらの設計パターンを利用させてもらおうと思う。
追記:NGUI なるものを使っているのか・・・。いったんは UGUI で頑張ってみる。

下記のコメントのプログラムを ReactiveProperty を使って書き換えて、時間経過によって発火する処理も追加してみた。いい感じ。
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class LightView : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI lightText;
[SerializeField] private Button _toggleButton;
public Button ToggleButton
{
get { return _toggleButton; }
}
/// <summary>
/// lightText の表示を更新する。
/// </summary>
/// <param name="isON"></param>
public void LightTextUpdate(bool isON)
{
lightText.text = isON ? "ON" : "OFF";
}
}
using System;
using UniRx;
using UnityEngine;
public class LightPresenter: MonoBehaviour
{
[SerializeField] private LightView _view;
private LightModel _model;
void Start()
{
Initialize();
}
public void Initialize()
{
_model = new LightModel();
bind();
setEvents();
}
/// <summary>
/// ReactiveProperty を View にバインドする。
/// </summary>
private void bind()
{
_model.IsON.Subscribe(_view.LightTextUpdate);
}
/// <summary>
/// Model への 影響を与える Event を設定する。
/// </summary>
private void setEvents ()
{
_view.ToggleButton.onClick.AddListener(onToggleButtonClicked);
Observable.Interval(TimeSpan.FromSeconds(3f))
.Subscribe(_ => _model.Toggle()).AddTo(this);
}
private void onToggleButtonClicked()
{
_model.Toggle();
}
}
public class LightModel
{
private ReactiveProperty<bool> _isON = new ReactiveProperty<bool>();
public IReadOnlyReactiveProperty<bool> IsON => _isON;
public void SetLight(bool isOn)
{
_isON.Value = isOn;
}
public void Toggle()
{
_isON.Value = !_isON.Value;
}
}

下記リンクのコメントにて、MVP が上手く分かれたサンプルプログラムを作成した。
今後はこちらのプログラムをテンプレートとして設計を実施し、特に問題が発生しなければ、本スクラップを閉じる予定。

新たにサンプルコードを作成した。
Model View Presenter の構成的には問題なさそう。
複雑になっていた設計がある程度固まってきたので、クローズする。