Unity
UniRx

良い感じに数値をインクリメント描写してくれる機能をUniRxで作る。

参考記事

[UniRx]購読を停止する
CompositeDisposableにおけるClearとDisposeの挙動
UniRx入門 その1

環境

Unity 2017.3.1
.NET 4.6(c#6.0)

TL;DR

  • 途中でインクリメント・デクリメントする最終的な値が変更してもシームレスに動作するプログラムをUniRxで作成した。
  • 例えばSTGの加算・最終値が代わり続ける得点や、ギャンブル台の演出によくある残りゲーム・獲得金額のインクリメント(インクリメント処理中もプレイによるG数及びコイン数の減算が発生する)に対応できる。
  • 色々良い所はあるけれども、インクリ・デクリ時に音を再生した際に、常に一定間隔で聞こえるのが良い。

IncrementDemo.gif
Button押下毎にStreamを新規で作成していますが、FrameCountが一定である点が今回の重要なポイントです。
インクリ・デクリ終了後に改めてStreamを作成する時は即時である点も重要。

解説

UniRxの基本的な部分については省略して、
- CompositeDisposable
- Stream間のframe間隔の同期
上記2点について解説したいと思います。

1: CompositeDisposable

CompositeDisposableは、複数まとめてDisposeすることのできるクラスです。

CompositeDisposableをDispose先として登録する方法は2種類あります。特に差は無いです。
1.StreamのAddTo()CompositeDisposableを記載する。

CompositeDisposable _Disporsables = new CompositeDisposable();
Observable.EveryUpdate().Subscribe(_ =>
{
//hogehoge
   //_DisporsablesがDisposeしたらこのStreamを終了する。
}).AddTo(_Disporsables);

//Observableが実施中ならばDisposeする。
_Disporsables.Clear();

2.CompositeDisposableStreamAdd()する。

CompositeDisposable _Disporsables = new CompositeDisposable();
var hogehogeStream = Observable.EveryUpdate().Subscribe(_ =>
{
//hogehoge
}).AddTo(this);

_Disposables.Add(hogehogeStream);
//Observableが実施中ならばDisposeする。
_Disporsables.Clear();

CompositeDisposableDispose()する時は、基本的にClear()を使用しましょう。
Dispose()を実施すると、CompositeDisposableが以後使えなくなりますが、Clear()ならば再利用が可能です。
今回は使いまわしているため、Clear()が妥当です。Dispose()は使わないぐらいが良いです。

2: Stream間のframe間隔の同期

実施中のStreamの破棄については解説いたしました。
次は、そのStreamの破棄と次のStreamの生成の間隔をStream自体のIntervalと合わせる方法についてです。

まずは、Stream実施時のTime.frameCountを保持しておくことです。

//frameCountを記憶しておく。
_PreviousFrameCount = Time.frameCount;

上記を、incrementStreamの購読内容に加えることで、破棄した時と直前のincrementStreamで購読した時のframe数の差異を算出できるようにしておきます。

開始時から現在までの累計frame数 - 開始時から直前にincrementStreamを実施した時までの累計frame数

により、直前にincrementStreamを実施した時から現在までの累計frame数を取得します。
あとは_FrameIntervalから上記の値を引くことで、現在から次の発行タイミングまでのframe数を算出することができます。

//incrementStreamで使用するTimer値。初期値は0。
var frameBetweenObservable = 0;
//incrementStreamが実行中だったら、
if (IsIncrementRunning)
{
    //直前にSubscribeした時と今回購読するストリームのタイミングを合わせるための値を算出する。
    frameBetweenObservable = _FrameInterval - (Time.frameCount - _PreviousFrameCount);
}

これで、frameBetweenObservableObservable.TimerFrameのTimer部分に引数として割り当てれば完成です。

サンプルコード

Increment.cs
using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class Increment : MonoBehaviour
{
    /// <summary>
    /// incrementStreamの生殺与奪権を持つ。
    /// </summary>
    CompositeDisposable _Disporsables = new CompositeDisposable();

    /// <summary>
    /// _PlayerHealthの値を変更するためのボタン
    /// </summary>
    [SerializeField]
    Button _ButtonForChangePlayerHealth;

    /// <summary>
    /// 実際のプレイヤーのHP。
    /// </summary>
    [SerializeField]
    IntReactiveProperty _PlayerHealth = new IntReactiveProperty(100);

    /// <summary>
    /// インクリ・デクリを実施する描画用Textコンポーネント
    /// </summary>
    [SerializeField]
    Text _IncrementText;

    /// <summary>
    /// インクリ・デクリを実施する間隔(frame値)
    /// </summary>
    [SerializeField]
    int _FrameInterval = 5;

    /// <summary>
    /// デバッグ用
    /// </summary>
    [SerializeField]
    Text _StatusForDebug;

    // Use this for initialization
    void Start()
    {
        //初期値の代入。
        _IncrementText.text = $"{_PlayerHealth.Value}";
        //_PlayerHealthの値をインクリ・デクリで表示するための、描画用のint値
        var _IntForIncrementText = _PlayerHealth.Value;
        //直前にincrementStreamがSubscribeされた時のFrameCount値
        var _PreviousFrameCount = 0;
        //_PlayerHealth.Subscribe内にあるincrementStreamの実行状態を表すBool値
        var IsIncrementRunning = false;


        //ボタンがクリックされたら購読する。
        _ButtonForChangePlayerHealth.OnClickAsObservable().Subscribe(_ =>
        {
            //0から100までの乱数を_PlayerHealthに代入して発行する。
            _PlayerHealth.Value = Random.Range(0, 101);
        }).AddTo(this);


        //_PlayerHealthの値が変更されたら購読(実施)する。
        _PlayerHealth.Subscribe(playerHealth =>
        {
            //incrementStreamで使用するTimer値。初期値は0。
            var frameBetweenObservable = 0;
            //incrementStreamが実行中だったら、
            if (IsIncrementRunning)
            {
                //直前にSubscribeした時と今回購読するストリームのタイミングを合わせるための値を算出する。
                frameBetweenObservable = _FrameInterval - (Time.frameCount - _PreviousFrameCount);
            }

            //incrementStreamが実施中ならばDisposeする。
            _Disporsables.Clear();


            //frameBetweenObservableだけ待機して_FrameInterval毎に実施する。
            var incrementStream = Observable.TimerFrame(frameBetweenObservable, _FrameInterval)
                //表示用int値とplayerHealthが同値になればDisposeする。
                .TakeWhile(_ => _IntForIncrementText != playerHealth)
                //購読
                .Subscribe(_ =>
                {
                    //Stream実行中フラグをONにする。
                    IsIncrementRunning = true;

                    //プレイヤーのHealthに合わせてインクリ・デクリする。Tween系をしてみるのも面白そう。
                    if (_IntForIncrementText < playerHealth) _IntForIncrementText++;
                    else _IntForIncrementText--;

                    //ここで音を出すと気持ちいい。
                    //_AudioSource.PlayOneShot(_AudioClip);

                    //インクリ・デクリした値を画面上のテキストに反映する。
                    _IncrementText.text = $"{_IntForIncrementText}";

                    //デバッグ用
                    _StatusForDebug.text = $"Player Health:{playerHealth}\n"
                        + $"FrameCount:{Time.frameCount},-:{Time.frameCount - _PreviousFrameCount},"
                        + $"%:{Time.frameCount % _FrameInterval}\n"
                        + $"FrameBetweenObservable:{frameBetweenObservable}";

                    //frameCountを記憶しておく。
                    _PreviousFrameCount = Time.frameCount;
                }
                //完了したらTimer機能をOff(待ち時間を0)にする。
                , () => IsIncrementRunning = false)
                .AddTo(this);

            //_DisporsablesにincrementStreamを登録する。
            _Disporsables.Add(incrementStream);

        }).AddTo(this);
    }
}