UniTaskとUniRxで、待ちの処理を実装する(後編:UniRx)
前編では、非同期処理のライブラリとしてUniTaskを軽く解説しました
UniTaskとUniRxで、待ちの処理を実装する(前編:UniTask) - Aizu-Progressive xr Lab blog
今回はUniRxというものを解説します
UniRxに関しては文章で説明するより、コードを見せる方が早そうです
void Start() { var count = 0; this.UpdateAsObservable() .Subscribe(_ => { if (Input.GetKeyDown(KeyCode.Space)) { print(count++); } }); }
はい
このコードだけで、スペースを押すごとにその回数を出力できます
フィールドに値を保持する必要がありません
正確にはライブラリの目的は、イベントハンドリングであり、イベントという概念をうまく扱う為のものですが
学習コストが高いので詳しくは触れない事にします
UniTaskと組み合わせるととても便利な部分と、UI等の処理には使えるかなという部分を紹介します
UniRx
まずUniRxの導入について
UniRxはAssetStoreにあるのでそちらから
UniRx - Reactive Extensions for Unity - Asset Store
コードで使うときは次の物が必要です
using UniRx; using UniRx.Triggers;
最初に見せたコードの事もそれなのですが
取り敢えず、好きなところでUpdate処理を書ける事を見せます
this.UpdateAsObservable() .Where(_ => Input.GetKeyDown(KeyCode.Space)) // フィルタ .Subscribe(_ => { print("スペースキーが押された"); });
ここでのthisは、自分自身・・・書いているコンポーネントのクラスの事ですね
そこから、UpdateAsObservable()というものを取得できて、Subscribeという物に、ラムダ式でUpdate処理を書くことになります
Whereという物を使うと、条件がtrueのときのみSubscribeの処理を実行できます
ここのラムダ式では、引数は存在するのですが、
値が無いを意味するUnit型という物であるため、
使わないので_
にしています
他にも
this.OnCollisionEnterAsObservable() .Subscribe(collision => { var renderer = collision.collider.GetComponent<Renderer>(); if (renderer != null) { renderer.enabled = false; } });
これで、OnCollisionEnter時にぶつかったColliderにRendererがあれば、表示を消す処理
this.OnCollisionStayAsObservable(); this.OnCollisionExitAsObservable(); this.OnTriggerEnterAsObservable(); this.OnTriggerStayAsObservable(); this.OnTriggerExitAsObservable();
これらのように、UniRxによって、
Updateの他にも一通りの
「決まったメソッドを定義して書かなければならないもの」
を任意の位置で書くことができるようになります
UniTaskと組み合わせる
ではUniTaskと組み合わせたら何ができるのか
見ていきましょう
いきなりちょっと複雑な例になりますが
例えば、
ボタンを押しているかつゲージが0より大きい場合にとある動作をし続けるというのがあるとします
そして、ゲージが0になったら動作をやめて、ゲージが回復し始めます
ゲージはUIとして表示されなくとも
ゲームの走る動作なんかはそういうのがよくありますよね
なので走る動作として実装してみます
using System; using System.Threading; using System.Collections; using System.Collections.Generic; using UnityEngine; using UniRx; using UniRx.Triggers; using UniRx.Async; using UniRx.Async.Triggers; public class Player : MonoBehaviour { public float walkSpeed; public float runSpeed; public float statminaRecoverySpeed; public float staminaDecreaseSpeed; public float staminaMax; public float stamina; [NonSerialized] public float currentSpeed; void Start() { Run(this.GetCancellationTokenOnDestroy()).Forget(); // 初期化 stamina = staminaMax; currentSpeed = walkSpeed; // 移動処理の登録(前後のみ) this.UpdateAsObservable() // UniRx .Subscribe(_ => { Vector3 moveDir = Vector3.zero; if (Input.GetKey(KeyCode.W)) { moveDir += Vector3.forward; } if (Input.GetKey(KeyCode.D)) { moveDir += Vector3.back; } transform.position += moveDir * currentSpeed * Time.deltaTime; // スタミナの回復は常にしている stamina += statminaRecoverySpeed * Time.deltaTime; if (stamina > staminaMax) stamina = staminaMax; }); } async UniTask Run(CancellationToken token) { // 走り始めの条件判定 await UniTask.WaitUntil(() => { var staminaRate = stamina / staminaMax; var b1 = Input.GetKeyDown(KeyCode.LeftShift); var b2 = staminaRate > 0.3f; return b1 && b2; }, cancellationToken: token); // 走り始める currentSpeed = runSpeed; var disposable = // スタミナ減少処理 this.UpdateAsObservable() // UniRx .Where(_ => Input.GetKey(KeyCode.W)) .Subscribe(_ => stamina -= staminaDecreaseSpeed * Time.deltaTime); // SubscribeしたのをCancellationTokenでCancelしたときに止めたいならこう書く token.Register(() => disposable.Dispose()); // 止まる条件判定 await UniTask.WaitUntil(() => { var b1 = Input.GetKeyUp(KeyCode.LeftShift) || Input.GetKeyUp(KeyCode.W); var b2 = stamina < 0f; return b1 || b2; }, cancellationToken: token); // 止まる disposable.Dispose(); // スタミナ減少処理の停止 currentSpeed = walkSpeed; if (stamina < 0f) stamina = 0f; // 繰り返す Run(token).Forget(); } }
このコードをGameObjectにアタッチして、
このような感じで値を設定すると、
WキーとShiftキーを押すと、移動速度が上がり、
スタミナが無くなるか、WキーやShiftキーが離されれば移動速度が下がるところが確認できます
Staminaも正しく上がったり下がったりするのが確認できます
UniRxを使ったところは2か所ありますが、
二つ目のところでdisposable
という物を取得しています。
このdisposable
をDispose
することで、登録した処理を止めることが出来ます
このように、UniTaskとUniRxを組み合わせることで、
async/awaitの非同期的な処理によって、Update動作を始めたり止めたりできるので
複雑そうな処理を割と簡単に書く事ができました
UIの更新
UniRxにある、ReactivePropertyというものを使うと
主にUI等の更新処理を書けます
テキストの更新は毎フレーム行うと、メモリの使い方が悪くなりますから
public FloatReactiveProperty hp; public Text text; void Start() { this.UpdateAsObservable() .Subscribe(_ => hp.Value += Time.deltaTime); // hpを増やすとする // hpの更新処理。hpの値が変更されたら処理が実行される // AddToを付けないとメモリに悪いので一応付けた方が良い hp.Subscribe(v => text.text = v.ToString()).AddTo(this); }
UniRxについてとアニメーション実装してみた
ここからちょっと難易度が上がります
需要があるかはわからないですがUniRxとUniTaskを用いて、ちょっとスクリプトのみでアニメーションを実装してみます
まず先に、
UniRxの型について解説します
UniRxではIObservable
という型が重要です
this.UpdateAsObservable()
の型や
this.OnTriggerEnterAsObservable()
の型は
それぞれ、
IObservable<Unit>
と
IObservable<Collision>
であり、
これらの型はWhere
やSubscribe
を使うことができます
Where
やSubscribe
時のラムダ式の引数の型はIObservable<T>
のT
の部分の型です
UniRxを使うと、次のような処理も楽に書けます
コルーチンが前提知識になるので知らない人は調べて欲しいかなってところですが
分からないなら飛ばしても構いません
好きなところから、指定した時間毎フレーム経過時間と指定した時間の割合を取って何か処理をしたい場合
void Start() { // コルーチンに渡したobserverから流された値が流れてくる // oはobserver Observable.FromCorountine<float>(o => Timer(3f, o)) .Subscribe(t => print(t)).AddTo(this); } IEnumerator Timer(float actTime, IObserver<float> observer) { var sTime = Time.time; // 最初の時間 while(true) { var rate = (Time.time - sTime) / actTime; // 経過時間 / 指定した時間 if (rate > 1f) break; // 割合が1より大きいなら終了 observer.OnNext(rate); // 割合を送る yield return null; } observer.OnNext(1f); // 最後に1だけ送る observer.OnCompleted(); // Observableを終了させる }
これで、3秒間0から1までの値を出力してくれます
ここでは、コルーチンに渡されたobserverに
OnNextで経過時間の割合を流します
FromCorountineの型はIObservable
IObserver
Subscribe時に流した値を使うことが出来ます
ここでは、経過時間の割合が出力されますね
OnCompletedすると、Observable側で値が流れるのが終了されたというのを検知できます
AddToしているのは、コンポーネントがDestroyしたときに処理をキャンセルする為です
UniTaskと組み合わせて、
アニメーションさせてみます
using System; using System.Threading; using System.Collections; using System.Collections.Generic; using UnityEngine; using UniRx; using UniRx.Triggers; using UniRx.Async; using UniRx.Async.Triggers; public class SphereAnimation : MonoBehaviour { void Start() { PlayAnimation(this.GetCancellationTokenOnDestroy()).Forget(); } async UniTask PlayAnimation(CancellationToken token) { await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space) , cancellationToken: token); await Animation(token); PlayAnimation(token).Forget(); } async UniTask Animation(CancellationToken token) { IObservable<float> timer; IDisposable disposable; // 弾む、1回目 timer = Observable.FromCoroutine<float>(o => Timer(0.5f, o)); disposable = timer.Subscribe(t => transform.position = Vector3.up * (-(8f*t*t) + (8f*t))); // 放物線 token.Register(() => disposable.Dispose()); await timer.ToUniTask(token); // 弾む、2回目 // 同じObservableの場合、設定しなおさなくていい(例外あり) disposable = timer.Subscribe(t => transform.position = Vector3.up * (-(8f*t*t) + (8f*t))); token.Register(() => disposable.Dispose()); await timer.ToUniTask(token); // 1回転 disposable = timer.Subscribe(t => transform.position = new Vector3( Mathf.Sin(Mathf.PI * 2f * t), 0f, Mathf.Cos(Mathf.PI * 2f * t) )); token.Register(() => disposable.Dispose()); await timer.ToUniTask(token); // 早く弾む disposable = timer.Subscribe(t => transform.position = Vector3.up * (-Mathf.Sin(Mathf.PI * t * 4f) + 1f)); token.Register(() => disposable.Dispose()); await timer.ToUniTask(token); } IEnumerator Timer(float actTime, IObserver<float> observer) { var sTime = Time.time; while(true) { var rate = (Time.time - sTime) / actTime; if (rate > 1f) break; observer.OnNext(rate); yield return null; } observer.OnNext(1f); observer.OnCompleted(); } }
このコードを球にアタッチして
シーンを再生してSpaceキーを押すと
次のアニメーションが動きます
最後の例はずいぶん難しいかもしれませんが、
UniTaskとUniRxを組み合わせると結構良い感じにコードが書ける事がわかったと思います
最後に
UniRxは基本的にはイベントハンドリングというのが目的で扱う物です
一応、UniRxはいろんな書き方ができてしまい、
UniRxで非同期処理もできなくはないのですが、
UniRxは非同期処理の為に使ってはいけません
(↑これは僕の経験談でもある)
"待ち"が入る非同期処理はUniTask
動作を登録したいときはUniRx
のような使い分けが大事です
それを分かったうえで、UniRxを学習するのも悪くは無いでしょう
とりさんの記事とかで、しっかり学べます
UniRx入門 その1 - Qiita