> 技术文档 > 【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件

【推荐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异步编程

但它们都有缺点:

技术方案 主要问题 GC压力 线程安全 易用性 协程(Coroutine) 不能返回值,异常处理困难 中 主线程 一般 C# Task 与Unity主线程调度不兼容 高 多线程 复杂 UniTask 无 几乎为零 自动同步 优秀

UniTask专为Unity设计,解决了上述所有痛点,提供了:

  • 零GC的高性能异步操作
  • 完美的Unity主线程集成
  • 直观的async/await语法
  • 丰富的Unity特定功能

一、基础入门

1、UniTask官方地址

  • github地址:https://github.com/Cysharp/UniTask
  • 码云地址:https://gitee.com/unity_data/UniTask

2、安装 UniTask

  1. 从 GitHub 下载最新版本:https://github.com/Cysharp/UniTask/releases
    【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件
  2. 下载 .unitypackage 文件并导入到你的项目中
  3. 在代码中添加命名空间: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 UniTaskasync 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 生命周期的不同阶段执行。
【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件

  • 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\"); } }}

结果
【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件

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 协程,我们可以方便地将传统协程嵌入到异步方法中,无需担心协程的调度问题。

结果
【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——UniTask插件

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 高效的异步操作。

结果
【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——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,充分利用各自的优势,从而使代码结构更清晰。

结果
【推荐100个unity插件】比 Unity 自带协程更高效的异步处理方式,高性能和0GC的async/await异步方案——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 替换你的协程和异步代码,你会发现异步编程原来可以如此简单高效!


专栏推荐

地址 【unity游戏开发入门到精通——C#篇】 【unity游戏开发入门到精通——unity通用篇】 【unity游戏开发入门到精通——unity3D篇】 【unity游戏开发入门到精通——unity2D篇】 【unity实战】 【制作100个Unity游戏】 【推荐100个unity插件】 【实现100个unity特效】 【unity框架/工具集开发】 【unity游戏开发——模型篇】 【unity游戏开发——InputSystem】 【unity游戏开发——Animator动画】 【unity游戏开发——UGUI】 【unity游戏开发——联网篇】 【unity游戏开发——优化篇】 【unity游戏开发——shader篇】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述