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


##参考記事
[[UniRx]購読を停止する](https://qiita.com/yaegaki/items/bea845df0b011d515afb)
[CompositeDisposableにおけるClearとDisposeの挙動](https://qiita.com/Nitudon/items/0d86a4fab3670a2bb5bc)
[UniRx入門 その1](https://qiita.com/toRisouP/items/2f1643e344c741dd94f8)

##環境
Unity 2017.3.1
.NET 4.6(c#6.0)

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

![IncrementDemo.gif](https://qiita-image-store.s3.amazonaws.com/0/150633/74a22466-9faf-59cf-9d9f-766e1446766d.gif)
Button押下毎にStreamを新規で作成していますが、FrameCountが一定である点が今回の重要なポイントです。
インクリ・デクリ終了後に改めてStreamを作成する時は即時である点も重要。

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

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

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

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

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

2.`CompositeDisposable`に`Stream`を`Add()`する。

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

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

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

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

```c#
//frameCountを記憶しておく。
_PreviousFrameCount = Time.frameCount;
```
上記を、`incrementStream`の購読内容に加えることで、破棄した時と直前の`incrementStream`で購読した時のframe数の差異を算出できるようにしておきます。

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

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

```c#

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

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

#サンプルコード

```c#: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);
    }
}
```