Unity 异步编程 基础使用
Unity 异步编程 基础使用
1.前言
先前开发游戏过程中,有用到Zenject + UniTask + UniRx + Localization + GoogleSheet + Addressable的方式来开发游戏,后续会逐个介绍,本篇进行UniTask的介绍,希望起到抛砖引玉作用。
传统的异步编程通常使用 Coroutine 或 Task,但它们各有优缺点。UniTask 是一个专为Unity设计的高性能异步编程库,它结合了 Coroutine 和 Task 的优点,提供了更简洁、更高效的异步编程方式。本文将介绍一下UniTask的基础。
2.UniTask 的好处
- 高性能:UniTask 是专为Unity设计的,性能比 Task 更高,特别是在频繁创建和销毁任务的场景中。
- 简洁的语法:UniTask 提供了类似于 async/await 的语法,使得异步代码更加简洁和易读。
与Unity无缝集成:UniTask 支持 UnityEngine 的各种异步操作,如 WaitForSeconds、WaitForEndOfFrame 等。 - 丰富的功能:UniTask 提供了许多高级功能,如延续操作、取消和异常处理、超时处理等。
UniTask 的常用语法基础
可以将类型返回为 struct UniTask(或 UniTask),它是 Task 的Unity专用轻量级替代方案
//实现0开销(0GC和快速执行)的async/await Unity集成
async UniTask<string> DemoAsync()
{
//可以await Unity async对象
var asset = await Resources.LoadAsync<TextAsset>("foo");
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
await SceneManager.LoadSceneAsync("scene2");
//.WithCancellation 启用取消方法,GetCancellationTokenOnDestroy 与 GameObject 的生命周期同步
var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());
//.ToUniTask 接受进度回调(和完整的参数),Progress.Create 是 IProgress<T> 的轻量级替代品
var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
//像协程一样,是await基于帧的操作
await UniTask.DelayFrame(100);
//替换 yield return new WaitForSeconds/WaitForSecondsRealtime
await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
//产生任何播放器循环时间(PreUpdate、Update、LateUpdate 等...)
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
//替换 yield return null
await UniTask.Yield();
await UniTask.NextFrame();
//替换 WaitForEndOfFrame(需要 MonoBehaviour(CoroutineRunner))
await UniTask.WaitForEndOfFrame(this); //这是 MonoBehaviour
//替换 yield return new WaitForFixedUpdate(同 UniTask.Yield(PlayerLoopTiming.FixedUpdate))
await UniTask.WaitForFixedUpdate();
//替换 yield return WaitUntil
await UniTask.WaitUntil(() => isActive == false);
//WaitUntil 的帮助方法
await UniTask.WaitUntilValueChanged(this, x => x.isActive);
//可以await IEnumerator 协程
await FooCoroutineEnumerator();
//可以await C#标准Task
await Task.Run(() => 100);
//多线程,此代码下运行在 ThreadPool 上
await UniTask.SwitchToThreadPool();
/*在线程池上工作*/
//返回主线程(与 UniRx 中的 `ObserveOnMainThread` 相同)
await UniTask.SwitchToMainThread();
//获取async网络请求
async UniTask<string> GetTextAsync(UnityWebRequest req)
{
var op = await req.SendWebRequest();
return op.downloadHandler.text;
}
var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
//并发async await并通过元组语法轻松获取结果
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
//WhenAll的简写,tuple可以直接await
var (google2, bing2, yahoo2) = await (task1, task2, task3);
//返回async值。(或者您可以使用 `UniTask`(无结果)、`UniTaskVoid`(即发即弃))。
return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}
3.UniTask 的常用语法晋级
3.1 延续操作(返回值元组)
1.UniTask.WhenAll(全部完成后)
2.UniTask.WhenAny(任一完成后)
public async UniTaskVoid LoadManyAsync()
{
// 并行加载.
var (a, b, c) = await UniTask.WhenAll(
LoadAsSprite("foo"),
LoadAsSprite("bar"),
LoadAsSprite("baz"));
}
延续操作,也可通过返回值元组来实现
//同时返回
private async UniTask<(int, string)> PerformTasks()
{
int result1 = await Task1();
string result2 = await Task2();
return (result1, result2);
}
private async UniTask<int> Task1()
{
await UniTask.Delay(1000);
return 42;
}
private async UniTask<string> Task2()
{
await UniTask.Delay(2000);
return "Hello, UniTask!";
}
3.2 取消和异常处理
UniTask 支持任务的取消和异常处理,可以使用 CancellationToken 和 try catch 语句来实现。
private void Start()
{
_cts = new CancellationTokenSource();
PerformTask(_cts.Token).Forget();
}
private void OnDestroy()
{
_cts.Cancel();
}
//支持try catch
private async UniTaskVoid PerformTask(CancellationToken token)
{
try
{
await UniTask.Delay(5000, cancellationToken: token);
Debug.Log("Task completed");
}
catch (OperationCanceledException)
{
Debug.Log("Task was cancelled");
}
}
CancellationToken表示异步的生命周期
一些UniTask工厂方法有一个CancellationToken cancellationToken = default参数。此外,Unity的一些异步操作有WithCancellation(CancellationToken)和ToUniTask(…, CancellationToken cancellation = default)扩展方法
下边是个标准的CancellationTokenSource用法
var cts = new CancellationTokenSource();
cancelButton.onClick.AddListener(() =>
{
cts.Cancel();
});
await UnityWebRequest.Get("http://www.baidu.com").SendWebRequest().WithCancellation(cts.Token);
await UniTask.DelayFrame(1000, cancellationToken: cts.Token);
3.3超时处理
UniTask 提供了超时处理功能,使用 Timeout ,CancelAfterSlim,CreateLinkedTokenSource ,TimeoutController ,等来实现。
下边是简单 Timeout 的使用
private async void Start()
{
try
{
await PerformTask().Timeout(TimeSpan.FromSeconds(3));
Debug.Log("Task completed");
}
catch (TimeoutException)
{
Debug.Log("Task timed out");
}
}
private async UniTask PerformTask()
{
await UniTask.Delay(5000);
}
3.4返回值(*)
UniTask 支持多种返回值类型,包括 void、UniTask、UniTask 等等。
下边是个简单的示例
private async void Start()
{
await PerformVoidTask();
int result = await PerformIntTask();
Debug.Log($"Result: {result}");
}
private async UniTaskVoid PerformVoidTask()
{
await UniTask.Delay(1000);
Debug.Log("Void task completed");
}
//进行Int类型的返回
private async UniTask<int> PerformIntTask()
{
await UniTask.Delay(2000);
return 42;
}
3.5注册到Event的异步委托(lambda)
UniTask 支持将异步委托注册到事件中,可以使用 async 关键字来实现。
public event Func<UniTask> OnTaskRequested;
private async void Start()
{
//进行委托的注入
OnTaskRequested += async () =>
{
await UniTask.Delay(1000);
Debug.Log("Event task completed");
};
if (OnTaskRequested != null)
{
await OnTaskRequested.Invoke();
}
}
不要使用async void。可以使用UniTask.Action或UniTask.UnityAction,它们都通过async UniTaskVoid lambda创建一个委托。
// 这是不好的: async void
actEvent += async () => { };
unityEvent += async () => { };
// 这是好的: 通过lamada创建Action
actEvent += UniTask.Action(async () => { await UniTask.Yield(); });
unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });
3.6委托中生成UniTask的工厂方法
当然也 可以在委托中使用工厂方法来生成UniTask。
使用工厂方法来生成UniTask,并在委托中调用该方法,下边是示例:
private async void Start()
{
Func<UniTask> taskFactory = () => PerformTask();
await taskFactory.Invoke();
}
private async UniTask PerformTask()
{
await UniTask.Delay(1000);
Debug.Log("Factory task completed");
}
4.UniTask 注意事项
- 性能:虽然UniTask性能较高,但在频繁创建和销毁任务的场景中,仍需注意性能优化。
- 取消和异常处理:在使用取消和异常处理时,需要确保正确处理 CancellationToken 和异常,以避免内存泄漏和未处理的异常。
- 与Unity主线程的交互:在异步任务中操作Unity对象时,需要确保在主线程中执行,可以使用 UniTask.SwitchToMainThread 方法来切换到主线程。
- IEnumerator.ToUniTask 限制,不支持WaitForEndOframe/WaitForFixedUpdate/Coroutine
生命周期与StartCoroutine不一样,它使用指定的PlayerLoopTiming,默认的PlayerLoopTiming.Update会在MonoBehaviour的Update和StartCoroutine的循环之前运行。 - 部分平台兼容较差,比如微信小游戏平台
5.总结
通过本文的介绍,相信大家对UniTask有了一个全面的了解。UniTask 是一个非常强大的异步编程库,可以帮助我们更高效地编写异步代码,提高代码的可读性和可维护性。希望大家在实际项目中能够灵活应用UniTask,写出高质量的代码,新人创作不易,谢谢大家。