Unity3D插件UniRx(基于Unity的响应式编程框架)插件教程

Posted 恬静的小魔龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D插件UniRx(基于Unity的响应式编程框架)插件教程相关的知识,希望对你有一定的参考价值。

推荐阅读

大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。

一、介绍UniRx插件

UniRx是一种基于Unity3D的响应式编程框架

UniRx就是Unity版本的Rx响应式扩展响应式就是观察者和定时器,扩展指的是LINQ的操作符。Rx响应式扩展的特点就是擅长处理时间上的异步的逻辑。用Rx响应式扩展的方式编程可以很好地组织大量异步与并行处理。

UniRx重写了.Net的响应式扩展,主要作用是解决时间上异步的逻辑,让异步逻辑变得更加简洁和优雅。

Unity3D通常是单线程,但是UniRx可以让多线程更容易。

UniRx可以简化 UGUI 的编程,所有的UI事件可以转化为UniRx 的事件流。

UniRx支持的平台有PC/Mac/android/ios/WebGL/WindowsStore等平台和库。

二、为什么使用UniRx插件

在项目中的一些逻辑操作需要做异步时间处理,比如说动画播放、网络请求、资源加载、场景过渡等等,这种情况通常要使用协程,也就是WWWCoroutine,但是使用协程来做异步通常不是一个很好的选择,因为:

  • 协程不能返回值,它的返回类型必须是IEnumerator
  • 协程不能处理异常,因为yield return 语句没有办法try-catch
  • 会导致使用大量的回调来处理逻辑
  • 使用协程会导致程序的耦合性高,造成协程中的逻辑过于复杂

UniRx就是为了解决这些问题来的,那么它有哪些优点呢:

  • UniRx的使用方式介于回调和事件之间,有事件的概念,也使用了回调,回调是在事件经过组织之后,只需要调用一次进行事件的处理。
  • UniRx促进了多线程的操作,提供了UGUI的UI编程,UI事件可以转化为UniRx的事件流。
  • Unity3D在2017版本后支持了C#中的astnc/awaitUniRx也为Unity提供了更轻量、强大的astnc/await集成。

三、UniRx插件下载

源码地址:
https://github.com/neuecc/UniRx

Unity Asset Store 地址(免费):
http://u3d.as/content/neuecc/uni-rx-reactive-extensions-for-unity/7tT

插件下载地址:
https://github.com/neuecc/UniRx/releases

UniRx中的astnc/await集成
https://github.com/Cysharp/UniTask

四、怎么使用UniRx插件

4-1、快速入门

将插件导入到项目中:

新建脚本UniRxTest.cs编辑代码,实现一个双击检测Demo:

using System;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    void Start()
    
        // Observable.EveryUpdate调用协程的yield return null。
        // 它位于Update之后,LateUpdate之前。
        // Where等待操作的事件(当前事件是左键单击)
        var doubleClick = Observable.EveryUpdate()
            .Where(value => Input.GetMouseButtonDown(0));

        // Buffer 添加一个事件
        // Throttle 响应的最大间隔
        // TimeSpan.FromMilliseconds(250) 设置为250毫秒
        // Where 等待操作的事件(当前事件是左键单击)
        // Subscribe 绑定委托
        doubleClick.Buffer(doubleClick.Throttle(TimeSpan.FromMilliseconds(250)))
            .Where(value => value.Count >= 2)
            .Subscribe(value => Debug.Log("双击! 点击次数:" + value.Count));
    

运行结果:

这个Demo使用了5行代码就演示了以下功能:

  • Update作为事件流
  • 组合事件流
  • 合并自身流
  • 方便处理基于时间的操作

4-2、定时功能(与协程对比)

在平时项目开发中,可能会遇到需要经过一段时间出发某些逻辑的操作,可以用协程这么写:

using System;
using System.Collections;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    void Start()
    
        // 每5秒调用一次函数
        StartCoroutine(Timer(5, DoSomething));
    

    // 定时器
    IEnumerator Timer(float seconds, Action callback)
    
        yield return new WaitForSeconds(seconds);
        callback();
    

    // 调用函数
    void DoSomething()
    
        Debug.Log("TODO");
    

那么用UniRx怎么写呢:

using System;
using System.Collections;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    void Start()
    
        // 每5秒调用一次函数
        Observable.Timer(TimeSpan.FromSeconds(5))
           .Subscribe(value => DoSomething(););
    

    // 调用函数
    void DoSomething()
    
        Debug.Log("TODO");
    

甚至可以简化成一行代码:

using System;
using System.Collections;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    void Start()
    
        // 每5秒调用一次函数
        Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(value =>  Debug.Log("TODO"); );
    

为了避免this销毁的时候,流程还没有销毁的情况,可以加一行代码:

using System;
using System.Collections;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    void Start()
    
        // 每5秒调用一次函数
        Observable.Timer(TimeSpan.FromSeconds(5))
           .Subscribe(value =>  DoSomething(); )
           .AddTo(this);
    

    // 调用函数
    void DoSomething()
    
        Debug.Log("TODO");
    

AddTo(this)之后,就会将延迟和this(MonoBehaviour)绑定在一起了,当this被销毁的时候,定义的流程也会被销毁。

4-3、GET和POST操作

一般写法:

using System;
using System.Collections;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;

public class UniRxTest : MonoBehaviour

    void Start()
    
        StartCoroutine(RequestData("www.baidu.com", new WWWForm(), ReturnValue));
    

    //回调函数
    private void ReturnValue(string value)
    
        Debug.Log(value);
    

    /// <summary>
    /// 数据请求与发送
    /// </summary>
    /// <param name="url">请求的url</param>
    /// <param name="form">表单</param>
    /// <param name="dele">返回数据</param>
    /// <returns></returns>
    private IEnumerator RequestData(string url, WWWForm form, Action<string> dele = null)
    
        UnityWebRequest req = UnityWebRequest.Post(url, form);
        yield return req.SendWebRequest();
        if (req.result == UnityWebRequest.Result.ProtocolError)
        
            dele?.Invoke(req.error);
        

        if (req.isDone)
        
            dele?.Invoke(req.downloadHandler.text);
        
    

用UniRx写法:

using System;
using System.Collections;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;

public class UniRxTest : MonoBehaviour

    void Start()
    
        var request = ObservableWWW.Post("www.baidu.com", new WWWForm())
            .Subscribe(value => Debug.Log(value))
            .AddTo(this);
    

注意:不是讨论那个写法好,那么写法不好,只是使用UniRx更加简洁,更推荐是用UnityWebRequest,因为UnityWebRequest功能更完善,更加有效。

4-4、加载场景-AsyncOperation

在异步加载资源或者异步加载场景的时候往往会用到 AsyncOperation。

UniRx 对 AsyncOperation 做了支持。使得加载进度可以很容易地监听。

示例代码如下:

using UniRx;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace UniRxLesson

    public class AsyncOperationExample : MonoBehaviour
    
        void Start()
        
            var progressObservable = new ScheduledNotifier();

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

                            Resources.LoadAsync("TestCanvas").AsAsyncOperationObservable()
                                     .Subscribe(resourceRequest =>
                                     
                                         Instantiate(resourceRequest.asset);
                                     );
                        );

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

4-5、UGUI支持

示例代码:

using System;
using System.Collections;
using UniRx;
using UniRx.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class UniRxTest : MonoBehaviour

    public Button mButton;
    public Toggle mToggle;
    public InputField mInput;
    public Text mText;
    public Slider mSlider;

    void Start()
    
        // Button 按钮绑定事件
        mButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));

        // Toggle 控制其他UI对象的激活
        mToggle.OnValueChangedAsObservable().SubscribeToInteractable(mButton);

        // mInput Where筛选值不等于空的情况 绑定Text组件
        mInput.OnValueChangedAsObservable()
        .Where(x => x != null)
        .SubscribeToText(mText);

        // mSlider 绑定Text组件
        mSlider.OnValueChangedAsObservable()
            .SubscribeToText(mText, x => Math.Round(x, 2).ToString());
    

可以看出来,UniRx去绑定UI还是很好用的,但不仅于此。

使用 UniRx可以很容易地实现 MVP(MVRP)设计模式。

为什么应该用 MVP模式而不是 MVVM模式?Unity 没有提供 UI 绑定机制,创建一个绑定层过于复杂并且会对性能造成影响(使用反射)。尽管如此,视图还是需要更新。 Presenters层知道 View 组件并且能更新它们。

虽然没有真的绑定,但 Observables 可以通知订阅者,功能上也差不多。这种模式叫做 Reactive Presenter 设计模式,示例代码如下:

using System;
using System.Collections;
using UniRx;
using UniRx.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class UniRxTest : MonoBehaviour

    public Button mButton;
    public Toggle mToggle;
    public Text MyText;

    // 状态更改Model
    Enemy enemy = new Enemy(1000);

    void Start()
    
        // 以响应式的方式从视图和模型中提供用户事件
        mButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 100);
        mToggle.OnValueChangedAsObservable().SubscribeToInteractable(mButton);

        // Model通过Rx通知更新视图
        enemy.CurrentHp.SubscribeToText(MyText);
        enemy.IsDead.Where(isDead => isDead)
            .Subscribe(_ =>
            
                mToggle.interactable = mButton.interactable = false;
            );
    

// 所有属性的值更改时都会通知
public class Enemy

    public ReactiveProperty<long> CurrentHp  get; private set; 
    public ReadOnlyReactiveProperty<bool> IsDead  get; private set; 

    public Enemy(int initialHp)
    
        // 声明属性
        CurrentHp = new ReactiveProperty<long>(initialHp);
        IsDead = CurrentHp.Select(x => x <= 10).ToReadOnlyReactiveProperty();
    

4-6、响应式属性ReactiveProperty

UniRx还有一个很强的属性ReactiveProperty,也就是响应式属性,之所以强大,是因为它让变量的变化过程中可以增加更多的功能更加的灵活。

比如,要监听一个变量值是否发生变化,可以这么写:

using System;
using System.Collections;
using UniRx;
using UniRx.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class UniRxTest : MonoBehaviour

    public int mAge;
    public int Age
    
        get
        
            return mAge;
        
        set
        
            if (mAge != value)
            
                mAge = value;
                OnAgeChanged();
            
        
    

    public void OnAgeChanged()
    
        Debug.Log("Value变化了");
    

上述代码虽然也可以完成监听变量值变化的功能,但是如果要在外部访问,还需要写一个委托来监听,比较麻烦,如果用UniRx就会简单许多:

using System;
using System.Collections;
using UniRx;
using UniRx.Diagnostics;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class UniRxTest : MonoBehaviour

    public ReactiveProperty<int> Age = new ReactiveProperty<int>();
    void Start()
    
        // 绑定值
        Age.Subscribe(value =>
        
            Debug.Log("通知值变化");
        );

        // 改变值可以用 变量.value来获取或者更改
        Age.Value = 5;
    

4-7、Animation播放某一帧的动画

代码主要用到了UniRx.Async,后面UniRx.Async被分割成Cysharp/UniTask,需要再导入Cysharp/UniTask包,步骤如下:

(1)Window→Package Manager打开包管理器:

(2)选择Add package from git URL…

(3)添加 https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask 至包管理器

(4)导入完成

(5)修改Api Compatibility Level:

示例代码:

using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;

public class UniRxTest : MonoBehaviour

    private bool stopLoop = false;//动画控制
    void Start()
    
    

    /// <summary>
    /// Animation播放指定帧的动画
    /// </summary>
    /// <param name="myAnim">动画组件</param>
    /// <param name="startTimeInt">开始时间</param>
    /// <param name="endTimeInt">结束时间</param>
    private async void PlayAnimation(Animation myAnim, int startTimeInt, int endTimeInt)
    
        int speed = GetSpeed(startTimeInt, endTimeInt);
        float frame = GetFrame(myAnim);
        float startTime;
        float endTime;
        if (speed == 1)
        
            startTime = frame * startTimeInt;
            endTime = frame * endTimeInt;
        
        else
        
            startTime = frame * endTimeInt;
            endTime = frame * startTimeInt;
        
        stopLoop = false;

        while (!stopLoop)
        
            myAnim[myAnim.clip.name].time = startTime;//跳过开始帧
            myAnim[myAnim.clip.name].speed = speed;//正播还是倒播
            myAnim.Play(myAnim.clip.name);//Play()
            await UniTask.DelayFrame(UniRx - Unity响应式编程插件

UniRx - 你会爱上的 Unity响应式编程

Unity开源项目精选UniRx:Unity中的响应式编程

C#游戏框架uFrame

Unity基于响应式编程(Reactive programming)入门

Unity3D日常开发Unity3D中实现不规则Button按钮的精准响应