注意
:考虑到后续接触的插件会越来越多,我将插件相关的内容单独分开,并全部整合放在【推荐100个unity插件】专栏里,感兴趣的小伙伴可以前往逐一查看学习。
文章目录
前言
1、UniTask 是什么?
UniTask 是专为 Unity 设计的高性能异步编程库,它提供了比 C# 原生 Task 更轻量、更高效的异步解决方案。简单来说,它让 Unity 中的异步编程(比如加载资源、等待时间、网络请求等)变得更简单、更快速,而且不产生GC内存垃圾
。
如果你还不理解什么是GC,可以参考:【从零开始入门unity游戏开发之——C#篇04】栈(Stack)和堆(Heap),值类型和引用类型,以及特殊的引用类型string,垃圾回收(GC)
2、为什么需要 UniTask?
想象你在做游戏时需要:
- 等待 3 秒后显示一个提示
- 加载一个大资源时不卡住游戏
- 等待玩家点击按钮后再继续
传统做法是用协程(Coroutine)或C#的Task。
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
- 从 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 方法中等待按钮被点击三次后再执行后续代码,适用于需要捕捉用户快速连续操作的场景。
/// <summary>
/// 该方法使用异步 LINQ 监听 btn3 三次点击。
/// 注意:该方法只会监听一次三连点击,不会持续监听。
/// 如果需要持续监听,需要自己加while
/// </summary>
/// <param name="cancellationToken"></param>
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,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!