知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【Unity】UniRx【3】 #86

前回の成果

IObserverインターフェースとストリームの寿命について学んだ。

soramamenatan.hatenablog.com


今回やること

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

qiita.com


事前準備

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

assetstore.unity.com


ストリームのソース

UniRxのストリームは以下の3つによって構成されています。

  • メッセージを発行するソース
  • メッセージを伝えるオペレータ
  • メッセージを購読する
構成例
// メッセージを発行するソース
Subject<string> subject = new Subject<string>();
subject
    // メッセージを伝えるオペレータ
    .Select(str => int.Parse(str))
    // メッセージを購読する
    .Subscribe(x => {
    });


今回は、「メッセージを発行するソース」に焦点を当てて説明していきます。


メッセージを発行するソース

ストリームソースを用意する手段はいくつか存在します。
まずは、今まで使用したSubjectシリーズについて説明します。


Subjectシリーズ

今まで使用してきたSubjectですが、こちらを使用するのが基本となります。
このSubjectにはいくつか派生が存在するので、今回は以下の4つ紹介していきます。

まずは簡単に表で説明します。

名前 機能
Subject<T> OnNextで発行
BehaviorSubject<T> OnNextで発行された最新の値をキャッシュ
Subscribeでキャッシュされた値を発行
ReplaySubject<T> OnNextで発行された値を全てキャッシュ
Subscribeでキャッシュされた値を全て発行
AsyncSubject<T> OnNextで発行された最新の値をキャッシュ
OnCompletedでキャッシュされた値を発行

次に個別に解説していきます。

Subject<T>

OnNextで発行する基本的なものとなります。

Subject
/// <summary>
/// Subject
/// OnNextで発行
/// </summary>
private void ExcuteSubject() {
    // イベント発行
    Subject<int> subject = new Subject<int>();
    // イベント登録
    subject.Subscribe(x => {
        Debug.Log("OnNext : " + x);
    }, () => {
        Debug.Log("OnCompleted");
    });
    // イベント実行
    subject.OnNext(1);
    subject.OnNext(10);
    subject.OnCompleted();
}
結果

OnNextが呼ばれると値を発行します。

f:id:soramamenatan:20201221114022p:plain


BehaviorSubject<T>

OnNextで値を発行し、その値をキャッシュします。
次のOnNextでも同様に、値を発行しますが既にキャッシュされている値は破棄して、最新のもののみをキャッシュします。
Subscribeで新たに購読者を登録した場合、キャッシュした値を発行します。
このメソッドのみ、コンストラクタで初期値を指定する必要があります。

BehaviorSubject
/// <summary>
/// BehaviorSubject
/// OnNextで発行された最新の値をキャッシュ
/// Subscribeでキャッシュされた値を発行
/// </summary>
private void ExcuteBehaviorSubject() {
    BehaviorSubject<int> subject = new BehaviorSubject<int>(0);

    IDisposable stream1 = subject.Subscribe(x => {
        Debug.Log("ストリーム1 : " + x);
    }, () => {
        Debug.Log("ストリーム1 OnCompleted");
    });
    subject.OnNext(1);
    subject.OnNext(10);

    // 最新の値のみキャッシュされているので、OnNext(10)のみ呼ばれる
    IDisposable stream2 = subject.Subscribe(x => {
        Debug.Log("ストリーム2 : " + x);
    }, () => {
        Debug.Log("ストリーム2 OnCompleted");
    });
    subject.OnCompleted();
    // OnCompleted後なので呼ばれない
    subject.OnNext(100);

    // 最新の値のみキャッシュされているので、OnCompletedのみ呼ばれる
    IDisposable stream3 = subject.Subscribe(x => {
        Debug.Log("ストリーム3 : " + x);
    }, () => {
        Debug.Log("ストリーム3 OnCompleted");
    });
}
結果

ストリーム1を登録した際に、コンストラクタで指定されている0が発行されます。
ストリーム2を登録した際には、事前に呼ばれた10が発行されます。
ストリーム3を登録した場合、最後のキャッシュがOnCompletedなので、OnCompletedが発行されます。

f:id:soramamenatan:20201221115641p:plain


ReplaySubject<T>

OnNextで値を発行し、その値をキャッシュします。
Subscribeで新たに購読者を登録した場合、すべての値を発行します。

ReplaySubject
/// <summary>
/// ReplaySubject
/// OnNextで発行された値を全てキャッシュ
/// Subscribeでキャッシュされた値を全て発行
/// </summary>
private void ExcuteReplaySubject() {
    ReplaySubject<int> subject = new ReplaySubject<int>();
    subject.OnNext(1);

    IDisposable stream1 = subject.Subscribe(x => {
        Debug.Log("ストリーム1 : " + x);
    }, () => {
        Debug.Log("ストリーム1 OnCompleted");
    });
    subject.OnNext(10);

    // キャッシュされているので、OnNextも呼ばれる
    IDisposable stream2 = subject.Subscribe(x => {
        Debug.Log("ストリーム2 : " + x);
    }, () => {
        Debug.Log("ストリーム2 OnCompleted");
    });
    subject.OnCompleted();
    // OnCompleted後なので呼ばれない
    subject.OnNext(100);

    // キャッシュされているので、OnNextも呼ばれる
    IDisposable stream3 = subject.Subscribe(x => {
        Debug.Log("ストリーム3 : " + x);
    }, () => {
        Debug.Log("ストリーム3 OnCompleted");
    });
}
結果

ストリーム2を登録した際には、すでにキャッシュされている1と10が発行されます。
ストリーム3を登録した際に、OnCompletedが呼ばれていても全ての値が発行されます。

f:id:soramamenatan:20201221150003p:plain


AsyncSubject<T>

OnNextで値を発行せずに最新の値のみキャッシュします。
そしてOnCompletedでキャッシュされた値を発行します。
Subscribeで新たに購読者を登録した場合、OnCompletedを呼ぶ前ですと何も発行されません。
OnCompleted後ですと、キャッシュされた値とOnCompletedが呼ばれます。

AsyncSubject
/// <summary>
/// AsyncSubject
/// OnNextで発行された最新の値をキャッシュ
/// OnCompletedでキャッシュされた値を発行
/// </summary>
private void ExcuteAsyncSubject() {
    AsyncSubject<int> subject = new AsyncSubject<int>();
    subject.OnNext(1);

    // OnCompleted()が呼ばれていないので、OnNext(1)は呼ばれない
    IDisposable stream1 = subject.Subscribe(x => {
        Debug.Log("ストリーム1 : " + x);
    }, () => {
        Debug.Log("ストリーム1 OnCompleted");
    });
    subject.OnNext(10);

    IDisposable stream2 = subject.Subscribe(x => {
        Debug.Log("ストリーム2 : " + x);
    }, () => {
        Debug.Log("ストリーム2 OnCompleted");
    });
    // OnCompletedが呼ばれたので、最新のキャッシュも発行
    subject.OnCompleted();
    // OnCompleted後なので呼ばれない
    subject.OnNext(100);

    // OnNext(10)とOnCompletedを発行
    IDisposable stream3 = subject.Subscribe(x => {
        Debug.Log("ストリーム3 : " + x);
    }, () => {
        Debug.Log("ストリーム3 OnCompleted");
    });
}
結果

最新の値のみキャッシュされるので、全てのストリームで10のみ発行されます。
ストリーム3を登録した際に、OnCompletedが呼ばれていますが、キャッシュされた値とOnCompletedが呼ばれます。

f:id:soramamenatan:20201221150616p:plain


Subjectシリーズ一覧

一覧としてまとめました。

名前 OnNext時 OnCompleted時 Subscribe時
Subject<T> 値を発行 OnCompletedを発行 何もしない
BehaviorSubject<T> 値を発行
最新の値のみキャッシュ
OnCompletedを発行 キャッシュされた値を発行
ReplaySubject<T> 値を発行
値をキャッシュ
OnCompletedを発行 キャッシュされた全ての値を発行
AsyncSubject<T> 最新の値のみキャッシュ キャッシュされた値とOnCompletedを発行 何もしないが、OnCOmpletedが呼ばれた後だとキャッシュされた値とOnCompletedを発行


ReactivePropertyシリーズ

ReactiveProperty<T>

値を渡した際に、同じ値の場合何もしない、違う値の場合OnNextを発行してくれるものとなります。

ReactiveProperty<T>のサンプル
/// <summary>
/// ReactivePropertyの実行
/// </summary>
private void ExcuteReactiveProperty() {
    // ReactiveProperty
    ReactiveProperty<int> intRp = new ReactiveProperty<int>();
    // 初期値を指定可能
    ReactiveProperty<string> stringRp = new ReactiveProperty<string>("Hoge");

    // Subscribe時に値を発行
    intRp.Subscribe(x => {
        Debug.Log("int RP : " + x);
    });
    stringRp.Subscribe(x => {
        Debug.Log("string RP : " + x);
    });

    // 値を書き換えるとOnNext
    intRp.Value = 10;
    stringRp.Value = "Fuga";

    // 同じ値なので何もしない
    intRp.Value = 10;
    stringRp.Value = "Fuga";

    // 値の代入も可能
    int value = intRp.Value;
    Debug.Log("value : " + value);
}
結果

値が変更された時にOnNextが発行されています。
同じ値の場合には、何も発行されていません。

f:id:soramamenatan:20201222105017p:plain


ReactiveCollection<T>

ReactivePropertyのListのようなものになります。
用意されているメソッドは以下となります。

  • 要素の追加
  • 要素の削除
  • 素数の変換
  • 要素の上書き
  • 要素の移動
  • リストのクリア
ReactiveCollection<T>のサンプル
/// <summary>
/// ReactiveCollectionの実行
/// </summary>
private void ExcuteReactiveCollection() {
    // ReactiveCollection
    ReactiveCollection<string> rcString = new ReactiveCollection<string>();

    // Subscribe時に値を発行
    // 要素追加時
    rcString
        .ObserveAdd()
        .Subscribe(x => {
            Debug.Log(string.Format("[{0}]番目に[{1}]を追加", x.Index, x.Value));
        });

    // 要素削除時
    rcString
        .ObserveRemove()
        .Subscribe(x => {
            Debug.Log(string.Format("[{0}]番目の[{1}]を削除", x.Index, x.Value));
        });

    // 要素を追加するとOnNext
    rcString.Add("Hoge");
    rcString.Add("Fuga");
    rcString.Add("Foo");

    // 要素を削除するとOnNext
    rcString.Remove("Fuga");
    foreach (string str in rcString) {
        Debug.Log("rcStringの中身 : " + str);
    }

    // 存在しないので呼ばれない
    rcString.Remove("nothing");
}
結果

要素を追加、削除した時にOnNextが発行されています。
foreachで要素を確認した際にFugaがListから削除されているのがわかります。

f:id:soramamenatan:20210102154331p:plain


ReactiveDictionary<T>

ReactivePropertyのDictionaryのようなものとなります。
基本的な挙動は、ReactiveCollection<T>と同じとなります。


ReactiveDictionary<T>のサンプル
/// <summary>
/// ReactiveDictionaryの実行
/// </summary>
private void ExcuteReactiveDictionary() {
    // ReactiveDictionary
    ReactiveDictionary<string, int> reactiveDictionary = new ReactiveDictionary<string, int>();

    // Subscribe時に値を発行
    // 要素追加時
    reactiveDictionary
        .ObserveAdd()
        .Subscribe(x => {
            Debug.Log(string.Format("要素追加, Key : {0}, Value : {1}", x.Key, x.Value));
        });

    // 要素削除時
    reactiveDictionary
        .ObserveRemove()
        .Subscribe(x => {
            Debug.Log(string.Format("要素削除, Key : {0}, Value : {1}", x.Key, x.Value));
        });

    // 要素数変化時
    reactiveDictionary
        .ObserveCountChanged()
        .Subscribe(x => {
            Debug.Log(string.Format("要素数変化, Count : {0}", x));
        });

    // 要素を追加するとOnNext
    reactiveDictionary.Add("スライム", 10);
    reactiveDictionary.Add("ドラゴン", 100);
    reactiveDictionary.Add("魔王", 1000);

    // 要素を削除するとOnNext
    reactiveDictionary.Remove("スライム");
    foreach(var keyValue in reactiveDictionary) {
        Debug.Log(string.Format("Dictionaryの中身, Key : {0}, Value : {1}", keyValue.Key, keyValue.Value));
    }
}
結果

要素を追加、削除した時にOnNextが発行されています。 また、ObserveCountChangedを追加しているので素数が変化する度にOnNextが発行されます。

f:id:soramamenatan:20210103184643p:plain


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


参考サイト様

qiita.com

light11.hatenadiary.com