Closed7

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

ehime-iyokanehime-iyokan

elapsedTime(経過時間)Modelを作り、データの流れを実装してみる

参考プログラム:#リレーするだけのpresenter を見つつ、試しに以下のようなコードを組んでみた。
参考プログラムはデータの受け渡しと同時に、以下2点も同時に実現しており、初学者の自分としては理解がしづらかったため、できるだけ単純に実装する。

  1. 一部の変数を非公開にして、アクセスを制限すること
  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);
    }
}
ehime-iyokanehime-iyokan

↑の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;
    }
}

ehime-iyokanehime-iyokan

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

追記:NGUI なるものを使っているのか・・・。いったんは UGUI で頑張ってみる。

ehime-iyokanehime-iyokan

下記のコメントのプログラムを ReactiveProperty を使って書き換えて、時間経過によって発火する処理も追加してみた。いい感じ。

View
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";
    }
}
Presenter & Model
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;
    }
}
ehime-iyokanehime-iyokan

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

このスクラップは3ヶ月前にクローズされました