792 字
4 分钟
Unity 异步之 UniTask
TIP

原文章地址 本文章是原贴文章的简体中文加与加入个人理解的总结

在 Unity 开发中,异步处理是无法避免的,例如资源加载,网络请求或等待游戏流程事件。Coroutine 是 Unity 内置且最常见的方案,但实际项目中非常容易出现流程分散,可读性下降,以及性能受限等问题。

UniTask 的优势#

高效轻量#

UniTask 的设计目标时间少 GC 分配,解决 Unity 中过多的内存回收问题,相较于 Task 与 Coroutine,性能表现更加出色。

使用方便#

基于 C# 的 async/await 语法,让异步程序像同步程序一样直观可读,可以摆脱回调地狱。

与 Unity 深度整合#

内置了许多针对 Unity 的 API,例如等待帧结束,条件达成,场景加载等,完全满足日常的开发需求。

使用方式#

基本结构#

UniTask 的核心是 async/await。以下是一个简单的例子。

using Cysharp.Threading.Tasks;
using UnityEngine;
public class UniTaskDemo : MonoBehaviour
{
private async void Start()
{
await TaskExample(); // <--
Debug.Log("Task Finished");
}
private async UniTask TaskExample()
{
Debug.Log("Task Started");
await UniTask.Delay(2000); // Delay 2 seconds
Debug.Log("Delay Finished");
}
}

alt text

  • UniTask.Delay是常用的异步 API。
  • UniTask是async支援的只要单位。

另外,也可以使用 UniTask.Create 创建一个 UniTask:

private async void Start()
{
await UniTask.Create(async () =>
{
Debug.Log("Task Started");
await UniTask.Delay(2000); // Delay 2 seconds
Debug.Log("Delay Finished");
});
Debug.Log("Task Finished");
}

延迟控制#

使用UniTask替代Coroutine的yield进行时间或帧数控制,例如:

await UniTask.Yield(); // replacement of yield return null
await UniTask.Delay(TimeSpan.FromSeconds(2), ignoreTimeScale: false);
await UniTask.DelayFrame(30);
await UniTask.WaitForEndOfFrame();

等待条件达成#

WaitUntil可以等待玩家操作或某些条件成立,另外也有WhenAll,WhenAny可以使用。

await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
Debug.Log("Space key pressed");
await UniTask.WhenAll(Task1(), Task2());
Debug.Log("All tasks completed");
await UniTask.WhenAny(Task1(), Task2());
Debug.Log("One of the tasks completed");
private async UniTask Task1()
{
await UniTask.Delay(1000);
Debug.Log("Task 1 completed!");
}
private async UniTask Task2()
{
await UniTask.Delay(2000);
Debug.Log("Task 2 completed!");
}

alt text

整合 Unity API#

如果希望 Unity API 可以使用 UnityTask 的功能,可以通过ToUniTask()将不同类型的异步操作转换到UniTask的生态中。

例如,同时加载多个资源并等待他们全部完成。

var (asset1, asset2) = await UniTask.WhenAll(
Resources.LoadAsync<TextAsset>("Asset1").ToUniTask(),
Resources.LoadAsync<TextAsset>("Asset2").ToUniTask());

另外,ToUniTask可以搭配Progress使用,实时监控加载速度:

var progress = Progress.Create<float>(x => Debug.Log($"Progress: {x * 100}%"));
var asset3 = await Resources
.LoadAsync<TextAsset>("Asset3")
.ToUniTask(progress);

取消操作#

搭配CancellationToken管理任务生命周期:

var cts = new CancellationTokenSource();
try
{
await UniTask.Delay(5000, cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
Debug.Log("Cancelled!");
}
finally
{
cts.Dispose();
}

在 Unity 2022.2 之后,你甚至可以在MonoBehaviour中使用DestoryCancellationToken:

var asset4 = await Resources
.LoadAsync<TextAsset>("Asset4")
.WithCancellation(this.GetCancellationTokenOnDestroy()); // <--

超时任务#

可设置任务的超时时间,避免长时间阻塞:

var timeout = TimeSpan.FromSeconds(3);
await UniTask.Delay(5000).TimeoutWithoutException(timeout);

原任务需要5s才能完成,超时时间只有3s,等待3s后,超时机制先结束等待;不会因为超时而抛出TimeoutException

与 UGUI 时间整合#

将 Unity 的按钮事件与异步操作结合:

await button.OnClickAsync(); // Wait for button click
Debug.Log("Button clicked!");
await button.OnClickAsAsyncEnumerable()
.Take(3)
.ForEachAsync(_ => Debug.Log("Button clicked!"));
Debug.Log("Task completed!");

alt text

与 Unity 其他插件整合#

UniTask 支持其他常用的插件,例如 Addressables,DOTween:

// Addressables
await Addressables.LoadAssetAsync<GameObject>("MyPrefab").ToUniTask();
// DOTween
await transform.DOMove(Vector3.zero, 2f).ToUniTask();

无论你是刚接触异步编程的新手,还是经验丰富的老手,UniTask 都能带了全新的开发体验。