参考記事
[UniRx]購読を停止する
CompositeDisposableにおけるClearとDisposeの挙動
UniRx入門 その1
環境
Unity 2017.3.1
.NET 4.6(c#6.0)
TL;DR
- 途中でインクリメント・デクリメントする最終的な値が変更してもシームレスに動作するプログラムをUniRxで作成した。
- 例えばSTGの加算・最終値が代わり続ける得点や、ギャンブル台の演出によくある残りゲーム・獲得金額のインクリメント(インクリメント処理中もプレイによるG数及びコイン数の減算が発生する)に対応できる。
- 色々良い所はあるけれども、インクリ・デクリ時に音を再生した際に、常に一定間隔で聞こえるのが良い。
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.CompositeDisposable
にStream
をAdd()
する。
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
を保持しておくことです。
//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);
}
これで、frameBetweenObservable
をObservable.TimerFrame
のTimer部分に引数として割り当てれば完成です。
サンプルコード
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);
}
}