随风逐叶 随风逐叶
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档

rontian

从事游戏开发10多年的老菜鸟一枚!
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档
  • 框架简介
  • TinaX.Core
  • 基于TinaX创建一个扩展库
  • TinaX.VFS
  • TinaX.UIKit
  • TinaX.I18N
  • TinaX.Lua
  • XLua

  • Google.Protobuf
  • Lua-Protobuf
  • 一些优秀的第三方库

    • CatLib

    • UniRx

      • 简介
      • 基础教程
      • 官方入门文档
      • UniRx入门系列一
      • UniRx入门系列二
      • UniRx入门系列三
      • UniRx入门系列四
        • UniRx入门系列五
      • UniTask

    目录

    UniRx入门系列四

    # UniRx入门系列四

    # 上节回顾


    上次我们介绍了几种构建流的方式。这节,我想把重点放在更具实用性的转换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");
            });
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    以上方法可以将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);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    # UpdateAsObservable工作原理

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

    /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary>
    public static IObservable<Unit> UpdateAsObservable(this Component component)
        {
            if (component == null || component.gameObject == null) return Observable.Empty<Unit>();
            
            return GetOrAddComponent<ObservableUpdateTrigger>(component.gameObject).UpdateAsObservable();
        }
    1234567
    using System; // require keep for Windows Universal App
    using UnityEngine;
    namespace UniRx.Triggers
    {
        [DisallowMultipleComponent]
        public class ObservableUpdateTrigger : ObservableTriggerBase
        {
            Subject<Unit> update;
    
            /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary>
            void Update()
            {
                if (update != null) update.OnNext(Unit.Default);
            }
    
            /// <summary>Update is called every frame, if the MonoBehaviour is enabled.</summary>
            public IObservable<Unit> UpdateAsObservable()
            {
                return update ?? (update = new Subject<Unit>());
            }
    
            protected override void RaiseOnCompletedOnDestroy()
            {
                if (update != null)
                {
                    update.OnCompleted();
                }
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    当调用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!")
                );
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    其使用方式和上面说过的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);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

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

    using UnityEngine;
    using UniRx;
    public class TestUniRX : MonoBehaviour
    {
        void Start()
        {
            Observable.EveryUpdate()
            .Subscribe(frameCount =>
            {
                Debug.Log(frameCount);
            }).AddTo(gameObject);
            
            Destroy(gameObject, 2.0f);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 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");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    使用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");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52

    使用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");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    比较上述两种代码的实现差异,如果没有使用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);
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

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

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

    上次更新: 2023/10/17, 14:09:52 访问次数: 0
    UniRx入门系列三
    UniRx入门系列五

    ← UniRx入门系列三 UniRx入门系列五→

    最近更新
    01
    一些Shell常用的功能写法整理
    10-20
    02
    删除git仓库submodule的步骤
    10-20
    03
    django基本命令
    10-16
    更多文章>
    Copyright © 2017-2025 随风逐叶
    沪ICP备18008791号-1 | 沪公网安备31011502401077号

    网站访问总次数: 0次
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式