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

定时功能

在开发中,经常用到的功能

以下从三种方式做一下比较

A:普通做法就是定义一个变量,通过记录,改变和判断来做

        private float mStartTime;

        void Start()
        {
            mStartTime = Time.time;
        }

        void Update()
        {
            if (Time.time - mStartTime > 5)
            {
                Debug.Log("do something");

                mStartTime = float.MaxValue;
            }
        }

B:再之后学到了协程,改变了一下方法

        void Start()
        {
            StartCoroutine(Timer(5, () =>
            {
                Debug.Log("do something");
            }));
        }

        IEnumerator Timer(float seconds,Action callback)
        {
            yield return new WaitForSeconds(seconds);
            callback();
        }

C:现在学习UniRx,方法再次改变

Observable.Timer(TimeSpan.FromSeconds(5.0f))
                      .Subscribe(_ =>
                      {
                          Debug.Log("do something");
                      });

这里需要注意一点,上面的代码是没有和 MonoBehaviour 进⾏⽣命周期绑定的,意思是当 this(MonoBehaviour) Destroy 的时候,这个延时逻辑不会被销毁掉,会造成空指针异常。但是要绑定也很简单,这里引出一个关键字,那就是AddTo,添加上就是:

Observable.Timer(TimeSpan.FromSeconds(5))
                    .Subscribe(_ => 
                    { 
                         Debug.Log("do something");
                    })
                   .AddTo(this);

这样,当 this(MonoBehaviour) Destroy 的时候,这个延时逻辑也会销毁掉,从⽽避免造成空指针异常。


AddTo

既然提到了AddTo,那就来介绍一下。

其实字⾯意思上理解很简单,就是添加到。添加到哪⾥呢?其实就是 Unity 的 GameObject 或者 MonoBehaviour。 WHY?

是因为, GameObject 和 MonoBehaviour 可以获取到 OnDestroy 事件。也就是 GameObject 或 MonoBehaviour 的销毁事件。

这个销毁事件,是⽤来与 UniRx 进⾏销毁事件的绑定,也就是当 GameObject 或者 MonoBehaviour 被销毁时,同样去销毁正在进⾏的 UniRx 任务。

本质上, AddTo 是⼀个 静态扩展关键字,他对 IDisposable 进⾏了扩展。

其实只要任何实现了 IDisposable 的接⼝,都可以使⽤ AddTo API,不管是不是 UniRx 的 API。
当 GameObject 销毁时,就会调⽤ IDisposable 的 OnDispose 这个⽅法。

有了 AddTo,在开启 Observable.EveryUpdate 时调⽤ 当前脚本的⽅法,则不会造成引⽤异常等错误,它使得 UniRx 的使⽤
更加安全


相互独立的Update

在之前的时候,有过一些⽐较麻烦的情况。就是在 MonoBehaviour 的 Update 中掺杂了⼤量互相⽆关的逻辑,致使代码可读性降低。但是 UniRx 可以改善这个问题。下面的例子,使用了 UniRx 的方法代替了在原来的Update里面写上很多判断和方法,把它们都相互分开了。

void Start()
        {

            bool buttonClicked = false;

            // 方法A:监听鼠标左键
            Observable.EveryUpdate()
                      .Subscribe(_ =>
                      {
                          if (Input.GetMouseButtonDown(0))
                          {
                              Debug.Log("left mouse button clicked");
                              buttonClicked = true;
                          }
                      })
                      .AddTo(this);

            // 方法B:监听鼠标右键
            Observable.EveryUpdate()
                      .Subscribe(_ =>
                      {
                          if (Input.GetMouseButtonDown(1))
                          {
                              Debug.Log("right mouse button clicked");
                              buttonClicked = true;
                          }
                      })
                      .AddTo(this);
        }

虽然在代码⻓度上没有任何改善,但是最起码,这些 Update 中的逻辑互相之间独⽴了。

状态跳转、延时等等这些经常在 Update ⾥实现的逻辑,都可以使⽤以上这种⽅式独⽴。

不过这种 UniRx 的使⽤还⽐较初级,随着对 UniRx 的深⼊,也会渐渐淘汰,因为后边有更好的实现⽅式。


UniRx 的基本语法格式

这里列举一个例子

private void Start()
{
     Observable.Timer(TimeSpan.FromSeconds(2.0f)).Subscribe(_ =>
     {
         Debug.Log("延时两秒");
         
     }).AddTo(this);
}

.Observable.XXX().Subscribe() 是⾮常典型的 UniRx 格式。

这里可以根据词汇的理解来看:

订阅可被观察的定时器。
从发布者到订阅者。
其概念关系很容易理解。
• Timer 是可观察的。
• 可观察的才能被订阅。

UniRx 的侧重点,是事件从发布者到订阅者之间的过程如何处理,重要的是两点之间的线,也就是事件的传递过程。


操作符 Where

之前有一段代码:

Observable.EveryUpdate()
       .Subscribe(_ =>
       {
            if (Input.GetMouseButtonUp(0))
            {
                // do something
            }
       }).AddTo(this);

这段代码实现了⿏标按钮的点击事件处理,但是这段代码,还不够简洁。如何更加简洁?
那就使⽤Where 操作符,代码如下:

Observable.EveryUpdate()2
       .Where(_ => Input.GetMouseButtonUp(0))
       .Subscribe(_ =>
       {
            // do something
       }).AddTo(this);

Where 在这⾥,我们可以理解成⼀个条件语句,也就是 if 语句,相当于是 if (Input.GetMouseButtonUp(0))

也就是说,Where 是⼀个过滤的操作,过滤掉不满⾜条件的事件。

  1. EveryUpdate 是事件的发布者。他会每帧会发送⼀个事件过来。
  2. Subscribe 是事件的接收者,接收的是 EveryUpdate 发送的事件。
  3. Where 则是在事件的发布者和接收者之间的⼀个过滤操作。会过滤掉不满⾜条件的事件。

所以, Subscribe 处理的事件,都是满⾜ Input.GetMouseButtonUp(0) 条件的事件。


操作符 First

在之前介绍 UniRx 的时候,有过这样一段代码,实现的是“只处理第⼀次⿏标点击事件”这个功能:

Observable.EveryUpdate()
          .Where(_ => Input.GetMouseButtonUp(0))
          .First()
          .Subscribe(_ => { /* do something */ })
          .AddTo(this);

事件通过 Where 过滤之后,⼜通过 First 进⾏了⼀次过滤。

但是First 还可以传⼀个条件进去。也就是说,可以不使⽤ Where 就可以搞定,简化后是:

Observable.EveryUpdate()
          .First(_ => Input.GetMouseButtonUp(0))
          .Subscribe(_ => { /* do something */ })
          .AddTo(this);

对 UGUI 的支持

比如按钮点击事件的注册:

button.OnClickAsObservable()
      .First()
      .Subscribe(_ =>
      {
           Debug.Log("button clicked");
      });

对 Toggle 的注册:

 toggle.OnValueChangedAsObservable()
       .Where(on => on)
       .Subscribe(on =>
       {
             Debug.LogFormat("toggle value changed: {0}", on);
       });

还⽀持 EventSystem 的各种 Trigger 接⼝的监听。

举一个小例子:

⽐如: Image 本身是 Graphic 类型的, Graphic 类,只要实现 IDragHandler 就可以进⾏拖拽事件的监听。
但是使⽤ UniRx 就不⽤那么麻烦。

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

ReactiveProperty

它名为响应式属性,它可以替代⼀切变量,给变量创造了很多功能。

假如我们想监听⼀个值是否发⽣了改变,可以这样实现:

public class ReactivePropertyExample : MonoBehaviour
    {
        //public IntReactiveProperty Age = new IntReactiveProperty(0);
        public ReactiveProperty<int> Age = new ReactiveProperty<int>();
        
        void Start()
        {
            Age.Subscribe(age =>
            {
                Debug.Log("inner received age changed");
            });

            Age.Value = 10;
        }
    }

    //在外部调用
    public class PersonView
    {
        ReactivePropertyExample mReactiveProeprtyExample;

        void Init()
        {
            mReactiveProeprtyExample.Age.Subscribe((age) =>
            {
                Debug.Log(age);
            });
        }
    }

当任何时候, Age 的值被设置后,就会通知所有 Subscribe 的回调函数。

而 Age 可以被 Subscribe 多次的。
并且同样⽀持 First、 Where 等操作符。

这样可以实现⼀个叫做 MVP 的架构模式。也就是 在 Ctrl 中,进⾏ Model 和 View 的绑定。Model 的所有属性都是⽤ ReactiveProperty,然后在 Ctrl 中进⾏订阅。通过 View 更改 Model 的属性值。形成⼀个 View->Ctrl->Model->Ctrl->View 这样的事件响应环。


MVP模式

借着 ReactiveProperty 的提出,来实现一个简易的 MVP 模式:

EnemyModel mEnemy = new EnemyModel(200);

 void Start()
        {
            var attackBtn = transform.Find("Button").GetComponent<Button>();
            var HPText = transform.Find("Text").GetComponent<Text>();

            attackBtn.OnClickAsObservable()
                     .Subscribe(_ =>
                     {
                         mEnemy.HP.Value -= 99;
                     });

            mEnemy.HP.SubscribeToText(HPText);

            mEnemy.IsDead
                  .Where(isDead => isDead)
                  .SubscribeToInteractable(attackBtn);
        }
    }

    // Model
    public class EnemyModel
    {
        public ReactiveProperty<long> HP;

        public IReadOnlyReactiveProperty<bool> IsDead;

        public EnemyModel(long initialHP)
        {
            HP = new ReactiveProperty<long>(initialHP);

            IsDead = HP.Select(hp => hp <= 0).ToReactiveProperty();
        }
    }

这段代码理解起来⾮常简单, Enemy 是⼀个数据类,我们可以理解成 Model。
而 Start 部分则是 Ctrl 的代码。它将 Hierarchy 中的 UI 控件 与 Model 绑定在了⼀起。当Model 有改变则通知 UI 更新,当从 UI 接收到点击事件则对 Model 进⾏值的更改。这就是⼀个⾮常简单的 MVP 模式。


操作符 Merge

Merge 的意思是合并。
在 UniRx 的世界⾥,任何东⻄都是以事件流的形式存在的。
而在之前使⽤的 Update、 Timer 等,全都是开启了⼀条事件流。
但是 UniRx 可以开启两个或多个事件流,并使⽤ Merge 进⾏事件流的合并:

void Start()
        {
            var leftMouseClickStream  = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0));
            var rightMouseClickStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(1));

            Observable.Merge(leftMouseClickStream, rightMouseClickStream )
                      .Subscribe(_ =>
                      {
                          Debug.Log("mouse clicked");
                      });
        }

以上代码的实现的逻辑是 “当⿏标左键 右键点击时都会进⾏处理“。
也就是说, Merge 操作符将 leftMouseClickStream 和 rightMouseClickStream 合并成了⼀个事件流。


实践小案例:当前页面,只有第一次按钮点击有效

代码:

void Start()
        {
            var btnA = transform.Find("ButtonA").GetComponent<Button>();
            var btnB = transform.Find("ButtonB").GetComponent<Button>();
            var btnC = transform.Find("ButtonC").GetComponent<Button>();

            var aStream = btnA.OnClickAsObservable().Select(_ => "A");
            var bStream = btnB.OnClickAsObservable().Select(_ => "B");
            var cStream = btnC.OnClickAsObservable().Select(_ => "C");

            Observable.Merge(aStream, bStream, cStream)
                      .First()
                      .Subscribe(btnId =>
                      {
                          Debug.LogFormat("button {0} clicked", btnId);
                          Observable.Timer(TimeSpan.FromSeconds(1.0f)).Subscribe(__ =>
                          {
                              gameObject.SetActive(false);
                          });
                      });
        }

这里还处理了一个问题,就是当处理按钮事件的时候要知道是哪个按钮被点击了,于是使⽤了 Select 操作符。
换言之,就是对应着,每个按钮自己所对应的事件。

猜你喜欢

转载自blog.csdn.net/THIOUSTHIOUS/article/details/86431741