【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件
注意
:考虑到后续接触的插件会越来越多,我将插件相关的内容单独分开,并全部整合放在【推荐100个unity插件】专栏里,感兴趣的小伙伴可以前往逐一查看学习。
文章目录
- 前言
-
- 1、UniTask 是什么?
- 2、为什么需要 UniTask?
- 一、基础入门
-
- 1、UniTask官方地址
- 2、安装 UniTask
- 3、第一个UniTask示例
- 4、UniTask vs UniTaskVoid
- 6、调用
- 二、核心功能详解
-
- 1、延时操作
- 2、线程切换
- 3. 等待条件满足
-
- 3.1 UniTask.WaitUntil:等待条件成立
- 3.2 UniTask.WhenAll:同时等待多个条件满足
- 3.3 UniTask.WhenAny:等待任意一个条件满足
- 3.4 UniTask.WaitUntilValueChanged:等待值发生变化
- 4、资源加载
-
- 4.1 普通资源加载
-
- 方式一 加载本地资源
- 方式二 加载远程资源
- 4.2 带进度回调资源加载
-
- 方法一 加载资源
- 方法二 分步加载场景
- 三、高级特性
-
- 1、取消操作
-
- 1.1 使用 CancellationToken 取消等待操作
- 1.2 CreateLinkedTokenSource 多条件取消实现
- 1.3 GetCancellationTokenOnDestroy绑定GameObject取消
- 1.4 CancelAfterSlim延迟取消
- 2、超时处理
- 3、UniTask监听UGUI
-
- 示例一:按钮不同监听方式
- 示例二:判断单击与双击
- 示例三:异步 LINQ 监听三连点击
- 示例四:事件排队处理
- 示例五:监听输入框结束编辑事件
- 示例六:监听 Toggle 值变化
- 示例七:监听 Slider 值变化
- 4、UniTask与协程转换
-
- 4.1 直接 Await 协程
- 4.2 将 UniTask 转换为协程
- 4.3 将协程转换为 UniTask
- 5、令牌复用机制
- 6、异常处理策略
-
- 6.1 uppressCancellationThrow异常处理优化
- 6.2 全局异常监听
- 6.3 异常过滤处理
- 7、UniTaskCompleitonSource 的使用:设置结果与取消任务
-
- 什么是 UniTaskCompletionSource?
- 示例:设置结果和取消任务
- 在上述代码中:
- 注意事项
- 五、常见问题解答
- 六、总结
- 专栏推荐
- 完结
前言
1、UniTask 是什么?
UniTask 是专为 Unity 设计的高性能异步编程库,它提供了比 C# 原生 Task 更轻量、更高效的异步解决方案。简单来说,它让 Unity 中的异步编程(比如加载资源、等待时间、网络请求等)变得更简单、更快速,而且不产生GC内存垃圾
。
如果你还不理解什么是GC,可以参考:【从零开始入门unity游戏开发之——C#篇04】栈(Stack)和堆(Heap),值类型和引用类型,以及特殊的引用类型string,垃圾回收(GC)
2、为什么需要 UniTask?
想象你在做游戏时需要:
- 等待 3 秒后显示一个提示
- 加载一个大资源时不卡住游戏
- 等待玩家点击按钮后再继续
传统做法是用协程(Coroutine)或C#的Task。
协程知识可以参考:【零基础入门unity游戏开发——unity通用篇27】协同程序协程(Coroutine)的使用介绍
Task多线程相关知识可以参考:【从零开始入门unity游戏开发之——C#篇37】进程、线程、C# 中实现多线程有多种方案和async/await异步编程
但它们都有缺点:
UniTask专为Unity设计,解决了上述所有痛点,提供了:
- 零GC的高性能异步操作
- 完美的Unity主线程集成
- 直观的async/await语法
- 丰富的Unity特定功能
一、基础入门
1、UniTask官方地址
- github地址:https://github.com/Cysharp/UniTask
- 码云地址:https://gitee.com/unity_data/UniTask
2、安装 UniTask
- 从 GitHub 下载最新版本:https://github.com/Cysharp/UniTask/releases
- 下载
.unitypackage
文件并导入到你的项目中 - 在代码中添加命名空间:
using Cysharp.Threading.Tasks;
注意
:UniTask 功能依赖于 C# 7.0,所以需要的 Unity 最低版本是Unity 2018.3 ,官方支持的最低版本是Unity2018.4.13f1.
3、第一个UniTask示例
async UniTaskVoid StartCountdown(){ for (int i = 3; i > 0; i--) { Debug.Log($\"倒计时: {i}\"); await UniTask.Delay(1000); // 等待1秒 } Debug.Log(\"延迟调用结束\");}// 调用void Start(){ StartCountdown().Forget(); // Forget表示不等待结果}
4、UniTask vs UniTaskVoid
- UniTask:需要await等待的任务,可以返回值
async UniTask<int> LoadDataAsync(){ await UniTask.Delay(500); return 42; // 返回结果}
- UniTaskVoid:\"即发即忘\"的任务,不返回结果
async UniTaskVoid ShowEffect(){ await UniTask.Delay(300); PlayParticleEffect();}
📌 最佳实践:async UniTaskVoid是async UniTask的轻量级版本。优先使用UniTask,只有确定不需要等待的任务才用UniTaskVoid
6、调用
async void是一个原生的 C# 任务系统,因此它不在 UniTask 系统上运行。避免 async void,尽量使用 async UniTask
或 async UniTaskVoid
。如果您不需要等待(即发即弃),那么使用UniTaskVoid会更好。不幸的是,要解除警告,您需要在尾部添加Forget()
或者使用_ =
。
❌ 错误做法:
async void Start() // 避免使用async void{ await LoadData();}
✅ 正确做法:
async UniTaskVoid Start() // 使用UniTaskVoid{ await StartCountdown();}// 或者void Start(){ LoadData().Forget(); // 明确忽略结果 //或者 _ = StartCountdown();}
二、核心功能详解
1、延时操作
UniTask 提供了多种延时方式:
// 等待1秒(受Time.timeScale影响)await UniTask.Delay(1000);// 使用 TimeSpan 等待1秒(受Time.timeScale影响)await UniTask.Delay(TimeSpan.FromSeconds(1));// 等待1秒(不受Time.timeScale影响)await UniTask.Delay(1000, ignoreTimeScale: true);// 等待当前帧结束。类似于协程中的 WaitForEndOfFrame。await UniTask.WaitForEndOfFrame();//需要注意的是,在 `Unity 2023.1 之前`的版本中,WaitForEndOfFrame 需要传入一个 `MonoBehaviour` 实例作为参数。// await UniTask.WaitForEndOfFrame(this); // 等待下一帧await UniTask.NextFrame();// 等待固定60帧await UniTask.DelayFrame(60);// 默认在 Update 之后执行。默认情况下 `UniTask.Yield()` 等同于 `UniTask.Yield(PlayerLoopTiming.Update)`// 协程 yield return null 的替代方案await UniTask.Yield();// 让出执行权,下一帧继续// 等待物理更新后执行,等同于await UniTask.WaitForFixedUpdate();协程yield return new WaitForFixedUpdate 的替代方案await UniTask.Yield(PlayerLoopTiming.FixedUpdate);// 在 LateUpdate 阶段后继续await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
常见 PlayerLoopTiming 选项
可以通过指定不同的 PlayerLoopTiming
,可以控制代码在 Unity 生命周期的不同阶段执行。
EarlyUpdate
: Unity 早期更新阶段FixedUpdate
: 物理更新阶段PreUpdate
: 更新前阶段Update
: 常规 Update 阶段PreLateUpdate
: LateUpdate 前阶段PostLateUpdate
: 一帧完全结束后(最常用)
2、线程切换
// 切换到线程池线程执行耗时操作await UniTask.SwitchToThreadPool();// TODO:执行耗时计算// 返回主线程// await UniTask.Yield();await UniTask.SwitchToMainThread();
3. 等待条件满足
3.1 UniTask.WaitUntil:等待条件成立
async UniTaskVoid WaitUntilCondition(){ await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Debug.Log(\"玩家按下了空格键!\");}
3.2 UniTask.WhenAll:同时等待多个条件满足
// 同时执行三个任务,全部完成后继续await UniTask.WhenAll( Task1(), Task2(), Task3());
实战:同时等待多个小球达到目标位置
using Cysharp.Threading.Tasks;using UnityEngine;using System.Threading;public class WhenAllExample : MonoBehaviour{ public GameObject ball1; public GameObject ball2; private CancellationTokenSource _cancellationTokenSource; void Start() { _cancellationTokenSource = new CancellationTokenSource(); WaitForAllBallsToReachPosition(_cancellationTokenSource.Token).Forget(); } async UniTaskVoid WaitForAllBallsToReachPosition(CancellationToken token) { // 分别创建两个等待任务,监控 ball1 和 ball2 的 x 坐标 var task1 = UniTask.WaitUntil(() => ball1.transform.position.x > 1, cancellationToken: token); var task2 = UniTask.WaitUntil(() => ball2.transform.position.x > 1, cancellationToken: token); // 使用 WhenAll 同时等待两个任务完成 await UniTask.WhenAll(task1, task2); // 所有等待条件满足后,修改小球颜色 ball1.GetComponent<Renderer>().material.color = Color.blue; ball2.GetComponent<Renderer>().material.color = Color.red; } private void OnDestroy() { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); }}
这种方式适用于需要等待多个并行条件同时成立的场景,如多个动画结束、多个任务完成等。
3.3 UniTask.WhenAny:等待任意一个条件满足
有时我们希望用户只点击任意一个按钮就能触发下一步操作。在示例中,我们对两个按钮的点击事件分别设置等待条件,并通过 UniTask.WhenAny 来等待任一条件满足,满足后输出提示信息。
// 任意一个任务完成就继续await UniTask.WhenAny( Task1(), Task2(), Task3());
实战:等待任意一个按钮点击
using Cysharp.Threading.Tasks;using UnityEngine;using System.Threading;public class WhenAnyExample : MonoBehaviour{ public bool _isClick1; public bool _isClick2; private CancellationTokenSource _cancellationTokenSource; void Start() { _cancellationTokenSource = new CancellationTokenSource(); WaitForAnyButtonClick(_cancellationTokenSource.Token).Forget(); } async UniTaskVoid WaitForAnyButtonClick(CancellationToken token) { // 分别创建等待任务,监控 _isClick1 和 _isClick2 状态 var task1 = UniTask.WaitUntil(() => _isClick1, cancellationToken: token); var task2 = UniTask.WaitUntil(() => _isClick2, cancellationToken: token); // 使用 WhenAny 等待任意一个任务完成 await UniTask.WhenAny(task1, task2); // 当任意一个按钮被点击后,输出提示信息 Debug.Log(\"一个按钮被点击了\"); } private void OnDestroy() { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); }}
这种方式可以用在用户输入、系统状态变化等需要响应第一个满足条件的场景。
3.4 UniTask.WaitUntilValueChanged:等待值发生变化
UniTask.WaitUntilValueChanged 用于等待某个值发生变化,然后继续执行后续代码。
// 等待值变化await UniTask.WaitUntilValueChanged(transform, t => t.position);
实战:
using Cysharp.Threading.Tasks;using UnityEngine;using System.Threading;public class WaitUntilValueChangedExample : MonoBehaviour{ public GameObject ball; private CancellationTokenSource _cancellationTokenSource; void Start() { // 创建一个新的 CancellationTokenSource 实例,用于取消异步操作 _cancellationTokenSource = new CancellationTokenSource(); // 调用 WaitUntilPositionChanges 方法,并传递 CancellationToken // 使用 Forget 方法忽略返回的 Task,这样可以避免异步方法的结果未被处理导致的编译警告 WaitUntilPositionChanges(_cancellationTokenSource.Token).Forget(); } async UniTaskVoid WaitUntilPositionChanges(CancellationToken token) { // 第一个参数是要监视的对象,这里是球的transform // 第二个参数是一个Func委托,用于获取要监视的值,这里是transform的position属性 // cancellationToken参数用于传递一个取消令牌,可以在需要时取消等待操作 await UniTask.WaitUntilValueChanged(ball.transform, x => x.position, cancellationToken: token); Debug.Log(\"小球位置已变化\"); } private void OnDestroy() { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); }}
- 在上述示例中,WaitUntilPositionChanges 方法会等待直到 ball 的位置发生变化,然后输出日志信息。
- UniTask.WaitUntilValueChanged 方法接受一个对象和一个用于监视该对象某个属性或字段的委托,当该属性或字段的值发生变化时,等待结束,继续执行后续代码。
4、资源加载
4.1 普通资源加载
方式一 加载本地资源
async UniTask<Texture2D> LoadTexture(string path){ // 异步加载资源 var resource = await Resources.LoadAsync<Texture2D>(path); return (Texture2D)resource.asset;}
方式二 加载远程资源
async UniTask<string> FetchWebData(string url){ using var request = UnityWebRequest.Get(url); await request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { return request.downloadHandler.text; } else { throw new Exception($\"请求失败: {request.error}\"); }}
4.2 带进度回调资源加载
方法一 加载资源
using Cysharp.Threading.Tasks;using UnityEngine;public class ResUniTask : MonoBehaviour{ void Start() { LoadAsyncUniTask(); } async void LoadAsyncUniTask() { ResourceRequest res = Resources.LoadAsync<GameObject>(\"Prefabs/Cube\"); // 通过ToUniTask获取进度 await res.ToUniTask(Progress.Create<float>(p => { Debug.Log($\"加载进度: {p:P0}\"); })); // 直接获取资源 //var asset = await res; if (res.asset != null) { //实例化资源 Instantiate(res.asset); } else { Debug.LogError(\"[UniTask加载] 资源加载失败!\"); } }}
方法二 分步加载场景
async UniTaskVoid LoadSceneAsync(string sceneName){ // 显示加载界面 ShowLoadingScreen(); // 异步加载场景 var operation = SceneManager.LoadSceneAsync(sceneName); // 更新进度条 while (!operation.isDone) { UpdateProgressBar(operation.progress); await UniTask.Yield(); } // 隐藏加载界面 HideLoadingScreen();}
三、高级特性
1、取消操作
1.1 使用 CancellationToken 取消等待操作
在异步操作中,使用 CancellationToken 可以在需要时取消等待操作,防止出现无限等待的情况。
using Cysharp.Threading.Tasks;using UnityEngine;using System.Threading;using System;public class CancellationExample : MonoBehaviour{ private CancellationTokenSource _cancellationTokenSource; void Start() { // 创建一个新的 CancellationTokenSource 实例,用于生成取消令牌 _cancellationTokenSource = new CancellationTokenSource(); // 调用 PerformCancelableTask 方法,并传递生成的取消令牌 PerformCancelableTask(_cancellationTokenSource.Token).Forget(); Debug.Log(\"Start完成\"); } // 定义一个异步方法 PerformCancelableTask,用于执行一个可取消的任务 async UniTaskVoid PerformCancelableTask(CancellationToken token) { try { // 使用 UniTask.Delay 方法延迟 5000 毫秒(5 秒),同时传入 cancellationToken 参数 token 以支持任务取消 await UniTask.Delay(5000, cancellationToken: token); // 如果任务未被取消,延迟结束后输出 \"任务完成\" Debug.Log(\"任务完成\"); } catch (OperationCanceledException) { // 如果在等待过程中任务被取消,会抛出 OperationCanceledException 异常,捕获该异常并输出 \"任务被取消\" Debug.Log(\"任务被取消\"); } } void OnDestroy() { // 调用 _cancellationTokenSource 的 Cancel 方法,取消任何正在进行的异步操作。 _cancellationTokenSource.Cancel(); // 调用 _cancellationTokenSource 的 Dispose 方法,释放该对象占用的资源。 _cancellationTokenSource.Dispose(); }}
在上述示例中,PerformCancelableTask 方法会等待 5 秒钟,然后输出“任务完成”。如果在这 5 秒内对象被销毁,OnDestroy 方法会取消该任务,并输出“任务被取消”。
1.2 CreateLinkedTokenSource 多条件取消实现
using System;using System.Threading;using Cysharp.Threading.Tasks;using UnityEngine;using UnityEngine.UI;public class UniTaskCreateLinkedTokenSource : MonoBehaviour{ public Button cancelButton; private CancellationTokenSource cancelToken; private CancellationTokenSource timeoutToken; void Start() { cancelToken = new CancellationTokenSource(); cancelButton.onClick.AddListener(() => { cancelToken.Cancel(); // 点击按钮后取消。 }); timeoutToken = new CancellationTokenSource(); timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 设置5s超时。 BeginTestCancelAfter().Forget(); } async UniTaskVoid BeginTestCancelAfter() { try { // 链接 token var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token); int i = 0; while (true) { i++; Debug.Log($\"执行任务{i}次\"); // 等待 1 秒,并传入取消令牌,若取消则会抛出异常 await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: linkedTokenSource.Token); } } catch (OperationCanceledException ex) { if (timeoutToken.IsCancellationRequested) { Debug.Log(\"超时退出\"); } else if (cancelToken.IsCancellationRequested) { Debug.Log(\"点击取消退出\"); } } }}
1.3 GetCancellationTokenOnDestroy绑定GameObject取消
CancellationToken 不止可以由CancellationTokenSource,还可以使用 MonoBehaviour 的GetCancellationTokenOnDestroy扩展方法创建。
using System;using Cysharp.Threading.Tasks;using UnityEngine;public class UniTaskGetCancellationTokenOnDestroy : MonoBehaviour{ void Start() { BeginTestCancel().Forget(); } async UniTaskVoid BeginTestCancel() { try { int i = 0; while (true) { i++; Debug.Log($\"执行任务{i}次\"); // 等待 1 秒,当this GameObject Destroy的时候,就会执行Cancel await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: this.GetCancellationTokenOnDestroy()); } } catch (OperationCanceledException) { // 捕获到取消异常,打印信息 Debug.Log(\"捕获到取消异常OperationCanceledException\"); } }}
1.4 CancelAfterSlim延迟取消
超时是取消操作的变体。您可以通过 CancellationTokenSouce.CancelAfterSlim(TimeSpan) 设置超时,并将 CancellationToken 传递给异步方法。
using Cysharp.Threading.Tasks;using System;using System.Threading;using UnityEngine;public class UniTaskCancelAfter : MonoBehaviour{ private CancellationTokenSource _cancellationTokenSource; void Start() { // 创建一个新的取消令牌源实例 _cancellationTokenSource = new CancellationTokenSource(); // 启动一个异步任务 UniTaskVoid taskVoid = BeginTestCancelAfter(_cancellationTokenSource.Token); // 设置在指定时间(这里是 3 秒)后触发取消请求,实现超时处理 _cancellationTokenSource.CancelAfterSlim(TimeSpan.FromSeconds(3)); Debug.Log(\"Start完成\"); } async UniTaskVoid BeginTestCancelAfter(CancellationToken token) { try { int i = 0; while (true) { i++; Debug.Log($\"执行任务{i}次\"); // 等待 1 秒,并传入取消令牌,若取消则会抛出异常 await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token); } } catch (OperationCanceledException) { // 捕获到取消异常,打印信息 Debug.Log(\"捕获到取消异常OperationCanceledException\"); } }}
结果
2、超时处理
SomeAsyncOperation 超时处理
try{ // 3秒超时 await SomeAsyncOperation().Timeout(TimeSpan.FromSeconds(3));}catch (TimeoutException){ Debug.Log(\"操作超时\");}
3、UniTask监听UGUI
UniTask 在 UI 事件处理中的核心优势:
-
无阻塞异步操作
利用 async/await 机制,确保 UI 事件处理在后台进行,不会阻塞主线程,从而保障流畅的用户体验。 -
轻量高效的实现
采用结构体和对象池的设计,大幅降低垃圾回收压力,使得高频事件处理更加稳定。 -
灵活的事件调度
结合 CancellationToken 和异步 LINQ,不仅能够精细控制事件处理流程,还能轻松应对复杂的交互逻辑。
示例一:按钮不同监听方式
public Button myButton;async UniTaskVoid HandleButtonClick(){ // 等待按钮被点击 await myButton.OnClickAsync(); Debug.Log(\"按钮被点击了!\");}
示例二:判断单击与双击
在 OnClickBtn2UniTask 方法中,首先等待第一次点击,然后在 1 秒内判断是否有第二次点击,从而区分单击和双击操作。
private async UniTaskVoid OnClickBtn2UniTask(CancellationToken cancellationToken){ while (true) { // 等待第一次点击 var firstClickUniTask = btn2.OnClickAsync(cancellationToken); await firstClickUniTask; Debug.Log(\"OnClickBtn2UniTask 第1次点击\"); // 等待第二次点击或 1 秒超时 int index = await UniTask.WhenAny( btn2.OnClickAsync(cancellationToken), UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: cancellationToken) ); if (index == 0) { // 第二次点击在 1 秒内发生 Debug.Log(\"OnClickBtn2UniTask 时间间隔不超过1\"); } else { // 1 秒超时,视为单击 Debug.Log(\"OnClickBtn2UniTask 时间间隔超过1\"); } }}
示例三:异步 LINQ 监听三连点击
使用异步 LINQ 操作,在 OnClickBtn3TripleClick 方法中等待按钮被点击三次后再执行后续代码,适用于需要捕捉用户快速连续操作的场景。
/// /// 该方法使用异步 LINQ 监听 btn3 三次点击。/// 注意:该方法只会监听一次三连点击,不会持续监听。/// 如果需要持续监听,需要自己加while/// /// private async UniTaskVoid OnClickBtn3TripleClick(CancellationToken cancellationToken){ // 使用异步 LINQ 等待 btn3 被点击三次 // await btn3.OnClickAsAsyncEnumerable().Take(3).LastAsync(cancellationToken); await btn3.OnClickAsAsyncEnumerable().Take(3) .ForEachAsync(_ => { Debug.Log(\"OnClickBtn3TripleClick: 每次点击触发\"); }, cancellationToken); // // index从0开始 第一次点击index会是0 所以取到的是0 1 2 index为2时 也就是第三次点击 所以会输出三次点击完成 // var asyncEnumerable = btn3.OnClickAsAsyncEnumerable(); // await asyncEnumerable.Take(3).ForEachAsync // ((_, index) => // { // if (cancellationToken.IsCancellationRequested) return; // // if (index == 0) // { // Debug.Log(0); // } // else if (index == 1) // { // Debug.Log(1); // } // else // { // Debug.Log(2); // } // }, cancellationToken); Debug.Log(\"OnClickBtn3TripleClick: 三次点击完成\");}
示例四:事件排队处理
在 OnClickBtn4QueueEvents 方法中,通过事件排队的方式处理按钮点击,每次点击事件处理之间等待固定时间,确保事件顺序执行而不会互相干扰。
private async UniTaskVoid OnClickBtn4QueueEvents(CancellationToken cancellationToken){ int count = 0; await btn4.OnClickAsAsyncEnumerable().Queue().ForEachAwaitAsync(async _ => { Debug.Log($\"OnClickBtn4QueueEvents: 开始处理点击事件{count}\"); await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: cancellationToken); Debug.Log($\"OnClickBtn4QueueEvents: 点击事件{count}处理完成\"); count++; }, cancellationToken);}
示例五:监听输入框结束编辑事件
使用 OnInputFieldEndEdit 方法,通过异步流实时监听输入框结束编辑事件,并输出用户输入的内容。此方式适合需要捕捉用户输入并即时反馈的场景。
private async UniTaskVoid OnInputFieldEndEdit(CancellationToken cancellationToken){ // 监听输入框结束编辑事件 await foreach (var text in inputField.OnEndEditAsAsyncEnumerable().WithCancellation(cancellationToken)) { Debug.Log($\"OnInputFieldEndEdit: 输入框结束编辑,输入内容为: {text}\"); }}
示例六:监听 Toggle 值变化
在 OnToggleValueChangedAsync 方法中,通过异步流监听 Toggle 的值变化事件,每次变化时输出当前值,使状态变更能够及时反馈到日志中。
private async UniTaskVoid OnToggleValueChangedAsync(CancellationToken cancellationToken){ await toggle.OnValueChangedAsAsyncEnumerable(cancellationToken) .ForEachAsync(value => { Debug.Log(\"Toggle 值变化:\" + value); });}
示例七:监听 Slider 值变化
类似于 Toggle,OnSliderValueChangedAsync 方法通过异步流监听 Slider 数值变化,每次变化都输出当前值,方便对滑动条数值的动态监控。
private async UniTaskVoid OnSliderValueChangedAsync(CancellationToken cancellationToken){ await slider.OnValueChangedAsAsyncEnumerable(cancellationToken) .ForEachAsync(value => { Debug.Log(\"Slider 当前值:\" + value); });}
4、UniTask与协程转换
4.1 直接 Await 协程
在示例中,我们可以直接 await 一个协程方法来等待其完成。通过这种方式,不仅能够利用 UniTask 的语法糖简化代码,还能在异步方法中直接处理协程的结果。
using System.Collections;using Cysharp.Threading.Tasks;using UnityEngine;public class AwaitCoroutine : MonoBehaviour{ async void Start() { // 直接 await 协程 await CoroutineTest(); } // 直接 await 协程并等待其完成 IEnumerator CoroutineTest() { Debug.Log($\"协程开始,当前时间: {Time.time}\"); // 等待 1 秒 yield return new WaitForSeconds(1); Debug.Log($\"协程结束,当前时间: {Time.time}\"); }}
通过 await 协程,我们可以方便地将传统协程嵌入到异步方法中,无需担心协程的调度问题。
结果
4.2 将 UniTask 转换为协程
有时我们可能需要在传统协程中调用 UniTask 提供的功能。UniTask 提供了 ToCoroutine 扩展方法,可以将一个 UniTask 转换为协程,从而方便地在 StartCoroutine 中使用。
using System;using System.Collections;using Cysharp.Threading.Tasks;using UnityEngine;public class UniTaskToCoroutine : MonoBehaviour{ void Start() { StartCoroutine(UniTaskToCoroutineTest()); } // 将 UniTask 转换为协程并等待 IEnumerator UniTaskToCoroutineTest() { Debug.Log($\"UniTask 转换为协程开始,当前时间: {Time.time}\"); // 将 UniTask 延迟 1 秒转换为协程并等待 yield return UniTask.Delay(TimeSpan.FromSeconds(1)).ToCoroutine(); Debug.Log($\"UniTask 转换为协程结束,当前时间: {Time.time}\"); }}
这种转换方式允许开发者在协程环境中依然享受到 UniTask 高效的异步操作。
结果
4.3 将协程转换为 UniTask
同样地,如果希望在 UniTask 异步方法中使用已有的协程逻辑,可以将协程转换为 UniTask。通过 ToUniTask 扩展方法,我们可以方便地将协程转换为 UniTask,并通过 await 等待其完成。
using System.Collections;using Cysharp.Threading.Tasks;using UnityEngine;public class CoroutineToUniTask : MonoBehaviour{ async void Start() { await CoroutineToUniTaskTest(); } // 将协程转换为 UniTask 并等待 async UniTask CoroutineToUniTaskTest() { Debug.Log($\"协程转换为 UniTask 开始,当前时间: {Time.time}\"); // 创建一个协程实例 IEnumerator coroutine = CoroutineTest(); // 将协程转换为 UniTask 并等待 await coroutine.ToUniTask(this); Debug.Log($\"协程转换为 UniTask 结束,当前时间: {Time.time}\"); } IEnumerator CoroutineTest() { Debug.Log($\"协程开始,当前时间: {Time.time}\"); // 等待 1 秒 yield return new WaitForSeconds(1); Debug.Log($\"协程结束,当前时间: {Time.time}\"); }}
这种转换方式让我们可以在统一的异步方法中混合使用协程和 UniTask,充分利用各自的优势,从而使代码结构更清晰。
结果
5、令牌复用机制
使用 UniTask 的TimeoutController进行优化,减少每次调用异步方法时用于超时的 CancellationTokenSource 的堆内存分配
TimeoutController timeoutController = new TimeoutController(); // 提前创建好,以便复用。async UniTask FooAsync(){ try { // 您可以通过 timeoutController.Timeout(TimeSpan) 把超时设置传递到 cancellationToken。 await UnityWebRequest.Get(\"http://foo\").SendWebRequest() .WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5))); timeoutController.Reset(); // 当 await 完成后调用 Reset(停止超时计时器,并准备下一次复用)。 } catch (OperationCanceledException ex) { if (timeoutController.IsTimeout()) { UnityEngine.Debug.Log(\"timeout\"); } }}
使用new TimeoutController(CancellationToken),让超时结合其他取消源一起使用
TimeoutController timeoutController;CancellationTokenSource clickCancelSource;void Start(){ this.clickCancelSource = new CancellationTokenSource(); this.timeoutController = new TimeoutController(clickCancelSource);}
注意:UniTask 有.Timeout,.TimeoutWithoutException方法,但如果可以的话,尽量不要使用这些方法,请传递CancellationToken。因为.Timeout是在任务外部执行,所以无法停止超时任务。.Timeout意味着超时后忽略结果。如果您将一个CancellationToken传递给该方法,它将从任务内部执行,因此可以停止正在运行的任务。
6、异常处理策略
6.1 uppressCancellationThrow异常处理优化
将取消操作转换为布尔值判断,避免异常堆栈开销
// 传统方式(性能损耗)// 取消异步 UniTask 方法中的行为,请手动抛出OperationCanceledException。public async UniTask<int> FooAsync(){ await UniTask.Yield(); throw new OperationCanceledException();}// 优化方式(推荐)// 使用UniTask.SuppressCancellationThrow以避免抛出 OperationCanceledException 。它将返回(bool IsCanceled, T Result)而不是抛出异常。var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();if (isCanceled){ HandleTimeout();}
6.2 全局异常监听
当检测到取消时,所有方法都会向上游抛出并传播OperationCanceledException。当异常(不限于OperationCanceledException)没有在异步方法中处理时,它将被传播到UniTaskScheduler.UnobservedTaskException。默认情况下,将接收到的未处理异常作为一般异常写入日志。可以使用UniTaskScheduler.UnobservedExceptionWriteLogType更改日志级别。若想对接收到未处理异常时的处理进行自定义,请为UniTaskScheduler.UnobservedTaskException设置一个委托
UniTaskScheduler.UnobservedTaskException += ex =>{ Debug.LogError($\"未处理异常: {ex.Message}\");};
6.3 异常过滤处理
只想处理异常,忽略取消操作(让其传播到全局处理 cancellation 的地方),使用异常过滤器。
public async UniTask<int> BarAsync(){ try { var x = await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) // 在 C# 9.0 下改成 when (ex is not OperationCanceledException) { return -1; }}
Forget 方法 UniTask提供 同步方法中调用异步方法 不想await 又不想有警告 可用Forget
7、UniTaskCompleitonSource 的使用:设置结果与取消任务
在 Unity 开发中,使用异步编程可以有效提升应用的响应速度和用户体验。UniTask 是一个专为 Unity 设计的高性能异步库,它提供了类似于 C# Task 的功能,但更加轻量级且性能优化。本文将介绍如何使用 UniTaskCompletionSource 来手动控制异步任务的完成和取消。
什么是 UniTaskCompletionSource?
UniTaskCompletionSource 是 UniTask 提供的一个工具类,允许开发者手动控制异步任务的完成、失败或取消。它类似于 .NET 中的 TaskCompletionSource,但针对 Unity 环境进行了优化。
通过 UniTaskCompletionSource,我们可以在需要的地方创建一个未完成的任务,并在适当的时机手动设置其结果或取消状态,从而实现对异步流程的精确控制。
示例:设置结果和取消任务
以下示例演示了如何使用 UniTaskCompletionSource 来创建一个可手动控制的异步任务,并通过按钮点击事件来设置任务结果或取消任务。
using UnityEngine;using Cysharp.Threading.Tasks;using System;public class Lesson14_SetResultAndCancel : MonoBehaviour{ private UniTaskCompletionSource<string> _uniTaskCompletionSource; async void Start() { // 初始化任务源 _uniTaskCompletionSource = new UniTaskCompletionSource<string>(); try { // 等待任务完成 string result = await _uniTaskCompletionSource.Task; Debug.Log(\"任务完成,结果为:\" + result); } catch (OperationCanceledException) { Debug.Log(\"任务被取消\"); } catch (Exception ex) { Debug.LogError($\"任务执行出错: {ex.Message}\"); } } void OnGUI() { // 创建设置值按钮 if (GUI.Button(new Rect(540, 120, 120, 120), \"设置值\")) { // 设置任务结果 _uniTaskCompletionSource.TrySetResult(\"任务执行成功!\"); } // 创建取消按钮 if (GUI.Button(new Rect(540, 300, 120, 120), \"取消任务\")) { // 取消任务 _uniTaskCompletionSource.TrySetCanceled(); } }}
在上述代码中:
初始化任务源:在 Start 方法中,创建了一个 UniTaskCompletionSource 实例 _uniTaskCompletionSource。
等待任务完成:使用 await _uniTaskCompletionSource.Task 来等待任务的完成。如果任务被取消,会捕获 OperationCanceledException 异常;如果发生其他异常,会进行相应的错误处理。
设置任务结果:在 OnGUI 方法中,创建了一个按钮,当点击该按钮时,调用 _uniTaskCompletionSource.TrySetResult(“任务执行成功!”) 来手动设置任务的结果。
取消任务:同样在 OnGUI 方法中,创建了另一个按钮,当点击该按钮时,调用 _uniTaskCompletionSource.TrySetCanceled() 来取消任务。
注意事项
多次设置任务状态:TrySetResult、TrySetCanceled 和 TrySetException 方法都是尝试设置任务的状态,如果任务已经完成或被取消,再次调用这些方法将不会生效。因此,确保在任务未完成时才调用这些方法。
异常处理:在等待任务的过程中,建议使用 try-catch 块来捕获可能出现的异常,特别是 OperationCanceledException,以便正确处理任务取消的情况。
线程安全:UniTaskCompletionSource 的方法是线程安全的,可以在不同的线程中调用。但在 Unity 中,通常建议在主线程中操作 UI 和游戏对象。
通过上述示例和注意事项,我们可以在 Unity 中使用 UniTaskCompletionSource 来精确控制异步任务的完成和取消,从而编写出更为灵活和高效的异步代码。
五、常见问题解答
Q: UniTask 和协程哪个更好?
A: 大多数情况下 UniTask 更好,特别是需要返回值、异常处理或组合多个异步操作时。
Q: UniTask 会产生垃圾吗?
A: 几乎不会!这是 UniTask 的最大优势之一。
Q: 可以在 WebGL 中使用吗?
A: 可以!UniTask 完全支持 WebGL。
Q: 如何调试 UniTask?
A: 使用 Unity 的普通调试方法即可,UniTask 也提供了 TaskTracker 窗口可视化查看任务状态。
六、总结
UniTask 为 Unity 带来了现代化的异步编程体验,相比传统协程和 Task 有显著优势:
- ✅ 代码更简洁易读
- ✅ 性能更高,几乎零GC
- ✅ 更好的异常处理
- ✅ 更灵活的取消机制
- ✅ 原生支持 Unity 的各种异步操作
从今天开始尝试用 UniTask 替换你的协程和异步代码,你会发现异步编程原来可以如此简单高效!
专栏推荐
完结
好了,我是向宇
,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!