找回密码
 立即注册
查看: 283|回复: 3

[Unity3D]UniRx介绍以及使用

[复制链接]
发表于 2022-6-25 18:14 | 显示全部楼层 |阅读模式
国庆回来,发现有一段时间没写东西,今天继续更新一个比较好用的东西.
UniRx:
送上AssetStore链接:
UniRx - Reactive Extensions for Unity | Integration | Unity Asset Store
这是一个unity官方商店里一个免费的插件,看完本片文章,如果感兴趣的可以去下载学习学习.
什么是UniRx:一个响应式框架(链式编程),Unity编程框架,专注于解决时间上的异步.
UniRx简单示例先入个门:
public class UniRxExample : MonoBehaviour
        {
                private void Awake()
                {
                        Debug.Log("开始时间:" + Time.realtimeSinceStartup);
                        Observable.Timer(TimeSpan.FromSeconds(2))
                                .Subscribe(_ => {

                                        Debug.Log("延时2秒"+Time.realtimeSinceStartup);

                                }).AddTo(this);
                }
        }

直接看代码可能看不懂,简单解释一下,这是一个平时我们经常使用的一个延时的功能,可能一般情况我们是使用协程来做延时,但是协程很麻烦,需要开启,还需要关闭,而UniRx的实现方式更加简洁优雅.
代码做的事情很简单: 开启一个时间上的异步监听,Subscribe订阅需要处理的事情.AddTo(this)表示当前类防止销毁之后的异常处理,就像生命周期一样.Timer和Observable是一起的就跟发布者一样,当触发条件,或者条件达到的时候就会发送事件流到Subscribe中处理订阅者需要的逻辑。
看到这里可能会有点发布者订阅者设计模式意思.的确是这样的,Observable目前就理解一个发布者.Subscribe可以理解成订阅者.Timer是一个可以被观察,只有可以被观察的才可以被订阅.
这里算是一个简单的入门,因为当你熟悉之后,其实Rx 就是一个使用可观察集合和LINQ风格查询运算符组合成的基于异步和基于事件的可编程库.
为什么使用Rx:
通常,在Unity对网络操作要求使用WWW和Coroutine.但是出于以下几点原因(或者其它原因)使用协程来进行异步操作并不顺手:

  • 虽然协程的返回类型必须是IEnumerator,但是协程不能返回任何值。
  • 因为yield return 语句不能被try-catch结构体包裹,协程中不能处理异常。
  • 实现mvp 架构模式
  • 对UGUI进行了大量的增强
这种缺乏可组合性导致程序的紧耦合,往往造成IEnumators中逻辑过于复杂。
Rx可以解决异步调用的“伤痛”,Rx 是一个使用可观察集合和LINQ风格查询运算符组合成的基于异步和基于事件的可编程库。
游戏循环(every Update,OnCollisionEnter),传感器数据(Kinect,Leap Motion,VR Input 等等)这些类型的事件。Rx将事件表示为响应式序列。通过使用LINQ查询运算符,Rx变得容易组合且支持基于时间的操作。
引出Rx链式编程:
在前面的入门示例代码中,发现直接点点点引出下一个方法,编码方式就跟套娃一样,一层套一层.
如果熟悉过Linq编程的可能知道,Linq是操作数+关键字的的形式,RX同样支持Linq一些关键字,而且比Linq更加强大.
简单示例一个代码:
Linq:
private void Awake()
                {

                        List<int> number = new List<int> { 1, 52, 61, 20, 15, 60 };

                        number.Where(nu => nu > 20)
                                .ToList()
                                .ForEach(nu => { Debug.Log(nu); });

                       
                }
UniRx:
Observable.EveryUpdate()
                                .Where(_ => Input.GetMouseButtonDown(0))
                                .Subscribe(_ => {

                                        Debug.Log("鼠标点击");
                                });
运行结果:


可以看出,两个代码还是很像的,像wher这种操作符还有很多,比如Select,Frist.还有一些只有RX特有的,例如Repeat,skip,take等等,
从这个图(图在结尾)就可以看出RX的强大了.RX不仅支持一些像linq一些关键字,还有一些对monobehaviour脚本生命周期函数的支持,以及UGUI.接下来我在unity中实现一些代码:
public class RxLifeCycle : MonoBehaviour
        {
                private void Awake()
                {
                        Observable.EveryUpdate().Subscribe(_ => {
                                Debug.Log("类似于 unity updata");

                        }).AddTo(this);

                        Observable.EveryLateUpdate().Subscribe(_ => {
                                Debug.Log("类似于 unity Lateupdata");

                        }).AddTo(this);


                        Observable.EveryFixedUpdate().Subscribe(_ => {
                                Debug.Log("类似于 unity Fixedupdata");

                        }).AddTo(this);

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

                                Debug.Log("EveryEndOfFrame");
                        }).AddTo(this);
                }


可以结合一些关键字实现一些比较好用的操作,例如当你点击鼠标点击之后,输出一个结果,或者发送一个事件,一个回调函数
namespace SYFramework
{
        public class RxLifeCycle : MonoBehaviour
        {
                private void Awake()
                {
                        Observable.EveryUpdate().
                                Where(_=>Input.GetMouseButtonDown(0)) //相当于if 判断
                                .First() //只执行一次  Subscribe:订阅的事件或者回调
                                .Subscribe(_ => {
                                Debug.Log("鼠标只能点击一次");

                        }).AddTo(this);

                       
                }
        }

}


是不是很实用,只需要一个简单的代码就可以执行一个复杂的操作,而且代码还很简洁。如果你熟练使用之后,在你的项目中使用,会一劳永逸的。
Rx关键字:
在上面的示例代码中使用了常用的关键字,其实Rx可以理解成:
Rx = Observables + LINQ + Schedulers。
微软给的定义是,Rx 是一个函数库,让开发者可以利用可观察序列和 LINQ 风格查询操作符来编写异步和基于事件的程序,使用 Rx,开发者可以用 Observables 表示异步数据流,用 LINQ 操作符查询异步数据流, 用 Schedulers 参数化异步数据流的并发处理。
那接下来学习Rx一定要学习关键字和类似于LINQ查询操作符了。
Where :范围限定 条件过滤
Select:(以什么样的结果输出) 变换 选择什么样的结果做为输出成集合中 用于使用  
addto:生命周期 游戏物体销毁的时候一同销毁
Skip(value): (Linq: 跳过序列中指定数量的元素 然后返回剩余的元素)忽略初始值设置 可以设置1 第一次忽略
Observable.FromCoroutine(_ => StartIe()): 协程调用
ReactiveCommand: Execute():  CanExecute();
var proge = new ScheduledNotifier<float>():  加载进度
Frist:只做一次 判断
Distinct :去重
last: 取最后一个数据 可以使用条件表达式进行判断 输出
SelectMany:(Linq解释:将序列的每个元素投影到 IEnumerable<T> 并将结果序列合并为一个序列。 对每项再进行遍历处理再进行合成序列。)
Take :连续获取指定个数的事件
Concat:连接两个事件源
WhenAll:(linq 解释:确定序列中的所有元素是否都满足条件) 监听所有的事件源的完成完成事件 并行执行Observables
OfType: (linq:获取指定类型) 过滤指定类型的元素 事件
Cast:(Linq:将 IEnumerable 的元素强制转换为指定的类型) UniRx一样
GroupBy:(Linq:对序列中的元素进行分组。) UniRx:根据key 分组
Range:(Linq:生成指定范围内的整数的序列。)
TakeWhile:(Linq: 如果指定的条件为 true,则返回序列中的元素,然后跳过剩余的元素。)一直接收 直到条件满足
SkipWhile:(linq:如果指定的条件为 true,则跳过序列中的元素,然后返回剩余的元素。)一直忽略 直到满足事件
Zip:(,net4支持操作符 将指定函数应用于两个序列的对应元素,以生成结果序列。) 将一个匿名桉树,应用于两个事件源 函数会成为对处理两个事件源的事件
Repeat:(Linq: 在生成序列中重复该值的次数。)重复生成事件
TakeLast :Observable 完成后获取最后一个或指定个数
Single :(Linq:返回序列中的单个特定元素,与 First 非常类似,但是 Single 要确保其满足条件的元素在序列中只有一个。) 查询一个符合条件的元素
ToArray :(linq:创建数组)将完成的事件组成一个数组
ToList:(linq:创建list 集合) 将完成的事件组成一个list集合
Aggregate:(linq:对序列应用累加器函数。 将指定的种子值用作累加器的初始值,并使用指定的函数选择结果值。)聚合 实现累加或者累乘的功能
Empty:(Linq:返回具有指定类型参数的空 IEnumerable<T>)空的Observable

UniRx 特有的关键字:
Interval:         Observable.Interval(TimeSpan.FromSeconds(1f))  固定是时间执行一次回调
TakeUntil:在满足条件之前,连续获取数据(事件)
SkipUntil:在满足条件之前,忽略所有数据(事件)
Buffer(缓冲):每次缓存指定个数数据 满足则发送
Throttle(节流阀):


Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0))
                .Throttle(TimeSpan.FromSeconds(1))
                .Subscribe(_ => Debug.Log("一秒过后"));

Return: 直接返回指定数据,并发送完成事件  
Observable.Return("Heloo").Subscribe(Debug.Log);

Timer:延时Observable
                        Observable.Timer(TimeSpan.FromSeconds(1))
                                .Subscribe(_=> {

                                        Debug.Log("延时一秒");
                                });
Delay:在每件事情上延时  
  Observable.EveryUpdate()
                .Where(_ => Input.GetMouseButtonDown(0))
                .Delay(TimeSpan.FromSeconds(1.0f))
                .Subscribe(_ => { Debug.Log("mouse clicked"); })
                .AddTo(this);

Sample:定期发送离当前事件最近的数据(事件)
  Observable.Interval(TimeSpan.FromMilliseconds(50))
                .Sample(TimeSpan.FromSeconds(1))
                .Subscribe(_ => { Debug.Log(DateTime.Now.Second); })
                .AddTo(this);

Timestamp:为每个数据(事件) 包装一个时间戳
Observable.Interval(TimeSpan.FromSeconds(1.0f))
                .Timestamp()
                .Subscribe(timestamp => { Debug.Log(timestamp); })
                .AddTo(this);

ThrottleFirst:每隔一段时间内,发送第一个数据(事件)
        Observable.EveryUpdate()
                                .Where(_ => Input.GetMouseButtonDown(0))
                                .ThrottleFirst(TimeSpan.FromSeconds(5))
                                .Subscribe(_ => {

                                        Debug.Log("鼠标点击");
                                })
                                .AddTo(this);


TimeInterval:计算当前的事件与上一个事件的事件间隔,并发送

Observable.Interval(TimeSpan.FromMilliseconds(750))
                .TimeInterval()
                .Subscribe(timeInterval => Debug.LogFormat("{0}: {1}", timeInterval.Value, timeInterval.Interval));


Defer:直到有观察者订阅时才创建 Observable,并且为每个观察者创建一个新的 Observable

var ran =new System.Random();

                        Observable.Defer(() => Observable.Start(() => ran.Next()))
                                .Delay(TimeSpan.FromSeconds(1f))
                                .Repeat()
                                .Subscribe(_ =>
                                {

                                        Debug.Log(_);
                                })
       


                        .AddTo(this);

这里列举了一些常用的关键字,其实还有很多,如果想仔细了解RX具体使用,可以查阅官方文档。这里就介绍这么多了。
响应式属性:ReactiveProperty
接下来我通过Rx实现一个MVP架构,在项目中如果具体的去使用一些Rx特有的一些东西,也是接下来重点介绍的响应式属性。什么是响应式属性,顾名思义,就是通过改变其值的方法,该属性就可以通知订阅当前属性的一些事件。还有其强大之处,就是可以代替一切变量,并且可以让这个变量有更多的功能。一般情况下我们写一些可以回调的变量都是需要写一些委托,是需要一定的维护成本的。例如:
namespace SYFramework
{
        public  class NomalProprety :MonoBehaviour
        {
                private static string values;

                public static string Values
                {
                        get => values;
                        set
                        {
                                if (!values.Equals(value))
                                {
                                        values = value;
                                        callback?.Invoke(value);
                                }
                        }
                }

                public static Action<string> callback;



                private void Awake()
                {
                        callback = (va) => {

                                Debug.Log("va:" + va);
                        };



                }

                private void Update()
                {
                        if (Input.GetKeyDown(KeyCode.Space))
                        {
                                Values = "鼠标点击了";
                        }
                }
        }



}
所以如果引入RX可以让你的代码更加简洁明了,RX一行代码就可以解决,在下面的MVP中有具体使用。
下面是mvp实现代码:
Model类:
using UniRx;
namespace SYFramework
{

        /// <summary>
        /// 定义一个Model 接口
        /// </summary>
        public interface IModel
        {
//定义整型变量

                IntReactiveProperty Score { get; }
//定义字符串变量
                StringReactiveProperty Name { get; }
        }

        /// <summary>
        /// 实现Imodel 接口
        /// </summary>
        public  class GameData : IModel
        {
                public IntReactiveProperty Score { get; } = new IntReactiveProperty() { Value = 0 };

                public StringReactiveProperty Name { get; } = new StringReactiveProperty() { Value = "" };
        }

}
publicIntReactiveProperty Score {get;}=newIntReactiveProperty(){ Value =0}; 这种写法是4.0 .Net的一个特性,不了解的可以去查一下。具体使用就是像申明一个变量一样,但是这个属性具有事件流的性质,可以被订阅。
view类:
using UnityEngine;
using UniRx;
using UnityEngine.UI;
namespace SYFramework
{
        public class UniRxPanel : MonoBehaviour
        {
                public Button send_Btn;

                public Text score_Txt;

                public Text gameName_txt;



                public IModel model;
                private void Awake()
                {
                        model = new GameData();

                        //订阅 RX写法 对ugui的扩展支持
                        model.Score.SubscribeToText(score_Txt);

                        model.Name.SubscribeToText(gameName_txt);

                        //发送消息
                        send_Btn.OnClickAsObservable().Subscribe(_ => {

                                model.Name.Value = "游戏数据";

                                model.Score.Value = Random.Range(1,100);
                        });



                }
        }

}


view类中使用了RX对UGUI的封装,例如:send_Btn.OnClickAsObservable()就是对按钮监听,点击之后会以事件流的形式发送到Subscriber订阅中。其实Rx对UGUI的封装格式基本上都是以xxxAsObservable()形式,所以了解了这个格式基本上就知道常用UI控件的使用了。
UniRx 支持了可序列化的 ReactiveProperty 类型,比如 Int/LongReactivePropety、Float/DoubleReactiveProperty、StringReactiveProperty、BoolReactiveProperty,还有更多,请参见 UniRx 中的源码 InspectableReactiveProperty.cs。也支持泛型类型例如ReactiveProperty<int> ,ReactiveProperty<string> ,ReactiveProperty<bool>等等。
其实RX有很多强大的功能,我这里只是写了一小部分,很多大佬写的一些框架,例如uFrame 框架QFramework框架等等。这里是对自己学习的一个记录,如果你也在学习框架的编写,可以认真的学完RX,或许对你有用。
结尾送一整图

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-6-25 18:18 | 显示全部楼层
写文章不易,留个小心心吧[爱]
 楼主| 发表于 2022-6-25 18:21 | 显示全部楼层
[赞]
发表于 2022-6-25 18:29 | 显示全部楼层
[爱]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 07:27 , Processed in 0.092499 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表