UniRx学习笔记_上

目录

[TOC]

一、概述

1.UniRx作用

UniRxUnity Reactive Extensions——响应式扩展

  • 简洁优雅得实现异步逻辑
  • 优雅实现MVP/MVC模式
  • 对UGUI/UnityAPI提供增强
  • 提高编码效率

2.基本语法格式

Observable.Timer().Subscribe().AddTo(继承自IDisposable接口的对象)

其中:

  • Observable:可被观察的,UniRx中固定的开头
  • Timer:发布者,也被称作数据源/事件源
  • Subscribe:订阅者
  • AddTo:绑定销毁的生命周期

二、快速入门

1.计时器

(1)常规计时器

    public class CommonTimerExample : MonoBehaviour
    {
        private float mStartTime;
        private void Start()
        {
            mStartTime = Time.time;
        }
        private void Update()
        {
            if (Time.time - mStartTime >= 3f)
            {
                Debug.Log("Do Something");
                mStartTime = float.MaxValue;
            }
        }
    }

(2)Unity协程下的计时器

    public class CoroutineTimerExample : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(TimeTask(3.0f, () =>
             {
                 Debug.Log("Do Something");
             }));
        }
        IEnumerator TimeTask(float time, Action callback)
        {
            yield return new WaitForSeconds(time);
            callback?.Invoke();
        }
    }

(3)UniRx实现计时器

Observable.Timer(TimeSpan.FromSeconds(时间)).Subscribe(_={计时任务逻辑})

    public class UniRxTimerExample : MonoBehaviour
    {
        private void Start()
        {
            Observable.Timer(TimeSpan.FromSeconds(3.0f))
                .Subscribe(_ =>
                {
                    Debug.Log("Do Something");
                });
        }
    }

2.独立的Update

UniRx比较初级的使用:使用UniRx注册事件以避免在Update中增加逻辑。

Observable.EveryUpdate().Subscribe(_=>{待注册的逻辑})

    public class UpdateExample : MonoBehaviour
    {
        enum ButtonState
        {
            None,
            Clicked,
        }
        private void Start()
        {
            bool mouseClicked = false;
            ButtonState buttonState = ButtonState.None;
            //使用UniRx注册事件以避免在Update中增加逻辑
            Observable.EveryUpdate()
                .Subscribe(_ =>
                {
                    if (Input.GetMouseButtonDown(0))
                    {
                        Debug.Log("鼠标左键点击");
                        mouseClicked = true;
                    }
                });
            Observable.EveryUpdate()
                .Subscribe(_ =>
                {
                    if (Input.GetMouseButtonDown(1))
                    {
                        Debug.Log("鼠标右键点击");
                        mouseClicked = true;
                    }
                });
            Observable.EveryUpdate()
                .Subscribe(_ =>
                {
                    if (mouseClicked && buttonState == ButtonState.None)
                    {
                        buttonState = ButtonState.Clicked;
                        Debug.Log("按钮点击状态切换为:" + buttonState);
                    }
                });
        }
    }

3.UniRx的API:AddTo

AddTo概述:

  • 一个静态扩展关键字,对接口IDisposable进行了扩展;
  • 只要实现了此接口,都可以使用AddTo;
  • 当AddTo的目标销毁的时候,就会调用IDisposableOnDispose方法。

AddTo作用:

  • 将GameObject或MonoBehaviour销毁与UniRx绑定。
  • 当GameObjec或MonoBehaviour被销毁的时候,UniRx的相关任务也会被销毁,防止产生空引用异常,增加UniRx使用的安全性。

代码:

            Observable.Timer(TimeSpan.FromSeconds(2f))
                .Subscribe()
                .AddTo(this);//也可以是gameObject

当this脚本被销毁时,Timer同时被销毁,防止空引用异常。

4.操作符

UniRx中使用Observable.xxxx()会开启一条事件流(数据源/事件源),

然后使用一系列操作符对事件流进行处理、组织、整理,

最后进行订阅和生命周期绑定。

(1)Where

作用:对数据源(发布者)进行过滤;位于发布者和订阅者之间。

    public class WhereExample : MonoBehaviour
    {
        private void Start()
        {
            Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(0))
                .Subscribe(_ =>
                {
                    Debug.Log("鼠标左键点击");
                })
                .AddTo(this);
        }
    }

(2)First

作用:对数据源(发布者)进行过滤;获取满足First内部条件的数据源中的第一个数据;可替代Where

        private void Start()
        {
            Observable.EveryUpdate()//  数据源/事件源/事件流    发布者
                .First(_ => Input.GetMouseButtonDown(0))//  处理、组织、整理
                .Subscribe(_ => Debug.Log("第一次点击鼠标左键"))//  订阅者
                .AddTo(this);//  生命周期绑定
        }

(3)Select

作用:映射事件流,将事件流中的事件/数据映射为其他值(类型可改变)

//将点击按钮A的事件映射为字符串A
btnA.OnClickAsObservable().Select(_ => "A");

(4)Merge

作用:合并事件流

    public class MergeExample : MonoBehaviour
    {
        private void Start()
        {
            //开启一条鼠标点击事件流
            var leftClickEventStream = Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(0));
            //开启一条新的鼠标点击事件流
            var rightClickEventStream = Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(1));
            //合并事件流
            Observable.Merge(leftClickEventStream, rightClickEventStream)
                .Subscribe(_ => Debug.Log("点击鼠标"));
        }
    }

(5)WhenAll

作用:监听所有事件流都执行完毕的时候

例子:监听每个按钮都点击一次:

    //监听所有按钮点击一次
    public class AllBtnClickOneTime : MonoBehaviour
    {
        private void Start()
        {
            Button btnA = transform.Find("BtnA").GetComponent<Button>();
            Button btnB = transform.Find("BtnB").GetComponent<Button>();
            Button btnC = transform.Find("BtnC").GetComponent<Button>();

            var aStream = btnA.OnClickAsObservable().First();
            var bStream = btnB.OnClickAsObservable().First();
            var cStream = btnC.OnClickAsObservable().First();

            Observable.WhenAll(aStream, bStream, cStream)
                .Subscribe(_ => Debug.Log("按钮均点击过一次"));
        }
    }

5.对UGUI的支持

(1)Button

            button.OnClickAsObservable()
                .First()
                .Subscribe(_ => Debug.Log("第一次点击按钮"));

(2)Toggle

            toggle.OnValueChangedAsObservable()
                .Where(on => on)
                .Subscribe(_ => Debug.Log("开关为True"));

(3)Scrollbar

            scrollbar.OnValueChangedAsObservable()
                .Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue));

(4)ScrollRect

            scrollRect.OnValueChangedAsObservable()
                .Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue);

(5)Slider

            slider.OnValueChangedAsObservable()
                .Subscribe(sliderValue =>Debug.Log("Slider Value " + sliderValue));

(6)InputField

            mInputField.OnEndEditAsObservable()
                .Subscribe(result =>Debug.Log("Result :" + result));

(7)Triggers——拖拽事件

使用需导入UniRx.Triggers命名空间。具体API详见ObservableTriggerExtensions.Component.cs

            image.OnBeginDragAsObservable()
                .Subscribe(_ => Debug.Log("Begin Drag"));
            image.OnDragAsObservable()
                .Subscribe(_ => Debug.Log("Dragging"));
            image.OnEndDragAsObservable()
                .Subscribe(_ => Debug.Log("End Drag"));

(8)TextSubscribeToText

            //当 inputField 的输入值改变,则会马上显示在 resultText 上
            Text resultText = GetComponent<Text>();
            inputField.OnValueChangedAsObservable()
                .SubscribeToText(resultText);

(9)InteractableSubscribeToInteractable

            //IsDead为True时,设置atkBtn为不可交互
            mEnemy.IsDead.Where(isDead => isDead)
                .Select(isDead => !isDead)
                .SubscribeToInteractable(atkBtn);

6.ReactiveProperty——响应式属性

很多时候,当某个变量的值改变时,内部或外部需要执行相关逻辑,通常此时会将变量用属性封装,然后使用委托或事件供内部或外部注册值改变时候的逻辑。

UniRx提供ReactiveProperty响应式属性,以减少实现上述需求的代码量。

    public class ReactivePropertyExample : MonoBehaviour
    {
        //IntReactiveProperty:可序列化的响应式属性
        public IntReactiveProperty Age = new IntReactiveProperty(25);
        private void Start()
        {
            //注册的时候,会自动执行一次内部逻辑
            Age.Subscribe(age => Debug.Log("内部订阅Age改变时候需要执行的逻辑,此时Age:" + age));
            Age.Value = 26;
            new PersonA().Init(this);
        }
    }
    public class PersonA
    {
        public void Init(ReactivePropertyExample reactivePropertyExample)
        {
            //注册的时候,会自动执行一次内部逻辑
            reactivePropertyExample.Age.Subscribe(age => Debug.Log("外部订阅Age改变时候需要执行的逻辑,此时Age:" + age));
            reactivePropertyExample.Age.Value = 27;
        }
    }

运行结果:

picture0

7.简单的MVP模式实现

MV(R)P模式全称:Model—View—(Reactive)Presenter

picture1

代码:

    public class MVPExample : MonoBehaviour
    {
        EnemyModel mEnemy;
        //Unity中Hierarchy下的UI为View部分
        //Start内部为Presenter部分
        //EnemyModel为Model部分
        private void Start()
        {
            Button atkBtn = transform.Find("Button")
                .GetComponent<Button>();
            Text hpText = transform.Find("Text")
                .GetComponent<Text>();
            mEnemy = new EnemyModel(100);
            //将UI控件与Model绑定
            atkBtn.OnClickAsObservable().Subscribe(_ =>
            {
                mEnemy.Hp.Value -= 40;
            });
            mEnemy.Hp.SubscribeToText(hpText);
            mEnemy.IsDead.Where(isDead => isDead)
                .Select(isDead => !isDead)
                .SubscribeToInteractable(atkBtn);
        }
    }
    public class EnemyModel
    {
        public IntReactiveProperty Hp;
        public IReadOnlyReactiveProperty<bool> IsDead;
        public EnemyModel(int initialHp)
        {
            Hp = new IntReactiveProperty(initialHp);
            IsDead = Hp.Select(hp => hp <= 0)
                .ToReactiveProperty();
        }
    }

8.综合以上的一个小练习:点击某一个按钮后,所有按钮不再能点击,且2秒后隐藏所有按钮

    public class EventsMaskExample : MonoBehaviour
    {
        private void Start()
        {
            Button btnA = transform.Find("ButtonA").GetComponent<Button>();
            Button btnB = transform.Find("ButtonB").GetComponent<Button>();
            Button btnC = transform.Find("ButtonC").GetComponent<Button>();

            var btnAStream = btnA.OnClickAsObservable().Select(_ => "A");
            var btnBStream = btnB.OnClickAsObservable().Select(_ => "B");
            var btnCStream = btnC.OnClickAsObservable().Select(_ => "C");

            Observable.Merge(btnAStream, btnBStream, btnCStream)
                .First()
                .Subscribe(btnCode =>
                {
                    switch (btnCode)
                    {
                        case "A":
                            Debug.Log("点击按钮A");
                            break;
                        case "B":
                            Debug.Log("点击按钮B");
                            break;
                        case "C":
                            Debug.Log("点击按钮C");
                            break;
                    }
                    Observable.Timer(TimeSpan.FromSeconds(2f))
                    .Subscribe(_ => gameObject.SetActive(false));
                });
        }
    }

9.Unity生命周期

(1)EveryFixedUpdate

Observable.EveryFixedUpdate().Subscribe(_ => {});

(2)EveryEndOfFrame

Observable.EveryEndOfFrame().Subscribe(_ => {});

(3)EveryLateUpdate

Observable.EveryLateUpdate().Subscribe(_ => {});

(4)EveryAfterUpdate

Observable.EveryAfterUpdate().Subscribe(_ => {});

(5)EveryApplicationPause

Observable.EveryApplicationPause().Subscribe(paused => {});

(6)EveryApplicationFocus

Observable.EveryApplicationFocus().Subscribe(focused => {});

(7)EveryApplicationQuit

Observable.EveryApplicationQuit().Subscribe(_ => {}):

10.UniRx的Trigger

Observable.EveryUpdate()这个 API 有的时候在某个脚本中实现,需要绑定 MonoBehaviour 的⽣命周期(主要是 OnDestroy),当然也有的时候是全局的,而且永远不会被销毁的。

需要绑定 MonoBehaviour 生命周期的 EveryUpdate。只需要一个 AddTo 就可以进行绑定了。

Observable.EveryUpdate()
    .Subscribe(_ => {})
    .AddTo(this);

但其实有更简洁的实现:

this.UpdateAsObservable()
    .Subscribe(_ => {});

这种类型的 Observable 是Trigger,即触发器器。

触发器,字如其意,是当某个事件发生时,则会将该事件发送到Subscribe函数中,而这个触发器器,本身是一个功能脚本,这个脚本挂在 GameObject 上,来监听 GameObject 的某个事件发生,事件发生则会回调给注册它的 Subscribe 中。

触发器的操作和其他的事件源 (Observable) 是一样的,都支持 Where、First、Merge 等操作符。

Trigger类型的 Observable 和之前所有的Observable在表现上有些不一样:

  • 1.Trigger⼤大部分都是都是XXXAsObsrevable命名形式的。

  • 2.在使用Trigger的GameObject上都会挂上对应的Observable XXXTrigger.cs的脚本。

    AddTo()这个 API 其实是封装了了一种 Trigger: ObservableDestroyTrigger

    ObservableDestroyTrigger就是当 GameObject 销毁时获取事件的一个触发器。一般的Trigger都会配合MonoBehaviour一起使用。 ObservableDestroyTrigger的使用代码如下:

    this.OnDestroyAsObservable()
        .Subscribe(_ => {});
    

不同类型的Trigger:

  • 1.各种细分类型的Update

    this.FixedUpdateAsObservable().Subscribe(_ => {});

    this.UpdateAsObservable().Subscribe(_ => {});

    this.LateUpdateAsObservable().Subscribe(_ => {});

  • 2.各种碰撞的Trigger

    this.OnCollisionEnterAsObservable(collision => {});

    this.OnCollisionStayAsObservable(collision => {});

    this.OnCollisionExitAsObservable(collision => {});

  • 3.一些脚本的参数监听

    this.OnEnableAsObservable().Subscribe(_ => {});

    this.OnDisableAsObservable().Subscribe(_ => {});

  • 4.详情可以查看ObservableTriggerExtensions.csObervableTriggerExtensions.Component.cs中的API。

11.UniRx的协程

(1)Unity的Coroutine与UniRx的协程

API:.FromCoroutine()

    public class CoroutineExample : MonoBehaviour
    {
        private void Start()
        {
            //Unity的Coroutine:
            StartCoroutine(CoroutineA("Unity的Coroutine"));
            //UniRx的
            Observable.FromCoroutine(_ => CoroutineA("UniRx的Coroutine"))
                .Subscribe(_ => Debug.Log("协程执行完之后,再执行此处逻辑"));
        }
        IEnumerator CoroutineA(string content)
        {
            yield return new WaitForSeconds(1f);
            Debug.Log(content);
        }
    }

运行结果:

picture2

(2)ObservableYield对象之间的相互转化

API:.ToYieldInstruction()

    public class Observable2YieldExample : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(CoroutineA());
        }
        IEnumerator CoroutineA()
        {
            //将Observable转化为Yield对象
            yield return Observable.Timer(TimeSpan.FromSeconds(1.0f))
                .ToYieldInstruction();
            Debug.Log("1秒后执行");
        }
    }

(3)WhenAll:Coroutine的并行操作

    public class WhenAllExample : MonoBehaviour
    {
        IEnumerator A()
        {
            yield return new WaitForSeconds(1.0f);
            Debug.Log("A");
        }
        IEnumerator B()
        {
            yield return new WaitForSeconds(2.0f);
            Debug.Log("B");
        }
        private void Start()
        {
            var aStream = Observable.FromCoroutine(A);
            var bStream = Observable.FromCoroutine(B);
            //WhenAll类似于Merge操作符:用于操作事件流,监听所有事件流都执行完毕的时候
            Observable.WhenAll(aStream, bStream)
                .Subscribe(_ => Debug.Log("When All执行"));
        }
    }

运行结果:

picture3

12.UniRx中Observable的生命周期

结束时候的生命周期:OnNext => OnCompleted

出错时候的生命周期:OnError

  • UniRx中除了EveryUpdate的事件流,其他事件流都拥有OnNext和OnCompleted的生命周期:其中OnNext会在结束时先执行,之后执行OnCompleted。
  • 在EveryUpdate中OnNext会在每帧都执行。当EveryUpdate与First操作符共同使用的时候,EveryUpdate将拥有OnNext和OnCompleted生命周期。

测试代码:

    public class OnCompletesExample : MonoBehaviour
    {
        private void Start()
        {
            //Timer有OnNext、OnCompleted生命周期
            Observable.Timer(TimeSpan.FromSeconds(2f))
                .Subscribe(_ =>
                {
                    Debug.Log("OnNext:Timer--after 2 second");
                }, () =>
                 {
                     Debug.Log("OnCompleted:Timer");
                 });
            //EveryUpdate没有OnCompleted生命周期
            //加上First操作符后就有了OnCompleted生命周期
            Observable.EveryUpdate().First()
                .Subscribe(_ =>
                {
                    Debug.Log("OnNext:EveryUpdate--First");
                }, () =>
                 {
                     Debug.Log("OnCompleted:EveryUpdate--First");
                 });
            //FromCoroutine的OnNext和OnCompleted都是在协程的yield之后执行
            Observable.FromCoroutine(CoroutineA)
                .Subscribe(_ =>
                {
                    Debug.Log("OnNext:FromCoroutine--after 3 second");
                }, () =>
                 {
                     Debug.Log("OnCompleted--FromCoroutine");
                 });
        }
        IEnumerator CoroutineA()
        {
            yield return new WaitForSeconds(3f);
            Debug.Log("协程");
        }
    }

运行结果:

picture4

13.UniRx中的多线程

(1)Start 开启多线程

API:Observable.Start()

(2)ObserveOnMainThread 将对线程转入主线程

API:ObserveOnMainThread()

注意:各个线程中的返回值将被Subscribe按WhenAll中参数的顺序接收

测试代码:

    public class ThreadExample : MonoBehaviour
    {
        private void Start()
        {
            //使用Start开启线程
            var threadAStream = Observable.Start(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(5));
                return "a";
            });
            var threadBStream = Observable.Start(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return "b";
            });
            //使用ObserveOnMainThread将线程转入到主线程中
            //各个线程中的返回值将被Subscribe按WhenAll中参数的顺序接收
            Observable.WhenAll(threadAStream, threadBStream)
                .ObserveOnMainThread()
                .Subscribe(results => Debug.LogFormat("threadAStream:{0};threadBStream:{1}", results[0], results[1]));
        }
    }

运行结果:

picture5

14.UniRx中处理网络请求操作:ObservableWWW

(1)Get

  • 单个Get获取URL资源:

                //使用WWW请求、获得URL上的(Text)信息
                ObservableWWW.Get(@"https://huskytgame.github.io/")
                    .Subscribe(responseText =>
                    {
                        Debug.LogFormat("Response Text:{0}", responseText.Substring(0, 100));
                    }, exception =>
                     {
                         Debug.LogErrorFormat("异常:{0}", exception);
                     });
    
  • WhenAll一起使用

                var wwwAStream = ObservableWWW.Get(@"https://github.com/HuskyTGame");
                var wwwBStream = ObservableWWW.Get(@"https://aaaaaahuskytgame.github.io/");
                Observable.WhenAll(wwwAStream, wwwBStream)
                    .Subscribe(responseTexts =>
                    {
                        Debug.LogFormat("wwwAStream:{0}", responseTexts[0]);
                        Debug.LogFormat("wwwBStream:{0}", responseTexts[1]);
                    }, exception =>
                    {
                        Debug.LogErrorFormat("异常:{0}", exception);
                    });
    

    运行结果:

    picture6

(2)Post

(3)GetWWW:获取WWW

(4)PostWWW

(5)GetAnGetBytes:下载二进制数据

可以使用ScheduledNotifier(工作计划通告器)来获取下载进度信息:

        private string mUrl = @"https://txxs.mahua-yongjiu.com/20191219/8388_caa7a87a/1000k/hls/88a4c76b152000000.ts";
        private void Start()
        {
            var progressListener = new ScheduledNotifier<float>();
            ObservableWWW.GetAndGetBytes(mUrl, null, progressListener)
                .Subscribe(bytes =>
                {
                    Debug.LogFormat("下载的资源:{0}", bytes[0].ToString());
                });
            progressListener.Subscribe(progress =>
            {
                Debug.LogFormat("下载进度:{0}/1", progress);
            });
        }

运行结果:

picture7

15.ReactiveCommand:响应式命令

ReactiveCommand内部包含有一个只读的响应式属性CanExecute,该属性决定了响应式命令是否可执行。

该属性默认为true,但可以通过在构造响应式命令时传入适当的Observable来控制CanExecute

例子1:

            //默认CanExecute一直为True
            //CanExecute为只读响应式属性,只受构造时传入的Observable影响
            var reactiveCommand = new ReactiveCommand();
            //为响应式命令注册回调
            reactiveCommand.Subscribe(_ =>
            {
                Debug.Log("Execute");
            });
            //执行命令
            Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(0))
                .Subscribe(_ =>
                {
                    reactiveCommand.Execute();
                });

例子2:鼠标按下事件的响应:

            var mouseUpStream = Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonUp(0))
                .Select(_ => false);
            var mouseDownStream = Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(0))
                .Select(_ => true); ;
            var onMouseDownStream = Observable.Merge(mouseUpStream, mouseDownStream);
            //创建响应式命令
            //设置初始CanExecute属性为false
            var reactiveCommand = new ReactiveCommand(onMouseDownStream, false);
            //注册事件
            reactiveCommand.Subscribe(_ =>
            {
                Debug.Log("鼠标按下");
            });
            //将命令放入Update中监听
            Observable.EveryUpdate()
                .Subscribe(_ =>
                {
                    reactiveCommand.Execute();
                });

例子3:使用Where操作符,判断奇偶数

            //创建命令
            var reactiveCommand = new ReactiveCommand<int>();
            //使用Where操作符
            reactiveCommand.Where(x => x % 2 == 0)
                .Subscribe(x => Debug.LogFormat("{0}是偶数", x));
            //加上时间戳
            reactiveCommand.Where(x => x % 2 == 1)
                .Timestamp()
                .Subscribe(x => Debug.LogFormat("{0}是奇数,当前时间{1}", x.Value, x.Timestamp));

            reactiveCommand.Execute(9);
            reactiveCommand.Execute(10);

运行结果:

picture8

16.ReactiveCollectionReactiveDictionary:响应式集合与响应式字典

(1)ReactiveCollection

  • 添加:ObserveAdd()
  • 移除:ObserveRemove()
  • 个数改变:ObserveCountChanged()
  • 移动:ObserveMove()
  • 替换:ObserveReplace()
  • 重置:ObserveReset()
        private ReactiveCollection<string> mUrls = new ReactiveCollection<string>
        {
            "huskytgame.io",
            "baidu.com",
            "100524.com",
            "mooc.cn"
        };
        private void Start()
        {
            //添加
            mUrls.ObserveAdd()
                .Subscribe(value => Debug.LogFormat("添加的值为:{0};添加的值的索引为:{1}", value.Value, value.Index));
            //移除
            mUrls.ObserveRemove()
                .Subscribe(value => Debug.LogFormat("删除的值为:{0};删除的值的索引为:{1}", value.Value, value.Index));
            //个数改变
            mUrls.ObserveCountChanged()
                .Subscribe(count => Debug.LogFormat("此时List中元素个数:{0}", count));
            //移动
            mUrls.ObserveMove()
                .Subscribe(value => Debug.LogFormat("移动的元素为:{0},从索引{1}移动到索引{2}", value.Value, value.OldIndex, value.NewIndex));
            //替换
            mUrls.ObserveReplace()
                .Subscribe(value => Debug.LogFormat("替换索引为{0}的元素{1}为{2}:{0}", value.Index, value.OldValue, value.NewValue));
            //重置
            mUrls.ObserveReset()
                .Subscribe(_ => Debug.LogFormat("重置List"));

            mUrls.Add("twgame.cn");
            mUrls.Remove("100524.com");
            mUrls.RemoveAt(1);
            mUrls.Move(0, 2);
            mUrls[0] = "Hare.com";
            mUrls.Clear();
        }

运行结果:

picture9

(2)ReactiveDictionary

用法同ReactiveCollection

  • 添加:ObserveAdd()
  • 移除:ObserveRemove()
  • 个数改变:ObserveCountChanged()
  • 移动:ObserveMove()
  • 替换:ObserveReplace()
  • 重置:ObserveReset()

17.AsyncOperation:异步操作

UniRx可以很容易监听异步加载进度。

加载Prefab:

            //异步加载资源
            Resources.LoadAsync<GameObject>("Prefabs/AsyncOperationTestCanvas")
                .AsAsyncOperationObservable()
                .Subscribe(resourcesRequest =>
            {
                Instantiate(resourcesRequest.asset);
            });

加载场景,并监听加载进度:

            //异步加载场景
            var progressObservable = new ScheduledNotifier<float>();
            SceneManager.LoadSceneAsync(0)
                .AsAsyncOperationObservable(progressObservable)
                .Subscribe(_ =>
            {
                Debug.Log("场景加载完毕");
            });
            progressObservable.Subscribe(progress =>
            {
                Debug.LogFormat("场景加载进度:{0}", progress);
            });

运行结果:

picture10

三、TodoList实战

效果演示:

GIF1

Hierarchy

picture11

/****************************************************
	文件:Model.cs
	作者:HuskyT
	邮箱:1005240602@qq.com
	日期:2019/12/22 23:31:28
	功能:TodoList的Model层
*****************************************************/

using System;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

namespace StudyUniRx.TodoList
{
    [Serializable]
    public class TodoItem
    {
        /// <summary>
        /// 待办事项标识
        /// </summary>
        public int Id;
        /// <summary>
        /// 待办事项列表
        /// </summary>
        public StringReactiveProperty Content;
        /// <summary>
        /// 待办事项是否完成
        /// </summary>
        public BoolReactiveProperty Completed;
    }
    [Serializable]
    public class TodoList
    {
        private const string SAVE_KEY = "Model";
        private int mTopId = 2;
        public List<TodoItem> TodoItems = new List<TodoItem>
        {
            new TodoItem
            {
                Id = 0,
                Content = new StringReactiveProperty("完成UniRx学习任务"),
                Completed = new BoolReactiveProperty(false),
            },
            new TodoItem
            {
                Id = 1,
                Content = new StringReactiveProperty("完成UniRx的笔记整理"),
                Completed = new BoolReactiveProperty(false),
            }
        };
        public void Add(string content)
        {
            TodoItems.Add(new TodoItem
            {
                Id = mTopId,
                Completed = new BoolReactiveProperty(false),
                Content = new StringReactiveProperty(content),
            });
            mTopId %= int.MaxValue;
            mTopId++;
        }
        public void Save()
        {
            PlayerPrefs.SetString(SAVE_KEY, JsonUtility.ToJson(this));
        }
        public static TodoList Load()
        {
            string content = PlayerPrefs.GetString(SAVE_KEY, string.Empty);
            if (string.IsNullOrEmpty(content))
            {
                return new TodoList();
            }
            else
            {
                return JsonUtility.FromJson<TodoList>(content);
            }
        }
    }
}
/****************************************************
	文件:UIInputCtrl.cs
	作者:HuskyT
	邮箱:1005240602@qq.com
	日期:2019/12/23 20:43:8
	功能:输入控制部分
*****************************************************/

using UnityEngine;
using UnityEngine.UI;
using UniRx;

namespace StudyUniRx.TodoList
{

    public class UIInputCtrl : MonoBehaviour
    {
        public enum Mode
        {
            /// <summary>
            /// 非编辑模式
            /// </summary>
            NonEdit,
            /// <summary>
            /// 编辑模式
            /// </summary>
            Edit,
        }
        #region UI组件
        private InputField mInputContent;
        private Button mBtnAdd;
        private Button mBtnUpdate;
        private Button mBtnCancel;
        #endregion
        #region Model
        public TodoList Model;
        #endregion
        public ReactiveProperty<Mode> InputMode;

        private void Awake()
        {
            mInputContent = transform.Find("InputContent").GetComponent<InputField>();
            mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();
            mBtnUpdate = transform.Find("BtnUpdate").GetComponent<Button>();
            mBtnCancel = transform.Find("BtnCancel").GetComponent<Button>();
            mBtnCancel.gameObject.SetActive(false);
            InputMode = new ReactiveProperty<Mode>(Mode.NonEdit);//初始为非编辑模式
        }
        private void Start()
        {
            //输入框只有在有输入的时候,添加按钮才可交互
            mInputContent.OnValueChangedAsObservable()
                                     .Select(editContent => !string.IsNullOrEmpty(editContent))
                                     .Subscribe(hasInput =>
                                     {
                                         mBtnAdd.interactable = hasInput;
                                         mBtnCancel.gameObject.SetActive(hasInput);
                                         InputMode.Value = hasInput ? Mode.Edit : Mode.NonEdit;//赋值输入模式
                                     });
            //只有输入框内容变化的时候,更新按钮才可以交互
            mInputContent.OnValueChangedAsObservable()
                                     .Select(editContent => mCacheTodoItem != null && editContent != mCacheTodoItem.Content.Value && !string.IsNullOrEmpty(editContent))
                                     .SubscribeToInteractable(mBtnUpdate);
            //注册Add按钮的监听
            mBtnAdd.OnClickAsObservable()
                            .Subscribe(_ =>
                            {
                                Model.Add(mInputContent.text);
                                mInputContent.text = string.Empty;//输入框重置
                            });
            //注册Update按钮的监听
            mBtnUpdate.OnClickAsObservable()
                                 .Subscribe(_ =>
                                 {
                                     mCacheTodoItem.Content.Value = mInputContent.text;
                                     NonEditMode();
                                 });
            //注册Cancel按钮的监听
            mBtnCancel.OnClickAsObservable()
                                .Subscribe(_ =>
                                {
                                    NonEditMode();
                                });
        }

        public void NonEditMode()
        {
            mInputContent.text = string.Empty;//更新后清空输入框
            mCacheTodoItem = null;//TodoItem缓存置空
            mBtnAdd.gameObject.SetActive(true);
            mBtnUpdate.gameObject.SetActive(false);
            mBtnCancel.gameObject.SetActive(false);
        }
        private TodoItem mCacheTodoItem;
        public void EditMode(TodoItem todoItem)
        {
            mCacheTodoItem = todoItem;//缓存TodoItem
            mInputContent.text = todoItem.Content.Value;
            mBtnAdd.gameObject.SetActive(false);
            mBtnUpdate.gameObject.SetActive(true);
            mBtnCancel.gameObject.SetActive(true);
        }
    }
}
/****************************************************
	文件:UITodoList.cs
	作者:HuskyT
	邮箱:1005240602@qq.com
	日期:2019/12/23 15:28:47
	功能:管理整个TodoList(挂载在Canvas上)
*****************************************************/

using System.Linq;
using UnityEngine;
using UniRx;
using System.Collections.Generic;

namespace StudyUniRx.TodoList
{
    public class UITodoList : MonoBehaviour
    {
        #region UI组件
        [SerializeField]
        private Transform mContent;//ScrollView下的Content
        private UIInputCtrl mUIInputCtrl;
        private GameObject mUIEventMask;//遮罩
        #endregion
        #region Model
        private TodoList mModel = new TodoList();
        #endregion
        private UITodoItem mUITodoItemPrototype;

        private void Awake()
        {
            mUITodoItemPrototype = transform.Find("UITodoItemPrototype").GetComponent<UITodoItem>();
            mUIInputCtrl = transform.Find("InputCtrl").GetComponent<UIInputCtrl>();
            mUIEventMask = transform.Find("EventMask").gameObject;
            mModel = TodoList.Load();//加载Model
            mUIInputCtrl.Model = mModel;//注入Model
        }
        private void Start()
        {
            //注册待办事项列表Count变化时的监听
            mModel.TodoItems.ObserveEveryValueChanged(todoItems => todoItems.Count)
                                            .Subscribe(_ => OnDataChanged());
            //注册事件遮罩,响应输入模式的变化
            mUIInputCtrl.InputMode.Subscribe(mode =>
            {
                switch (mode)
                {
                    case UIInputCtrl.Mode.NonEdit:
                        mUIEventMask.SetActive(false);
                        break;
                    case UIInputCtrl.Mode.Edit:
                        mUIEventMask.SetActive(true);
                        break;
                    default:
                        break;
                }
            });
            //刷新一次TodoItem
            OnDataChanged();
        }
        /// <summary>
        /// 当TodoItem的Model改变时调用
        /// </summary>
        private void OnDataChanged()
        {
            ClearAllTodoItem(mContent);
            List<TodoItem> todoItems = mModel.TodoItems;
            todoItems.Where(todoItem => todoItem.Completed.Value == false)
                             .ToList()
                             .ForEach(todoItem =>
                             {
                                 todoItem.Completed.Subscribe(completed =>
                                 {
                                     //只有当完成的时候才执行OnDataChanged
                                     if (completed)
                                     {
                                         OnDataChanged();
                                     }
                                 });
                                 UITodoItem uiTodoItem = SpawnUITodoItem(mUITodoItemPrototype, todoItem, mContent);
                                 uiTodoItem.BtnSelf.OnClickAsObservable()
                                                                .Subscribe(_ =>
                                                                {
                                                                    mUIInputCtrl.EditMode(todoItem);
                                                                });
                             });
            mModel.Save();//存储
        }
        private UITodoItem SpawnUITodoItem(UITodoItem uiTodoItemPrototype, TodoItem model, Transform parent)
        {
            UITodoItem uiTodoItem = Instantiate(uiTodoItemPrototype);
            uiTodoItem.SetActive(true);
            uiTodoItem.SetParent(parent);
            uiTodoItem.ResetScale();
            uiTodoItem.SetModel(model);
            return uiTodoItem;
        }
        /// <summary>
        /// 清除所有Content下的TodoItem
        /// </summary>
        /// <param name="content"></param>
        private void ClearAllTodoItem(Transform content)
        {
            int itemCount = content.childCount;
            for (int i = 0; i < itemCount; i++)
            {
                Destroy(content.GetChild(i).gameObject);
            }
        }
    }
}
/****************************************************
	文件:UITodoItem.cs
	作者:HuskyT
	邮箱:1005240602@qq.com
	日期:2019/12/23 12:21:36
	功能:TodoItem——UI组件
*****************************************************/

using UnityEngine;
using UnityEngine.UI;
using UniRx;

namespace StudyUniRx.TodoList
{
    public class UITodoItem : MonoBehaviour
    {
        #region UI组件
        private Text mTxtContent;
        private Button mBtnCompleted;
        public Button BtnSelf;
        #endregion
        #region Model
        private TodoItem mModel;
        #endregion

        private void Awake()
        {
            mTxtContent = transform.Find("TxtContent").GetComponent<Text>();
            mBtnCompleted = transform.Find("BtnCompleted").GetComponent<Button>();
            BtnSelf = transform.GetComponent<Button>();
            mBtnCompleted.OnClickAsObservable()
                                       .Subscribe(_ => mModel.Completed.Value = true);
        }

        public void ResetScale()
        {
            transform.localScale = Vector3.one;
        }
        public void SetParent(Transform parent)
        {
            transform.SetParent(parent);
        }
        public void SetActive(bool activate)
        {
            gameObject.SetActive(activate);
        }
        public void SetModel(TodoItem model)
        {
            mModel = model;
            UpdateView(model.Content.Value);
            mModel.Content.Subscribe(UpdateView)
                                        .AddTo(this);
        }

        #region Private Function
        private void UpdateView(string content)
        {
            mTxtContent.text = content;
        }
        #endregion
    }
}