推荐阅读
大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新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插件
在项目中的一些逻辑操作需要做异步时间处理,比如说动画播放、网络请求、资源加载、场景过渡等等
,这种情况通常要使用协程,也就是WWW
和Coroutine
,但是使用协程来做异步通常不是一个很好的选择,因为:
- 协程不能返回值,它的返回类型必须是IEnumerator
- 协程不能处理异常,因为yield return 语句没有办法try-catch
- 会导致使用大量的回调来处理逻辑
- 使用协程会导致程序的耦合性高,造成协程中的逻辑过于复杂
UniRx
就是为了解决这些问题来的,那么它有哪些优点呢:
-
UniRx
的使用方式介于回调和事件之间,有事件的概念,也使用了回调,回调是在事件经过组织之后,只需要调用一次进行事件的处理。 -
UniRx
促进了多线程的操作,提供了UGUI的UI编程,UI事件可以转化为UniRx
的事件流。 - Unity3D在2017版本后支持了C#中的
astnc/await
,UniRx
也为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(1);//帧延缓,等Play()启动
await UniTask.WaitUntil(() => myAnim[myAnim.clip.name].time > endTime);//播放到指定的进度点则停止
myAnim.Stop();//停止播放
stopLoop = true;//停止播放
}
}
// 判断是正播还是倒播
int GetSpeed(int startTime, int endTime)
{
if (endTime - startTime > 0)
{
return 1;
}
else if (endTime - startTime < 0)
{
return -1;
}
else
{
return 1;
}
}
// 得到动画的播放帧率
float GetFrame(Animation myAnim)
{
return myAnim[myAnim.clip.name].length / 100;
}
}
五、后记
讲解了UniRx插件以及UniTask插件的使用方法,难度有点高,适合慢慢学习(收藏吃灰)。
在学习过程中有什么不懂的都可以在博客主页找到我的联系方式。
你的点赞就是对博主的支持,有问题记得留言:
博主主页有联系方式。
博主还有跟多宝藏文章等待你的发掘哦:
专栏 | 方向 | 简介 |
---|---|---|
Unity3D开发小游戏 | 小游戏开发教程 | 分享一些使用Unity3D引擎开发的小游戏,分享一些制作小游戏的教程。 |
Unity3D从入门到进阶 | 入门 | 从自学Unity中获取灵感,总结从零开始学习Unity的路线,有C#和Unity的知识。 |
Unity3D之UGUI | UGUI | Unity的UI系统UGUI全解析,从UGUI的基础控件开始讲起,然后将UGUI的原理,UGUI的使用全面教学。 |
Unity3D之读取数据 | 文件读取 | 使用Unity3D读取txt文档、json文档、xml文档、csv文档、Excel文档。 |
Unity3D之数据集合 | 数据集合 | 数组集合:数组、List、字典、堆栈、链表等数据集合知识分享。 |
Unity3D之VR/AR(虚拟仿真)开发 | 虚拟仿真 | 总结博主工作常见的虚拟仿真需求进行案例讲解。 |
Unity3D之插件 | 插件 | 主要分享在Unity开发中用到的一些插件使用方法,插件介绍等 |
Unity3D之日常开发 | 日常记录 | 主要是博主日常开发中用到的,用到的方法技巧,开发思路,代码分享等 |
Unity3D之日常BUG | 日常记录 | 记录在使用Unity3D编辑器开发项目过程中,遇到的BUG和坑,让后来人可以有些参考。 |