知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【Unity】UniRx【1】 #84

前回の成果

ソーベルフィルタについて学んだ。

soramamenatan.hatenablog.com


今回やること

UniRxについて学んでいきます。
以下のサイト様の手順に習って学んでいきます。

qiita.com


事前準備

アセットストアからUniRxをインポートします。

assetstore.unity.com


UniRxとは

ReactiveExtensionsをUnity向けに開発したライブラリとなっています。
ReactiveExtensionsとは、適当な値を0回以上通知するC#のeventや非同期処理といったものを統一的なプログラミングモデルで扱えるようにしたものとなります。
またこちらはデザインパターンのObserverパターンがベースになっております。

Observerパターン

簡単にまとめると、あるオブジェクトの状態が変化した際に、そのオブジェクト自身が観察者に状態の変化を通知するものです。

Observerパターンのクラス図

f:id:soramamenatan:20201212045129p:plain

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

f:id:soramamenatan:20201212052643p:plain

TimeCounter

f:id:soramamenatan:20201212052640p:plain

TimeView

f:id:soramamenatan:20201212052646p:plain

実行結果

結果としては、1秒ごとに1ずつ減らした値をTextに反映するものになります。

f:id:soramamenatan:20201212052757g:plain


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での比較

f:id:soramamenatan:20201212133739g:plain


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で登録する前に呼ばれたため、ログとして出ません。

f:id:soramamenatan:20201212135306p:plain


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("味方");
    }
}
結果

f:id:soramamenatan:20201212152050p:plain

これを敵との衝突判定をしたい場合、以下のような書き方もできます。

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("味方");
    }
}
結果

敵と衝突した時のみログを出すようにできました。

f:id:soramamenatan:20201212152752p:plain


Whereはフィルタリングするオペレータでしたが、他にも様々な働きをするオペレータがあるので詳しくは以下サイト様を参考にしてください。

qiita.com


ストリーム

SubjectをSubscribeすることや、Subjectにオペレータを挟んでSubscribeするといった
メッセージが発行されてからSubscribeに到達する処理の流れのことをストリームと呼びます。

今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

qiita.com