✌️

UniRx + MVP でサンプルコードを作成してみた

に公開

やること

MVP で切り分けつつ、
経過時間に応じてスコアを上昇させ、それを UI として表示するコードを作成してみる

結果

思った通りのものが実装できた。
経過時間に応じて、スコアを上昇させ、ボーナスタイムはスコアが2倍になる。

追記(2025/05/25):今回作成したプログラムの実装は全体的に MVVM 的だと ChatGPT 先生に指摘されてしまった・・・。
色々試して、MVP 寄りにしてみる予定。=> 実施済

背景

UI 関係をどのように設計していけばよいのか分からず途方に暮れていたところ、下記の「参考記事」を見つけた。

しかし、記事では MVP に切り分けつつ実装することが解説されていたものの、初学者である自分的には処理の流れを追うのが難しく、理解が難航していた。
そこで一部自分でサンプルを作って理解を深めようと思った。

やってみる

経過時間に合わせてスコアが上昇するプログラムを実装してみる

作るうえで、右往左往した内容は「参考スクラップ」参照。
以下のスクリプトをテキトーな GameObject にアタッチし、TextMeshPro をインスペクター上で設定すると動きます。

追記(2025/05/24):BehaviourSubject(bounusTimeSubject)は値をbool値バインディングしているため、「Model」というよりは「ViewModel」になりそう。
よって、「MVP」と「MVVM」が混在しているため、どちらかに統一したほうが良さそう。

作成したコード
using System;
using TMPro;
using UniRx;
using UnityEngine;

public class SampleView : MonoBehaviour
{
    private Presenter Presenter;

    // UI -> TextMeshPro をインスペクター上で設定する
    public TextMeshProUGUI timerText;
    public TextMeshProUGUI scoreText;
    void Start()
    {
        Presenter = new Presenter();

        Presenter.TimeValueStream.Subscribe(timeValue =>
        {
            timerText.text = $"Time  : {timeValue}";
        });
        Presenter.ScoreValueStream.Subscribe(scoreValue =>
        {
            scoreText.text = $"Score : {scoreValue}";
        });
        Presenter.BounusTimeStream.Subscribe(bounusTime =>
        {
            if (bounusTime)
            {
                scoreText.color = Color.red;
            }
            else
            {
                scoreText.color = Color.white;
            }
        });
    }
}
public class Presenter
{
    private Subject<int> timeValueSubject = new Subject<int>();
    private Subject<int> scoreValueSubject = new Subject<int>();
    private Subject<bool> bounusTimeSubject = new Subject<bool>();
    public IObservable<int> TimeValueStream => timeValueSubject;
    public IObservable<int> ScoreValueStream => scoreValueSubject;
    public IObservable<bool> BounusTimeStream => bounusTimeSubject;

    public Presenter()
    {
        Model.ElapsedTime.TimeValueStream.Subscribe(
            timeValue => timeValueSubject.OnNext(timeValue)
        );
        Model.Score.ScoreValueStream.Subscribe(
            scoreValue => scoreValueSubject.OnNext(scoreValue)
        );
        Model.Score.BounusTimeStream.Subscribe(
            bounusTime => bounusTimeSubject.OnNext(bounusTime)
        );
    }
}
public static class Model
{
    static ElapsedTime _elapsedTime;
    static Score _score;
    public static ElapsedTime ElapsedTime => _elapsedTime;
    public static Score Score => _score;

    static Model()
    {
        _elapsedTime = new ElapsedTime();
        _score = new Score();
    }
}
public class ElapsedTime
{
    private int value = 0;
    private Subject<int> timeValueSubject = new Subject<int>();
    public IObservable<int> TimeValueStream => timeValueSubject;
    public ElapsedTime()
    {
        Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ =>
        {
            value++;
            timeValueSubject.OnNext(value);
        });

    }
}
public class Score
{
    private int score = 0;
    private Subject<int> scoreValueSubject = new Subject<int>();
    private BehaviorSubject<bool> bounusTimeSubject = new BehaviorSubject<bool>(false);
    public IObservable<bool> BounusTimeStream => bounusTimeSubject;
    public IObservable<int> ScoreValueStream => scoreValueSubject;
    public Score()
    {
        const int bounusTime = 5;
        const int timeOffset = 1;

        // 5秒ごとにボーナスタイムを切り替えたいが、4秒で切り替わってしまうため offset を追加
        Observable.Timer(TimeSpan.FromSeconds(bounusTime + timeOffset), TimeSpan.FromSeconds(bounusTime))
            .Subscribe(_ =>
            {
                bool current = bounusTimeSubject.Value;
                if (!current)
                {
                    // BehaviorSubjectについての補足:OnNext に入れた値は、BehaviorSubject.Value に反映される
                    bounusTimeSubject.OnNext(IsBounusTime(3));
                }
                else
                {
                    // 連続でボーナスタイムが発生しないようにする
                    bounusTimeSubject.OnNext(false);
                }
            }
        );
        Observable.Interval(TimeSpan.FromSeconds(1))
            .WithLatestFrom(bounusTimeSubject, (_, isBounus) => isBounus)
            .Subscribe(isBounus =>
            {
                int points = 0;
                if (isBounus)
                {
                    points = 200;
                }
                else
                {
                    points = 100;
                }
                AddScore(points);

                scoreValueSubject.OnNext(score);
            }
        );
    }
    public void AddScore (int points)
    {
        score += points;
    }
    // 1 / N の確率で True を返す。
    public bool IsBounusTime (int N)
    {
        if (UnityEngine.Random.Range(0, N) == 0)
        {
            return true;
        } else
        {
            return false;
        }
    }
}

メモ

追記(2025/05/24):MVP/MVVM について、P(Presenter) と VM(ViewModel)の違いは以下?

  • ボタンを押した場合の機能を実現する例
    • Presenter
      • View への操作(ボタンを押下)に対して、Presenter を介して Model へ反映する
      • Model の動作を Presenter を介して、View へ反映する
    • ViewModel
      • View への操作(ボタンを押下)に対して、ViewModel のデータを UI で扱っているデータへバインドし、ModelView を介して Model へ反映する
      • Model の値は ViewModel で UI へバインドしているため、自動で反映される
  • 参考

Discussion