【Unity】UniRx【1】 #84
前回の成果
ソーベルフィルタについて学んだ。
今回やること
UniRxについて学んでいきます。
以下のサイト様の手順に習って学んでいきます。
事前準備
アセットストアからUniRxをインポートします。
UniRxとは
ReactiveExtensionsをUnity向けに開発したライブラリとなっています。
ReactiveExtensionsとは、適当な値を0回以上通知するC#のeventや非同期処理といったものを統一的なプログラミングモデルで扱えるようにしたものとなります。
またこちらはデザインパターンのObserverパターンがベースになっております。
Observerパターン
簡単にまとめると、あるオブジェクトの状態が変化した際に、そのオブジェクト自身が観察者に状態の変化を通知するものです。
Observerパターンのクラス図
qiita.com:より引用
C#のevent
C#の標準機能として、Observerとしてよく使用されるeventがあります。
そちらと比較してみます。
Script
カウントダウンクラス
using System.Collections; using UnityEngine; /// <summary> /// カウントダウンクラス /// </summary> public class TimeCounterEvent : MonoBehaviour { // イベントハンドラ public delegate void TimerEventHandler(int time); // イベント public event TimerEventHandler OnTimeChanged; void Start() { StartCoroutine(TimerCoroutine()); } /// <summary> /// 時間を計測する /// </summary> /// <returns></returns> IEnumerator TimerCoroutine() { int time = 100; while (time > 0) { time--; // イベント通知 OnTimeChanged(time); yield return new WaitForSeconds(1); } } }
時間表示クラス
using UnityEngine; using UnityEngine.UI; /// <summary> /// 時間表示クラス /// </summary> public class TimerViewEvent : MonoBehaviour { [SerializeField] private TimeCounterEvent _timeCounter; [SerializeField] private Text _counterText; void Start() { // 通知が来たら、Textをtimeの値で更新 _timeCounter.OnTimeChanged += (time) => { _counterText.text = time.ToString(); }; } }
Unity上
Hierarchy
TimeCounter
TimeView
実行結果
結果としては、1秒ごとに1ずつ減らした値をTextに反映するものになります。
UniRxでのタイマー
では、UniRxで同じことを実装してみます。
Script
カウントダウンクラス
using System.Collections; using UnityEngine; using UniRx; using System; /// <summary> /// カウントダウンクラス /// </summary> public class TimeCounterUniRx : MonoBehaviour { // イベント発行のインスタンス public Subject<int> timerSubject = new Subject<int>(); // イベントの購読側 public IObservable<int> OnTimeChanged { get { return timerSubject; } } void Start() { StartCoroutine(TimerCoroutine()); } /// <summary> /// 時間を計測する /// </summary> /// <returns></returns> IEnumerator TimerCoroutine() { int time = 100; while (time > 0) { time--; // イベント発行 timerSubject.OnNext(time); yield return new WaitForSeconds(1); } } }
時間表示クラス
using UnityEngine; using UnityEngine.UI; using UniRx; using System; /// <summary> /// 時間表示クラス /// </summary> public class TimerViewUniRx : MonoBehaviour { [SerializeField] private TimeCounterUniRx _timeCounter; [SerializeField] private Text _counterText; void Awake() { // 通知が来たら、Textをtimeの値で更新 _timeCounter.OnTimeChanged.Subscribe(time => { _counterText.text = time.ToString(); }); } }
処理は大きく変わっていません。
eventの代わりにSubjectが呼ばれているのがわかるかと思います。
もちろん結果は同じとなります。
eventとUniRxでの比較
Subjectがイベントの中心となり、OnNextで値を渡し、Subscribeで値を受け取ることが出来ます。
OnNext、Subscribe
上で使用した、OnNextとSubscribeについて学びます。
各メソッドの説明は以下となります。
メソッド名 | 挙動 |
---|---|
Subscribe | メッセージを受け取った際に実行する処理を登録 |
OnNext | Subscribeに登録された処理にメッセージを渡して実行 |
簡単にログを出してみます。
Logを出すクラス
using UnityEngine; using UniRx; using System; /// <summary> /// SubscribeとOnNextをLogで確認するクラス /// </summary> public class SubscribeOnNextLog : MonoBehaviour { // イベント発行 private Subject<string> logSubject = new Subject<string>(); void Start() { // イベント実行(ログに出ない) logSubject.OnNext("foo"); // イベント登録 logSubject.Subscribe(message => { Debug.Log("1回目のSubscribe : " + message); }); logSubject.Subscribe(message => { Debug.Log("2回目のSubscribe : " + message); }); logSubject.Subscribe(message => { Debug.Log("3回目のSubscribe : " + message); }); // イベント実行 logSubject.OnNext("Hoge"); logSubject.OnNext("Fuga"); } }
実行結果
以下の実行結果を見て頂けるとわかるように、Subscribeで登録した処理をOnNextで渡した値を用いて順番に処理しています。
fooはSubscribeで登録する前に呼ばれたため、ログとして出ません。
IObserverインターフェースとIObservableインターフェース
Subjectには、OnNext、Subscribeの2つのメソッドがあると説明しました。
こちらの説明は間違っていないのですが、
詳しくは、SubjectはIObserverインターフェースとIObservableインターフェースの2つを実装しています。
その2つのインターフェースについて掘り下げていきます。
IObserverインターフェース
メッセージを発行するインターフェースとなっています。
定義は以下となります。
IObserverインターフェースの定義
// defined from .NET Framework 4.0 and NETFX_CORE #if !(NETFX_CORE || NET_4_6 || NET_STANDARD_2_0 || UNITY_WSA_10_0) using System; namespace UniRx { public interface IObserver<T> { void OnCompleted(); void OnError(Exception error); void OnNext(T value); } } #endif
OnNextは先程説明しましたが、それも含めて定義されている3つのメソッドの説明は以下となります。
メソッド名 | 挙動 |
---|---|
OnCompleted | メッセージの発行が完了したことを通知 |
OnError | エラーを通知するメッセージを発行 |
OnNext | メッセージを渡して実行 |
IObservableインターフェース
イベントメッセージを購読できるインターフェースとなっています。
定義は以下となります。
IObservableインターフェースの定義
// defined from .NET Framework 4.0 and NETFX_CORE using System; #if !(NETFX_CORE || NET_4_6 || NET_STANDARD_2_0 || UNITY_WSA_10_0) namespace UniRx { public interface IObservable<T> { IDisposable Subscribe(IObserver<T> observer); } } #endif
こちらはSubscribeメソッドのみとなります。
補足
Subjectメソッドを使用する際に、定義ではIObserver
// 定義 public interface IObservable<T> { IDisposable Subscribe(IObserver<T> observer); } // 呼び出し Subject.Subscribe(message => { Debug.Log(message); });
これは、IObserverbleにActionを引数とする関数が定義されているためです。
ですので、使用する際は特にIObserver
定義済メソッド
public static IDisposable Subscribe<T>(this IObservable<T> source) { return source.Subscribe(UniRx.InternalUtil.ThrowObserver<T>.Instance); } public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext) { return source.Subscribe(Observer.CreateSubscribeObserver(onNext, Stubs.Throw, Stubs.Nop)); } public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action<Exception> onError) { return source.Subscribe(Observer.CreateSubscribeObserver(onNext, onError, Stubs.Nop)); } public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action onCompleted) { return source.Subscribe(Observer.CreateSubscribeObserver(onNext, Stubs.Throw, onCompleted)); } public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext, Action<Exception> onError, Action onCompleted) { return source.Subscribe(Observer.CreateSubscribeObserver(onNext, onError, onCompleted)); }
オペレータ
SubjectとSubscribeの間でメッセージを処理する部分のことをオペレータと呼びます。
Whereオペレータ
例えば、以下のようなコードがあるとします。
衝突ログコード
using UnityEngine; using UniRx; using System; public class WhereLog : MonoBehaviour { // イベント発行 private Subject<string> logSubject = new Subject<string>(); void Start() { // イベント登録 logSubject.Subscribe(message => { Debug.Log(message + "と衝突"); }); // イベント実行 logSubject.OnNext("敵"); logSubject.OnNext("味方"); logSubject.OnNext("敵"); logSubject.OnNext("味方"); } }
結果
これを敵との衝突判定をしたい場合、以下のような書き方もできます。
if文
logSubject.Subscribe(message => { if (message == "敵") { Debug.Log(message + "と衝突"); } });
ですが、UniRxですとifを使わずにWhereというオペレータを使用して同じことができます。
Whereを使用
using UnityEngine; using UniRx; using System; public class WhereLog : MonoBehaviour { private Subject<string> logSubject = new Subject<string>(); void Start() { // イベント登録 logSubject .Where(message => message == "敵") .Subscribe(message => { Debug.Log(message + "と衝突"); }); // イベント実行 logSubject.OnNext("敵"); logSubject.OnNext("味方"); logSubject.OnNext("敵"); logSubject.OnNext("味方"); } }
結果
敵と衝突した時のみログを出すようにできました。
Whereはフィルタリングするオペレータでしたが、他にも様々な働きをするオペレータがあるので詳しくは以下サイト様を参考にしてください。
ストリーム
SubjectをSubscribeすることや、Subjectにオペレータを挟んでSubscribeするといった
メッセージが発行されてからSubscribeに到達する処理の流れのことをストリームと呼びます。
今回は以上となります。
ここまでご視聴ありがとうございました。