UniRx入门系列(四)

原文链接: https://qiita.com/toRisouP/items/30c576c7b0a99f41fb87

上节回顾


上次我们介绍了几种构建流的方式。这节,我想把重点放在更具实用性的转换Unity的Updaet方法上。在本节中,我们将介绍如何使用UniRx将Unity中的Update转化为UniRx中的Observable.

如何将Update转化为流


有两种方法可以将Unity Update方法转化为流:

  • UniRx.Triggers中的UpdateAsObservable
  • Observable 中的EveryUpdate

上述两个方法在使用和操作上是相同的,但是其内部实现有很大的不同,下面我解释一下他们各自的用法和工作原理

UniRx.Triggers中的UpdateAsObservable


使用方法

1.using UniRx.Triggers;

2.this.UpdateAsObservable()

传递Unit类型数据

using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
    void Start()
    {
        this.UpdateAsObservable()
        .Subscribe(_ =>
        {
            Debug.Log("Update");
        });
    }
}

以上方法可以将Update事件转换成UniRx的流来使用。当流的GameObject 被销毁时会自动触发OnCompleted,此时,流的生命周期管理变得更加容易。

using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
    void Start()
    {
        this.UpdateAsObservable()
        .Subscribe(
            onNext: _ =>
            {
                Debug.Log("Update");
            },
            onCompleted: () =>
            {
                Debug.Log("OnCompleted");
            }
        );
        this.OnDestroyAsObservable()
        .Subscribe(_ =>
        {
            Debug.Log("OnDestroy");
        });

        Destroy(gameObject, 2.0f);
    }
}

UpdateAsObservable工作原理

UpdateAsObservable流是ObservableUpdateTrigger组件中具有实体的流。在调用UpdateAsObservable时,UniRx会自动触发相关GameObject的ObservableUpdateTrigger组件,利用这个组件发出相应的事件。

/// Update is called every frame, if the MonoBehaviour is enabled.
public static IObservable<Unit> UpdateAsObservable(this Component component)
    {
        if (component == null || component.gameObject == null) return Observable.Empty<Unit>();
        
        return GetOrAddComponent<ObservableUpdateTrigger>(component.gameObject).UpdateAsObservable();
    }
using System; // require keep for Windows Universal App
using UnityEngine;
namespace UniRx.Triggers
{
    [DisallowMultipleComponent]
    public class ObservableUpdateTrigger : ObservableTriggerBase
    {
        Subject<Unit> update;

        /// Update is called every frame, if the MonoBehaviour is enabled.
        void Update()
        {
            if (update != null) update.OnNext(Unit.Default);
        }

        /// Update is called every frame, if the MonoBehaviour is enabled.
        public IObservable<Unit> UpdateAsObservable()
        {
            return update ?? (update = new Subject<Unit>());
        }

        protected override void RaiseOnCompletedOnDestroy()
        {
            if (update != null)
            {
                update.OnCompleted();
            }
        }
    }
}

当调用UpdateAsObservable时,UniRx将添加一个ObservableUpdateTrigger组件到调用的GameObject对象上,并在执行ObserverUpdateTrigger中的Update时,将Update的行为作为UniRx的事件发布。注意一下两点:

  • ObservableUpdateTrigger 这个组件在你调用UpdateAsObservable时,UniRx会自动为调用组件附加的,请不要随意删除。
  • 因为每一个GameObject会共享一个ObservableUpdateTrigger,所以即使Subscribe本身有大量的Subscribe,也不会有过多的性能负担

Observable.EveryUpdate


使用方法

1.Observable.EveryUpdate()

传递long型数据

Subscribe(所经过的帧数)

using UnityEngine;
using UniRx;
public class UpdateSample : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
            .Subscribe(
                _ => Debug.Log("Update!")
            );
    }
}

其使用方式和上面说过的UpdateAsObservable基本一样。然而,需要注意一点的是,Observable.EveryUpdate()并不会自动发布OnComplete.他不像UniRx。Triggers中的 this.UpdateAsObservable() 那样,生命周期和GameObject 的生命周期有关联。所以,当你使用Observable.EvenryUpdate时你需要自己进行流体的生命周期管理。

未手动释放流,就算当前GameObject被销毁,流依然执行:

using UnityEngine;
using UniRx;
public class TestUniRX : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
        .Subscribe(frameCount =>
        {
            Debug.Log(frameCount);
        });
        Destroy(gameObject, 2.0f);
    }
}

在执行过程中,将流与当前GameObject生命周期关联,当前对象被释放时,流也被释放:

using UnityEngine;
using UniRx;
public class TestUniRX : MonoBehaviour
{
    void Start()
    {
        Observable.EveryUpdate()
        .Subscribe(frameCount =>
        {
            Debug.Log(frameCount);
        }).AddTo(gameObject);
        
        Destroy(gameObject, 2.0f);
    }
}

Observable.EveryUpdate的工作原理

Observable.EveryUpdate是利用UniRx提供的功能之一的微协程来运行的,其工作原理相对复杂一些。简单来说,就是当每次执行Observable.EveryUpdate时。他就会启动一个协程,除非你手动去终止这个协程,否则,协程会一直执行下去;所以说,管理好刘德生命周期很重要。然而,另外一方面,使用Observable.EveryUpdate有以下好处:

  • 因为Observable.EveryUpdate在单例上执行,可以用与在游戏过程中一直存在的流
  • 大量的Subscribe不会降低性能(微协程的概念)

另外MainThreadDispatcher是由一个单例创建的的GameObject.在使用UniRx的过程中,你可能会发现,有些东西是由UniRx生成的,这些东西对于UniRx来说也是必要的,所以不要随意删除他们。

UpdateAsObservable和Observable.EveryUpdate的使用区别

虽然二者的操作相似,但是内部实现有很大的不同,你应该清楚的了解每一个操作的工作原理,并根据你自己的实际情况来选择使用合适的方法。


  • UpdateAsObservable() 生命周期与GameObject的生命周期相关联,如果GameObject被销毁,流会被自动销毁
  • Observable.EveryUpdate 使用起来很方便,但是需要手动进行 Dispose

UpdateAsObservable的使用场所

一些适合使用UpdateAsObservable的地方:

  • 流的生命周期与GmaeObject的生命周期相关,如果GameObject被销毁,会自动发布OnCompleted。

Observable.EveryUpdate的使用场所

一些适合使用Observable.EveryUpdate的地方:

  • 当你想在不使用GameObject的纯类中使用Update事件
    • 通过单例获取Update事件,因此,你可以在不继承子MonoBehaviour的情况下使用
  • 在游戏周期中始终存在的流
    • 使用单例模式,不会自动发布OnCompleted
  • 需要频繁调用访问调用Update

因为使用了UniRx中的微协程,当你需要使用大量Update时调用时,它的性能表现比原生Unity提供的Update调用要好的多。


当然,选择使用哪种方式,全凭你自己的喜好。有人说,Observable.EvenryUpdate的表现更为出色,但是会经常忘记去手动释放它。假如你出了一个bug,流回停止,这是很好的;可怕的是,出现了一个bug,流并没有停止,而是继续向后传递,这是非常让人苦恼的。因此,不管性能上有多大差异,我个人还是比较建议使用UpdateAsObservable.

将Update转化为UniRx流的好处


在我看来,最因该使用UniRx的一个理由就是可以流化Update,将Update流化之后,你就可以:

  • 使用UniRx提供的操作符来描述游戏逻辑
  • 游戏逻辑将变得更加清晰

使用UniRx的操作符来组织游戏逻辑


UniRx中提供了大量与时间相关的操作符,所以,只需要使用UniRx提供的操作符,就可以很简洁的描述与时间相关的游戏逻辑。举个例子,一个射击游戏,当你点击发射按钮时,你可以在一定时间间隔内执行攻击动作。比如,射击游戏中的子弹发射,你按下了一个按钮,每隔n秒发射一次子弹。我们使用Unity的操作方式来实现的话,相对会比较繁琐,需要记录一些具体的时间点。但是,如果使用UniRx,实现如下:

using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
    private float interValSeconds = 0.25f;
    void Start()
    {
        this.UpdateAsObservable()
        .Where(_ => Input.GetKey(KeyCode.LeftControl))
        .ThrottleFirst(TimeSpan.FromSeconds(interValSeconds))
        .Subscribe(_ => Attack());
    }

    private void Attack()
    {
        Debug.Log("Attack");
    }
}

使用UniRx就可以向使用LINQ一样简洁的处理游戏逻辑。在大多数情况下,我们会在Update中塞入大量的判断逻辑,使得Update的逻辑变得杂乱无章,使用UniRx来处理,就可以避免这样的情况。

一个简单的控制角色移动、跳跃、攻击的例子

下方代码尝试描述一个常见的控制角色移动、跳跃、攻击的逻辑;在不适用UniRx的情况下,代码如下:

using UnityEngine;
public class TestUniRX : MonoBehaviour
{
    private CharacterController characterController;
    private bool isJumping;
    void Start()
    {
        characterController = GetComponent<CharacterController>();
    }
    void Update()
    {
        if (!isJumping)
        {
            var inputVector = new Vector3(
                Input.GetAxis("Horizontal"),
                0,
                Input.GetAxis("Vertical")
            );

            if (inputVector.magnitude > 0.1f)
            {
                var dir = inputVector.normalized;
                Move(dir);
            }
            if (Input.GetKeyDown(KeyCode.Space) && characterController.isGrounded)
            {
                Jump();
                isJumping = true;
            }
        }
        else
        {
            if (characterController.isGrounded)
            {
                isJumping = false;
                PlaySoundEffect();
            }
        }
    }
    private void PlaySoundEffect()
    {
        Debug.Log("播放音效");
    }
    private void Jump()
    {
        Debug.Log("跳跃");
    }
    private void Move(Vector3 dir)
    {
        Debug.Log("Move");
    }
}

使用UniRx实现上面描述的逻辑功能:

using UnityEngine;
using UniRx.Triggers;
using UniRx;
public class TestUniRX : MonoBehaviour
{
    private CharacterController characterController;
    private BoolReactiveProperty isJumping = new BoolReactiveProperty();
    void Start()
    {
        characterController = GetComponent<CharacterController>();

        this.UpdateAsObservable()
        .Where(_ => !isJumping.Value)
        .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
        .Where(x => x.magnitude > 0.1f)
        .Subscribe(x => Move(x.normalized));

        this.UpdateAsObservable()
        .Where(_ => Input.GetKeyDown(KeyCode.Space) 
            && !isJumping.Value && characterController.isGrounded)
        .Subscribe(_ =>
        {
            Jump();
            isJumping.Value = true;
        });

        characterController
        .ObserveEveryValueChanged(x => x.isGrounded)
        .Where(x => x && isJumping.Value)
        .Subscribe(_ => isJumping.Value = false)
        .AddTo(gameObject);

        isJumping.Where(x => !x)
        .Subscribe(_ => PlaySoundEffect());
    }

    private void PlaySoundEffect()
    {
        Debug.Log("播放音效");
    }

    private void Jump()
    {
        Debug.Log("Jump");
    }

    private void Move(Vector3 normalized)
    {
        Debug.Log("Move");
    }
}

比较上述两种代码的实现差异,如果没有使用UniRx,在Update中将会充斥着大量的判断逻辑,大量if语句的出现,会扰乱变量范围。但是,如果我们换成UniRx流来流化Update,我们就可以将处理划分逻辑单元来并排描述,并且变量的作用范围也被封闭在了流中。通过将Update流化,这样我们就可以用合适的单元来分段描述处理过程,同时也可以清楚的看到变量的作用范围。

总结


两种将Update转化为流的方式

  • 通常使用UpdateAsObservable 的方式
  • 特殊情况下使用Observable.EveryUpdate()

补充

ObserveEveryValueChanged

 void Start()
    {
        var characterController = GetComponent<CharacterController>();

        characterController
        .ObserveEveryValueChanged(x => x.isGrounded)
        .Where(x => x)
        .Subscribe(_ => Debug.Log("在地面"))
        .AddTo(gameObject);

        Observable.EveryUpdate()
        .Select(_ => characterController.isGrounded)
        .DistinctUntilChanged()
        .Where(x => x)
        .Subscribe(_ => Debug.Log("着地"))
        .AddTo(gameObject);
    }

上回描述了,ObserveEveryValueChanged是相当于… . . . . . (Observable) . . (EveryUpdate + Select + DistinctUntilChanged)的省略记法,事实上,这个解释不太正确。

ObserveEveryValueChanged 持有待监视对象的弱引用。换句话说,在ObserveEveryValueChanged的监视不会被计入GC的引用计数。另外,当被监视的对象被GC回收时,ObserveEveryValueChanged会自动发布OnCompleted.

更多内容欢迎访问:


UniRx入门系列(四)_第1张图片

你可能感兴趣的:(UniRx)