ゲーム制作勉強中!あこがれだったプログラマーに今からなろう!

昔、あこがれていたプログラマー。今からでも勉強してみようと思い立ち、チャレンジ開始! 勉強メモや、悪戦苦闘な日々の記録です。

UniRX・R3の購読終了(Dispose)について



昨日の実験をしただけでずいぶんUniRXへの理解が進んだ気がする。
・・・気がするだけだけど😅

今までは、変数を通知してくれるのがUniRXだと、漠然と思っていたけれど、
それはむしろ副産物で、本質はイベントを作ることができ、任意のクラスでそれを購読して
イベント駆動型のプログラミングを可能にしてくれるという方がいいみたい。

今日は、昨日の@toRisouPさんの記事の
qiita.com
を実験してみた。

昨日は文字列を通知するSubjectだったけれど、今日は特に値を持たない、イベントとして通知だけを行うUnit型の実験

まずは通知側

using UnityEngine;
using R3;
using Cysharp.Threading.Tasks;
using System;

public class Observable2 : MonoBehaviour
{
    //通知のみを行うSubject
    public Subject<Unit> UnitTestSubject = new Subject<Unit>();

    void Start()
    {
        Observable_Test();
    }

    async void Observable_Test()
    {
        int count = 0;

        while (true)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(1));
            if (count < 3)
            {
                //OnNextで通知を発行する。
                UnitTestSubject.OnNext(Unit.Default);
            }
            else
            {
                UnitTestSubject.OnCompleted();
                break;
            }

            Debug.Log("Observable_Test:by Observable2 : " + count);
            count++;

        }
    }
}


次は購読する側

using UnityEngine;
using R3;

public class View2 : MonoBehaviour
{
    [SerializeField]Observable2 observable2;

    void Start()
    {
        observable2.UnitTestSubject.Subscribe(
            _ => Debug.Log("by View:Event has been fired")
            );
    }
}



次にOnErrorの実験といきたかったけれど、OnErrorはUniRXとR3で挙動が変わった様で、
色々なドキュメントを読んでみたけれど、ちょっと理解できなかった😅

要は、UniRXでは例外を通知しようとすると購読が止まってしまうけれど、OnErrorRetryなどで
通知(購読?)を再開できる。
R3では例外では購読は止まらない様になった。

・・・という事なのか?

R3のOnErrorResumeとかについても、実験してみたが今ひとつ挙動が理解できなかった。

そして、UniRXでの

var subject = new subject<int>;
subject.Subscribe(
    x => Debug.Log(x),//通常時
    ex => Debug.Log("Error")//エラーの時
    () => Debug.Log("Completed")//購読を終了した時
);

() =>というのができなくなっていた。
結果的にいうと、UniRXではエラーの時に使用されていたexがR3では購読終了時に変わっている様子。

//Observable側
public class Observable2 : MonoBehaviour
{
    public Subject<int> Numbers = new Subject<int>();

    void Start()
    {
        Observable_Test();
    }

    async void Observable_Test()
    {
        int count = 0;

        while (true)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(1));
            if (count < 3)
            {
                Numbers.OnNext(count);
            }
            if (count > 2)
            {
                Numbers.OnCompleted();
                break;                
            }

            Debug.Log("from Observable2 Log: " + count);
            count++;

        }
    }
}

//Observer側
public class View2 : MonoBehaviour
{
    [SerializeField]Observable2 observable2;

    void Start()
    {
        observable2.Numbers
            .Subscribe(
                x => Debug.Log("from View Log: Get " + x),
                ex => Debug.Log("from View Log: " + ex)
            );
    }
}



とりあえず、購読が終了したことの通知を受け取る事はR3でもできるみたい。

次に、購読側から購読を終了する方法について
どうやら、Subject(Observable側)Subscribe(Observaer側)の関係性を保持しているのはSubject側らしい。
例えばEnemyManagerクラスSubjectEnemyクラスからSubscribeするとき、EnemyクラスDestroyされても、EnemyManagerクラスSubjectにはSubscribeが保持されたままになっている。

//Observable側
public class Observable2 : MonoBehaviour
{
    public Subject<int> Numbers = new Subject<int>();

    void Start()
    {
        Observable_Test();
    }

    async void Observable_Test()
    {
        int count = 0;

        while (true)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(1));
            if (count < 5)
            {
                Numbers.OnNext(count);
            }
            if (count > 4)
            {
                Numbers.OnCompleted();
                break;                
            }

            Debug.Log("from Observable2 Log: " + count);
            count++;

        }
    }
}

//Observer側
public class View2 : MonoBehaviour
{
    [SerializeField]Observable2 observable2;

    void Start()
    {
        observable2.Numbers
            .Subscribe(
                x => {if (x == 2)
                {
                    Destroy(gameObject);
                    Debug.Log(transform.position);
                }
                else
                {
                    Debug.Log(transform.position);
                }},
                ex => Debug.Log("from View Log: " + ex));
    }
}

このようにエラーの原因になったり、エラーが起こらないまでも無駄な処理でパフォーマンスに悪影響を及ぼしてしまう。
なので、Subscribeをする必要がなくなる時は、この関係性を終了させる必要があるらしい。

購読関係を終わらせる方法は、
・Observable側でOnCompletedが実行される。
・Observer側でDisposeが実行される。
のいずれかで行う。

Observer側でDisposeを実行する方法は2つあり、

//Observer側
using UnityEngine;
using R3;
using System;

public class View2 : MonoBehaviour
{
    [SerializeField]Observable2 observable2;
    private CompositeDisposable disposable = new CompositeDisposable();
    //UnitaskのCancellationTokenのようなものらしい・・・

    void Start()
    {
        observable2.Numbers
            .Subscribe(
                x => {if (x == 2)
                {
                    Destroy(gameObject);
                    disposable.Dispose();//ここで購読終了
                    Debug.Log(transform.position);
                }
                else
                {
                    Debug.Log(transform.position);
                }},
                ex => Debug.Log("from View Log: " + ex))
            
            .AddTo(disposable);//先ほどのCompositeDisposableを渡しておく
    }
}

この方法か、

//Observer側
using UnityEngine;
using R3;

public class View2 : MonoBehaviour
{
    [SerializeField]Observable2 observable2;

    void Start()
    {
 
        observable2.Numbers
            .Subscribe(
                x => {if (x == 2)
                {
                    Destroy(gameObject);
                    Debug.Log(transform.position);
                }
                else
                {
                    Debug.Log(transform.position);
                }},
                ex => Debug.Log("from View Log: " + ex))
            
            .AddTo(this); //←GameObjectが破壊された時にDisposeしてくれる。
    }
}

のいずれかが必要。



購読側のGameObjectが破壊されるなら後者の方法で良さそうだけど、そうでない時、例えば、ObjectPoolに返却する時は、前者の方法が必要という事だろうか。