UniTask v2 — Zero Allocation async/await for Unity, with Asynchronous LINQ
UniTask
- 为Unity提供一个高性能,0 GC 的 async/await 异步方案。
- 基于值类型的
UniTask<T>
和自定义的AsyncMethodBuilder
来实现0GC。 - 使所有 Unity 的 AsyncOperations 和 Coroutines 可等待。
- 基于 PlayerLoop 的任务(
UniTask.Yield
,UniTask.Delay
,UniTask.DelayFrame
, etc..) 可以替换所有协程操作。 - 对 MonoBehaviour 消息事件和 uGUI 事件进行 可等待/异步枚举 拓展。
- 完全在 Unity 的 PlayerLoop 上运行,因此可以不使用 Thread。
- 提供一个 TaskTracker EditorWindow 以追踪所有UniTask分配来预防内存泄漏。
- 与原生 Task/ValueTask/IValueTaskSource 高度兼容的行为。
安装
- 通过 [[OpenUPM]] 安装。
1
openupm-cn add com.cysharp.unitask
命名空间
1
using Cysharp.Threading.Tasks;
返回类型
1
2
3
4
5
6
7
8
9
10
11
12
13
// 替代 async void, 是 UniTask的轻量级版本
async UniTaskVoid
// 如果要调用返回 UniTaskVoid 的方法,需要 Forget
TestAsync().Forget();
// UniTask 可以用到 Mono 的 Start 方法中
private async UniTaskVoid Start()
{
}
// 替代Task<T>的轻量级方案
async UniTask<T>
await
- 不能多次等待同一个 await 实例,每个 await 实例最多只能等待一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 等待到下一帧
await UniTask.Yield(); // 最轻量,最快
await UniTask.NextFrame(); // = = yield return null
// 等待任何 playerloop 的生命周期
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
// 等待此帧结束 = yield return new WaitForEndOfFrame
await UniTask.WaitForEndOfFrame(this);
// 等待100帧(就像一个协程一样)
await UniTask.DelayFrame(100);
// 等待特定时长,并且可以设置在 PlayerLoop 返回的时刻 = new WaitForSeconds()
await UniTask.Delay(TimeSpan.FromSeconds(1), true, PlayerLoopTiming.Update);
// yield return WaitUntil 替代方案
await UniTask.WaitUntil(() => isActive == false);
// WaitUntil拓展,指定某个值改变时触发
await UniTask.WaitUntilValueChanged(this, x => x.isActive);
// await一个协程
await FooCoroutineEnumerator();
// 你可以直接 await 一个原生 task
await Task.Run(() => 100);
取消和超时
取消之后, await 后的逻辑不再执行。
- new 一个 CancellationTokenSource
1
2
3
4
5
6
7
8
9
10
// 使用 CancellationTokenSource
var cts = new CancellationTokenSource();
// 可选,设置自动超时
cts.CancelAfterSlim(TimeSpan.FromSeconds(5f));
cancelButton.onClick.AddListener(() =>
{
cts.Cancel();
});
await UniTask.DelayFrame(1000, cancellationToken: cts.Token).SuppressCancellationThrow();
- 调用 MonoBehaviour 的 GetCancellationTokenOnDestroy
1
2
3
4
5
6
7
// 获取一个依赖对象生命周期的Cancel句柄
// 当对象被销毁时,将会调用这个Cancel句柄,从而实现取消的功能
CancellationToken cancellationToken = this.GetCancellationTokenOnDestroy();
// 为异步操作启用取消功能
var (isCanceled, result) = await Resources.LoadAsync<TextAsset>("bar")
.WithCancellation(cancellationToken).SuppressCancellationThrow();
- 取消链式异步方法,所有异步方法都建议最后一个参数接受 cancellationToken,并将 CancellationToken 从头传递到尾。
1
2
3
4
5
6
7
8
9
10
await FooAsync(this.GetCancellationTokenOnDestroy());
// ---
async UniTask FooAsync(CancellationToken cancellationToken)
{
await BarAsync(cancellationToken).SuppressCancellationThrow();
}
async UniTask BarAsync(CancellationToken cancellationToken)
{
await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken).SuppressCancellationThrow();
}
- 任意生命周期内取消异步操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyBehaviour : MonoBehaviour
{
CancellationTokenSource disableCancellation = new CancellationTokenSource();
CancellationTokenSource destroyCancellation = new CancellationTokenSource();
private void OnEnable()
{
if (disableCancellation != null)
{
disableCancellation.Dispose();
}
disableCancellation = new CancellationTokenSource();
}
private void OnDisable()
{
disableCancellation.Cancel();
}
private void OnDestroy()
{
destroyCancellation.Cancel();
destroyCancellation.Dispose();
}
}
- 链接多个取消Token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var cancelToken = new CancellationTokenSource();
cancelButton.onClick.AddListener(()=>
{
cancelToken.Cancel(); // 点击按钮后取消
});
var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 设置5s超时
try
{
// 链接token
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token);
await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token);
}
catch (OperationCanceledException ex)
{
if (timeoutToken.IsCancellationRequested)
{
UnityEngine.Debug.Log("Timeout.");
}
else if (cancelToken.IsCancellationRequested)
{
UnityEngine.Debug.Log("Cancel clicked.");
}
}
进度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Progress.Create
var asset = await Resources.LoadAsync<TextAsset>("baz")
.ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
// 实现 IProgress<T> 接口
public class Foo : MonoBehaviour, IProgress<float>
{
public void Report(float value)
{
UnityEngine.Debug.Log(value);
}
public async UniTaskVoid WebRequest()
{
var request = await UnityWebRequest.Get("http://google.co.jp")
.SendWebRequest()
.ToUniTask(progress: this);
}
}
PlayerLoop
- Delay,DelayFrame,ToUniTask 等可以接受 PlayerLoopTiming 枚举值参数,指定异步何时运行。
- PlayerLoopList.md · GitHub
MonoBehaviour 事件
- 所有 MonoBehaviour 消息事件都可以转换异步流
AsyncTriggers
, - 可以通过
using Cysharp.Threading.Tasks.Triggers;
启用 - 通过
this.GetAsync***Trigger
触发,再通过GetAsync***Handler
获取句柄
1
2
3
4
5
6
7
8
9
10
11
// every update
this.GetAsyncUpdateTrigger().ForEachAsync(_ =>
{
Debug.Log("VAR");
});
// 碰撞两次后执行
var trigger = this.GetAsyncCollisionEnterTrigger().GetOnCollisionEnterAsyncHandler();
await trigger.OnCollisionEnterAsync();
await trigger.OnCollisionEnterAsync();
Debug.Log("VAR");
实用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UniTask.WhenAll() 等待所有异步结束
UniTask.WhenAny() 等待任何一个异步结束
// ---------------------------
public async UniTaskVoid LoadManyAsync()
{
// 并行加载.
var (a, b, c) = await UniTask.WhenAll(
LoadAsSprite("foo"),
LoadAsSprite("bar"),
LoadAsSprite("baz"));
}
async UniTask<Sprite> LoadAsSprite(string path)
{
var resource = await Resources.LoadAsync<Sprite>(path);
return (resource as Sprite);
}
异步流
- asynchronous stream support
- 所有标准 LINQ 查询运算符都可以应用于异步流.
- 除了标准查询运算符之外,还有其他 Unity 生成器,例如
EveryUpdate
、Timer
、TimerFrame
、Interval
、IntervalFrame
和EveryValueChanged
。并且还添加了额外的 UniTask 原始查询运算符,如Append
,Prepend
,DistinctUntilChanged
,ToHashSet
,Buffer
,CombineLatest
,Do
,Never
,ForEachAsync
,Pairwise
,Publish
,Queue
,Return
,SkipUntil
,TakeUntil
,SkipUntilCanceled
,TakeUntilCanceled
,TakeLast
,Subscribe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下面的语法可以代替 Update
await foreach (var _ in UniTaskAsyncEnumerable.EveryUpdate(token))
{
Debug.Log("Update() " + Time.frameCount);
}
// 将 Where 过滤器应用于每两次单击运行一次的按钮单击异步流
await okButton.OnClickAsAsyncEnumerable().Where((x, i) => i % 2 == 0).ForEachAsync(_ =>
{
});
// fire and forget,每两次响应一次,和 ForEachAsync 结果相同
okButton.OnClickAsAsyncEnumerable().Where((x, i) => i % 2 == 0).Subscribe(_ =>
{
});
UniTaskTracker
- 通过 Window -> UniTask Tracker 打开跟踪器窗口。
- UniTaskTracker 仅用于调试用途,因为启用跟踪和捕获堆栈跟踪很有用,但会对性能产生重大影响。推荐的用法是启用跟踪和堆栈跟踪以查找任务泄漏并在完成时禁用它们。
使用线程
1
2
3
4
5
// 之后切换到线程池模式
await UniTask.SwitchToThreadPool();
// 转回主线程
await UniTask.SwitchToMainThread();
DoTween支持
[[DOTween]]
- 需要添加
UNITASK_DOTWEEN_SUPPORT
符号定义?[[CS-操作符和控制流程#预处理器指令]] - DOTween 支持的默认行为 (await, WithCancellation, ToUniTask)
- await 等待动画播放完毕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void Start()
{
MoveAsync().Forget();
}
private async UniTaskVoid MoveAsync()
{
await transform.DOMoveX(2, 10);
await transform.DOMoveZ(5, 20);
var ct = this.GetCancellationTokenOnDestroy();
await UniTask.WhenAll(
transform.DOMoveX(10, 3).WithCancellation(ct),
transform.DOScale(10, 3).WithCancellation(ct));
)
}
Addressables支持
[[Addressable]]
- 等待
AsyncOperationHandle
,AsyncOperationHandle<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//要读取的 AssetReference
[SerializeField] AssetReference _target;
[SerializeField] private RawImage _image;
private void Start()
{
var token = this.GetCancellationTokenOnDestroy();
InitializeAsync(_target, token).Forget();
}
private async UniTaskVoid InitializeAsync(AssetReference target, CancellationToken token)
{
//等待 Addressables.load 与 await 异步加载资产
var texture = await Addressables.LoadAssetAsync<Texture>(target)
.WithCancellation(token);
_image.texture = texture;
}
Resources支持
[[Resources文件夹]]
1
2
// 直接等待Unity的AsynchronousObject
var asset = await Resources.LoadAsync<TextAsset>("foo");
SceneManager支持
[[Unity-SceneManager]]
- 不要使用 LoadSceneAsync.ToUniTask
1
await SceneManager.LoadSceneAsync("scene2");
uGUI事件支持
- 所有 uGUI 组件都实现了
AsAsyncEnumerable
异步事件流的转换。 - 使用异步可迭代和 LINQ Where 查询操作符实现每两次点击按钮执行操作
1
2
3
4
5
6
7
8
private void Awake()
{
// fire and forget
okButton.OnClickAsAsyncEnumerable().Where((x, i) => i % 2 == 0).Subscribe(_ =>
{
Debug.Log("Click every two times");
});
}
- Demo - 检测3次点击事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void Start()
{
TripleClick().Forget();
}
async UniTask TripleClick()
{
while(true)
{
// 默认情况下,使用了button.GetCancellationTokenOnDestroy 来管理异步生命周期
await button.OnClickAsync();
await button.OnClickAsync();
await button.OnClickAsync();
Debug.Log("Three times clicked");
}
}
// 更高效的方法
async UniTask TripleClick()
{
using var handler = okButton.GetAsyncClickEventHandler();
await handler.OnClickAsync();
await handler.OnClickAsync();
await handler.OnClickAsync();
Debug.Log("Three times clicked");
}
// 使用异步LINQ
async UniTask TripleClick(CancellationToken token)
{
await okButton.OnClickAsAsyncEnumerable().Take(3).LastAsync();
Debug.Log("Three times clicked");
}
// 使用异步LINQ
async UniTask TripleClick(CancellationToken token)
{
await button.OnClickAsAsyncEnumerable().Take(3).ForEachAsync(_ =>
{
// 每一次点击都执行
Debug.Log("Every clicked");
});
// 3次之后才执行
Debug.Log("Three times clicked, complete.");
}
UnityWebRequest支持
[[UnityWebRequest]]
1
2
3
4
5
6
7
8
// 1
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
// 2
UnityWebRequest req = new UnityWebRequest();
var op = await req.SendWebRequest();
return op.downloadHandler.text;
// 3
var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));