一些基础知识点及小案例(二)

对于 UI 的增强

UniRx 的Reactive Property 是可以完全实现⼀种 MVC 的变种(MVP),并且是⾮常明确的。这样在开发的时候就不⽤再去纠结怎么实现了。这个概念,就让 UGUI 的开发简化了很多。除此之外,还⽀持了⾮常多的 UGUI 控件。
所有的 UGUI 控件⽀持列出如下 :

 Button mButton;
 Toggle mToggle;
 Scrollbar mScrollbar;
 ScrollRect mScrollRect;
 Slider mSlider;
 InputField mInputField;
void Start()
{
   mButton.OnClickAsObservable().Subscribe(_ => Debug.Log("On Button Clicked"));
   mToggle.OnValueChangedAsObservable().Subscribe(on => Debug.Log("Toggle " + on));
   mScrollbar.OnValueChangedAsObservable().Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue));
   mScrollRect.OnValueChangedAsObservable().Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue);
   mSlider.OnValueChangedAsObservable().Subscribe(sliderValue =>Debug.Log("Slider Value " + sliderValue));
   mInputField.OnValueChangedAsObservable().Subscribe(inputText =>Debug.Log("Input Text: " + inputText));
   mInputField.OnEndEditAsObservable().Subscribe(result =>Debug.Log("Result :" + result));
}

当然除了 Observable 增强,还⽀持了 Subscribe 的增强。
⽐如 SubscribeToText

Text resultText = GetComponent<Text>();
mInputField.OnValueChangedAsObservable().SubscribeToText(resultText);

这段代码实现的功能是,当 mInputField 的输⼊值改变,则会⻢上显示在 resultText 上。
也就是完成了, mInputField 与 resultText 的绑定。除此之外还⽀持, SubscribeToInteractable,之前有过示例。


Unity的生命周期与Trigger

生命周期

对于 Unity 的 Observable 增强,在之前就接触过了。Observable.EveryUpdate 就是⽀持的 Unity 的 API。单单 Update 就是⽀持⾮常多细分类型的 Update 事件捕获:

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

除了 Update 还⽀持其他的事件,⽐如 ApplicationPause, Quit 等。

Observable.EveryApplicationPause().Subscribe(paused => {});
Observable.EveryApplicationFocus().Subscribe(focused => {});
Observable.EveryApplicationQuit().Subscribe(_ => {}):

有了以上这些,就不⽤再去创建⼀个单例类去实现⼀个如 “应⽤程序退出事件监听” 这种逻辑了。

Trigger

Observable.EveryUpdate 这个 API 有的时候在某个脚本中实现,需要绑定 MonoBehaviour 的⽣命周期(主要是 OnDestroy),当然也有的时候是全局的,⽽且永远不会被销毁的。需要绑定 MonoBehaviour ⽣命周期的 EveryUpdate,之前说过需要⼀个 AddTo 来进⾏绑定。但其实这个功能有更简洁的实现方法:

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 的脚本。

Trigger 在此之前我们是接触过的。
AddTo 这个 API 其实是封装了⼀种 Trigger: ObservableDestroyTrigger

就是当 GameObject 销毁时获取事件的⼀个触发器。⼀般的 Trigger 都会配合 MonoBehaviour ⼀起使⽤。

⽐如 ObservableDestroyTrigger 的使⽤代码如下:

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

除了 Destroy 还有⾮常多的 Trigger。⽐如各种细分类型的 Update:

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

还有各种碰撞的 Trigger:

this.OnCollisionEnterAsObservable(collision => {});
this.OnCollisionExitAsObservable(collision => {});
this.OnCollisionStayAsObservable(collision => {});

同样 2D 的碰撞也⽀持

this.OnCollision2DEnterAsObservable(collision2D => {});
this.OnCollision2DExitAsObservable(collision2D => {});
this.OnCollision2DStayAsObservable(collision2D => {});

⼀些脚本的参数监听:

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

除了MonoBehaviour,Trigger也⽀持了其他组件类型,⽐如 RectTransform、Transform、UIBehaviour 等等。
详情可以查看 ObservableTriggerExtensions.cs 和 ObervableTriggerExtensions.Component.cs 中的API。


UI Trigger

这里的重点是 UIBehaviour ,它是 UGUI 所有控件的基类,所有的 UGUI 控件会继承 UIBehaviour 的⽀持。
例如,所有的 Graphic 类型都⽀持 OnPointerDownAsObservable、 OnPointerEnterAsObservable、OnPointerEnterAsObservable 等 Trigger。所有的在 Inspector 上显示, Raycast Target 选定的都是 Graphic 类型,包括Image、 Text 等都是,也就是说 Image、 Text 全部⽀持 OnPointerDownAsObservable、 OnPointerEnterAsObservable 等Trigger。

如果想⾃⼰去接收⼀个 OnPointerDown 事件,那就需要实现⼀个 IPointerDownHandler 接⼝,而 UniRx 则把所有的 IXXXHandler 接⼝都做成 Trigger了,这样再也不⽤ UIEventListener.Get(gameObejct).onClick 这种⽅式了,因为这种⽅式问题很多。
UniRx 的实现⾮常细,也就是⼀个 IXXXHandler 就是⼀个 Trigger。

⽤的⽐较多的⼏个 Trigger:

mImage.OnBeginDragAsObservable().Subscribe(dragEvent => {});
mGraphic.OnDragAsObservable().Subscribe(dragEvent => {});
mText.OnEndDragAsObservable().Subscribe(dragEvent => {});
mImage.OnPointerClickAsObservable().Subscribe(clickEvent => {});

⾮常⽅便,除了常⽤的⼏个 Trigger 之外 还有⾮常多的实⽤的 Trigger。⽐如: OnSubmitAsObservableOnDropAsObservable 等等。具体可以参考 ObservableTriggerExtensions.Component.cs,只要能想到的基本上 UniRx 都会⽀持。但是要使用各种 Trigger 类型,就要导⼊命名空间:

using UniRx.Triggers;

下面一个小例子,对一张图片添加几个事件:

        Image mImage;
        
        void Start()
        {
            mImage = transform.Find("Image").GetComponent<Image>();

            mImage.OnBeginDragAsObservable()
                  .Subscribe(_ =>
                  {
                      Debug.Log("begin drag");
                  });

            mImage.OnDragAsObservable()
                  .Subscribe(_ =>
                  {
                    Debug.Log("dragging");
                  });

            mImage.OnEndDragAsObservable()
                  .Subscribe(_ =>
                  {
                      Debug.Log("end drag");
                  });

            mImage.OnPointerClickAsObservable()
                  .Subscribe(_ =>
                  {
                      Debug.Log("on pointer click");
                  });
        }

Coroutine 的操作

UniRx 对 Unity Coroutine 的⽀持,可以将⼀个 Coroutine 转化为事件源(Observable)。如:

IEnumerator CoroutineA()
        {
            yield return new WaitForSeconds(1.0f);
            Debug.Log("A");
        }

        private void Start()
        {
            Observable.FromCoroutine(_ => CoroutineA())
                      .Subscribe(_ =>
                      {
                           // do something
                      });
        }

也⽀持将 Observable 转化为⼀个 Coroutine 中的 yield 对象。如:

IEnumerator Delay1Second()
        {
            yield return Observable.Timer(TimeSpan.FromSeconds(1.0f)).ToYieldInstruction();
            Debug.Log("delay 1 seconds");
        }

        void Start()
        {
            StartCoroutine(Delay1Second());
        }

FromCoroutineToYieldInstruction 实现了 Observable 与 Coroutine 之间的互相转化。
⽽在之前说过, Observable 是⼀条事件流。 UniRx 的操作符,⽐如 Merge 可以处理多个流,可以将流进⾏合并。除了合并也⽀持别的操作,⽐如 顺序 (依赖) 执⾏ Coroutine,并⾏执⾏ Coroutine 等等。在之后,通过学习新的操作符,可以让 Coroutine 更加强⼤。


WhenAll:Coroutine 的并⾏操作

WhenAll 的意思是,当所有的事件流都结束,就会触发 Subscribe 注册的回调。
使⽤ WhenAll 可以实现 Coroutine 的并⾏操作。如:

        IEnumerator A()
        {
            yield return new WaitForSeconds(1.0f);
            Debug.Log("A");
        }

        IEnumerator B()
        {
            yield return new WaitForSeconds(2.0f);
            Debug.Log("B");
        }

        void Start()
        {
            var aStream = Observable.FromCoroutine(_ => A());
            var bStream = Observable.FromCoroutine(_ => B());

            Observable.WhenAll(aStream, bStream)
                      .Subscribe(_ =>
                      {
                          Debug.Log("when all 处理了");
                      });
        }

可以看到输出结果的先后顺序:
在这里插入图片描述
WhenAllMerge 是同类型的,是处理多个流的操作符。

当然除了并⾏实现 Coroutine 之外,还可以实现:所有的按钮都点击过⼀次的逻辑。如:

void Start()
        {

            var aStream = mButtonA.OnClickAsObservable().First();
            var bStream = mButtonB.OnClickAsObservable().First();
            var cStream = mButtonC.OnClickAsObservable().First();

            Observable.WhenAll(
                        aStream,
                        bStream,
                        cStream)
            .Subscribe(_ =>
            {
                Debug.Log("All clicked");
                
            }).AddTo(this);
        }

当点击完, A、 B、 C 按钮之后,才会有输出。


事件流的结束 OnCompleted

UniRx 的结束事件。
有的事件流是有结束事件的,⽐如 Timer、 First、 Coroutine 等。但有的则没有,⽐如 EveryUpdate 等。使⽤ Subscribe API 进⾏订阅的时候,第⼀个参数是 OnNext 回调的注册,这也是我们⼤部分情况下使⽤的回调。第⼆个参数则是 OnComplete。

Observable.Timer(TimeSpan.FromSeconds(1.0f)).Subscribe(_ =>
            {
               Debug.Log("OnNext:after 1 second");
            }, () =>
            {
               Debug.Log("OnCompleted");
            });

从输出结果可以看到,Timer 是会结束的,另外一个,如:

Observable.EveryUpdate().First().Subscribe(_ =>
            {
               Debug.Log("OnNext:First");
            }, () =>
            {
               Debug.Log("OnCompleted");
            }).AddTo(this);

这样也是会结束的,以为它取得的只是第一帧,但是当把First()去掉的时候,就会发现,它就不会结束了。
再看一个协程的例子:

void Start()
        {            
            Observable.FromCoroutine(A)
                      .Subscribe(_ =>
                      {
                          Debug.Log("OnNext:");
                      }, () =>
                      {
                          Debug.Log("OnCompleted:");
                      });
        }

        IEnumerator A()
        {
            yield return new WaitForSeconds(2.0f);
        }

两秒后,会看到两个输出结果,证明它也结束了。


Start:让多线程更简单

在 Unity 中我们⼀般⽤ Thread.Start 开启⼀个线程,但是当逻辑⾮常复杂的时候多线程⾮常难以管理。而 UniRx 改善了这⼀种状况。⼀个”当所有线程运⾏完成后,在主线程执⾏某个任务” 这个功能,使⽤ UniRx 可以这样来实现:

        void Start()
        {
            var threadAStream = Observable.Start(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                return 10;
            });

            var threadBStream = Observable.Start(() =>
            {
                Thread.Sleep(TimeSpan.FromSeconds(3));
                return 10;
            });

            Observable.WhenAll(threadAStream, threadBStream)
                      .ObserveOnMainThread()
                      .Subscribe(results =>
                      {
                          Debug.LogFormat("{0}:{1}", results[0], results[1]);
                      });
        }

三秒钟后,会有结果输出。

在这⾥有两个新的 API,⼀个是 Observable.Start,这个 API 的意思是开启⼀个线程流。另一个是 ObserveOnMainThread,这个 API 的意思是把 WhellAll 的结果转到主线程上。这样 Subscribe ⾥的回调就可以使⽤Unity 的 API 了(Unity 的很多 API 不可以在其他线程中使⽤ )。使⽤ UniRx 来处理线程逻辑⾮常简单。线程和 Coroutine (协程)都可以使⽤ WhenAll 这种操作符。


ObservableWWW ⽹络请求操作

以往我们不管使⽤ WWW 还是 UnityWebRequest 都要使⽤ Coroutine 去驱动。但是使⽤协程写出来的代码,需要⼀堆判断,而 UniRx 则是保持以往的简练⻛格提供了对⽹络请求的⽀持。代码如下:

        void Start()
        {
            ObservableWWW.Get("http://sikiedu.com")
                         .Subscribe(responseText =>
                         {
                             Debug.Log(responseText);
                         }, e =>
                         {
                             Debug.LogError(e);
                         });
        }

会在编辑器中,输出这个网页的html源码。

ObservableWWW 同样⽀持 WhenAll 操作符。如:

       void Start()
        {
            var sikieduStream = ObservableWWW.Get("http://sikiedu.com");
            var qframeworkStream = ObservableWWW.Get("http://qframework.io");

            Observable.WhenAll(sikieduStream, qframeworkStream)
                      .Subscribe(responseTexts =>
                      {
                          Debug.Log(responseTexts[0].Substring(0, 100));
                          Debug.Log(responseTexts[1].Substring(0, 100));
                      });
        }

除了 Get 也⽀持了 Post,还有 GetWWW 和 PostWWW 这种的辅助封装,还有 GetAndGetBytes 和PostAndGetBytes。这里列举一个文件下载的例子:

        void Start()
        {
            var progressObservable = new ScheduledNotifier<float>();
            
            //前面为文件地址,后面为进度指定了一个参数
            ObservableWWW.GetAndGetBytes("http://liangxiegame.com/media/QFramework_v0.0.9.unitypackage", progress:progressObservable)
                         .Subscribe(bytes => 
                        { 
                           //do something 在文件下载后
                        });

            progressObservable.Subscribe(progress =>
            {
                Debug.LogFormat("进度为:{0}", progress);
            });
        }

在编辑器中会输出,下载的进度,从零开始到最后下载完成:
在这里插入图片描述
ObservableWWW 的 API 都可以传进去⼀个 ScheduledNotifier() ,⽤来监听下载进度。Subscribe 之后传回来的值则是,当前的进度。⽽且 ObservableWWW 的 Get 和 Post 请求都可以⾃⼰传对应的 header 和 WWWForm。除了常⽤的 Get 和 Post 请求,也对 AssetBundle 的加载也做了简单的封装。提供了诸如 ObservableWWW.LoadFromCacheOrDownload 这样的 API。如果想深⼊了解,可以参考 ObservableWWW.cs


ReactiveCommand

ReactiveCommand 的定义:

public interface IReactiveCommand<T> : IObservable<T>
{
     IReadOnlyReactiveProperty<bool> CanExecute { get; }
     bool Execute(T parameter);
}

它提供了两个 API:
• CanExecte
• Execute

Execute ⽅法是被外部调⽤的。也就是这个 Command 的执⾏。只要外部调⽤,Execute 就会执⾏。而 CanExecute 则是内部使⽤的,并且对外部提供了只读访问。

当 CanExecute 为 false 时,在外部调⽤ Execute 则该 Command 不会被执⾏。
当 CanExecute 为 true 时,在外部调⽤ Execute 则该 Command 会被执⾏。

它的 false 或 true ,有其他的其他的 Observable 来决定。
看下面这段代码:

        void Start()
        {
            var reactiveCommand = new ReactiveCommand();

            reactiveCommand.Subscribe(_ =>
            {
                Debug.Log("Execute");
            });

            reactiveCommand.Execute();
            reactiveCommand.Execute();
            reactiveCommand.Execute();
        }

因为新创建的 ReactiveCommand 默认 CanExecute 为 true,所以执行三次,会输出三次Log。

只要调⽤ Execute。 Command 就会通知 Subscribe 的回调(因为 CanExecute 为 true)。CanExecute 的开启关闭是由 Observable (事件源)决定的。再来看下面这段代码:

        void Start()
        {
            var mouseDownStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0)).Select(_ => true);
            var mouseUpStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonUp(0)).Select(_ => false);

            var isMouseUp = Observable.Merge(mouseUpStream, mouseDownStream);

            var reactiveCommand = new ReactiveCommand(isMouseUp, false);

            reactiveCommand.Subscribe(_ =>
            {
                Debug.Log("reactive command executed;");
            });

            Observable.EveryUpdate().Subscribe(_ =>
            {
                reactiveCommand.Execute();
            });
        }

运行后的结果是,当按下⿏标左键时会持续输出 ,当抬起⿏标时,则停⽌输出。

当然 ReactiveCommand 也是可以被订阅(Subscribe) 的,在订阅之前呢,也可以使⽤ Where 等操作符进⾏事件操作。来看:

        void Start()
        {
            var reactiveCommand = new ReactiveCommand<int>();

            reactiveCommand.Where(x => x % 2 == 0).Subscribe(x => Debug.LogFormat("{0} 是 偶数", x));
            reactiveCommand.Where(x => x % 2 != 0).Timestamp().Subscribe(x => Debug.LogFormat("{0} 是 奇数 {1}", x.Value, x.Timestamp));

            reactiveCommand.Execute(10);
            reactiveCommand.Execute(11);
        }

结果如下:
在这里插入图片描述


ReactiveCollection

ReactiveCollection 类似于 List。
我们可以使⽤如下的操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、 Remove)
来看一下代码:

ReactiveCollection<int> mAges = new ReactiveCollection<int> { 1, 2, 3, 4, 5 };

        void Start()
        {
            mAges.ObserveAdd()
                 .Subscribe(addAge =>
                 {
                    Debug.LogFormat("add:{0}", addAge);
                 });

            mAges.ObserveRemove()
                 .Subscribe(removedAge =>
                 {
                    Debug.LogFormat("remove:{0}", removedAge);
                 });

            mAges.ObserveCountChanged()
                 .Subscribe(count =>
                 {
                    Debug.LogFormat("count:{0}", count);
                 });

            foreach (var age in mAges)
            {
                Debug.Log(age);
            }
            
            mAges.Add(6);
            mAges.Remove(2);
        }

输出结果为:
在这里插入图片描述
可以看到,每一次的 添加、删除、数量改变,都会有输出。


ReactiveDictionary

ReactiveDictionary 功能与 Dictionary ⼀样。
同样地,它也⽀持了几个操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、 Remove)
来看一下代码:

        ReactiveDictionary<string, string> mLanguageCode = new ReactiveDictionary<string, string>
        {
            {"cn","中文"},
            {"en","英文"}
        };

        void Start()
        {
            mLanguageCode.ObserveAdd().Subscribe(addedLanguage => Debug.LogFormat("add:{0}", addedLanguage));
            mLanguageCode.ObserveRemove().Subscribe(removedLanguage => Debug.LogFormat("remove:{0}", removedLanguage));
            mLanguageCode.ObserveCountChanged().Subscribe(count => Debug.LogFormat("count:{0}", count));

            mLanguageCode.Add("jp", "日文");
            mLanguageCode.Remove("en");
        }

输出结果为:
在这里插入图片描述
每当涉及到对应操作,也都会有输出。


AsyncOperation 加载场景

我们在异步加载资源或者异步加载场景的时候往往会⽤到 AsyncOperation。UniRx 对 AsyncOperation 做了⽀持。使得加载操作可以很容易地监听加载进度。

        void Start()
        {
            //Resources.LoadAsync<GameObject>("TestCanvas").AsAsyncOperationObservable()
            //.Subscribe(resourceRequest =>
            //{
            //    Instantiate(resourceRequest.asset);
            //});
            var progressObservable = new ScheduledNotifier<float>();

            SceneManager.LoadSceneAsync(0).AsAsyncOperationObservable(progressObservable)
                        .Subscribe(_ =>
                        {
                            Debug.Log("load done");
                        });

            progressObservable.Subscribe(progress =>
            {
                Debug.LogFormat("加载了:{0}", progress);
            });
        }

输出的结果就是:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/THIOUSTHIOUS/article/details/86478062
今日推荐