【Unity工具】UniTask笔记
聪头 游戏开发萌新

UniTask

时间:2023年11月28日13:34:19

———- 官方文档 ———-

开源库地址:https://github.com/Cysharp/UniTask

中文文档:https://github.com/Cysharp/UniTask/blob/master/README_CN.md

基础用法

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 使用UniTask所需的命名空间
using Cysharp.Threading.Tasks;

// 你可以返回一个形如 UniTask<T>(或 UniTask) 的类型,这种类型事为Unity定制的,作为替代原生Task<T>的轻量级方案
// 为Unity集成的 0GC,快速调用,0消耗的 async/await 方案
async UniTask<string> DemoAsync()
{
// 你可以等待一个Unity异步对象
var asset = await Resources.LoadAsync<TextAsset>("foo");
var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
await SceneManager.LoadSceneAsync("scene2");

// .WithCancellation 会启用取消功能,GetCancellationTokenOnDestroy 表示获取一个依赖对象生命周期的Cancel句柄,当对象被销毁时,将会调用这个Cancel句柄,从而实现取消的功能
var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());

// .ToUniTask 可接收一个 progress 回调以及一些配置参数,Progress.Create是IProgress<T>的轻量级替代方案
var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));

// 等待一个基于帧的延时操作(就像一个协程一样)
await UniTask.DelayFrame(100);

// yield return new WaitForSeconds/WaitForSecondsRealtime 的替代方案
await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);

// 可以等待任何 playerloop 的生命周期(PreUpdate, Update, LateUpdate, 等...)
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);

// yield return null 替代方案
await UniTask.Yield();
await UniTask.NextFrame();

// WaitForEndOfFrame 替代方案 (需要 MonoBehaviour(CoroutineRunner))
await UniTask.WaitForEndOfFrame(this); // 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 一个原生 task
await Task.Run(() => 100);

// 多线程示例,在此行代码后的内容都运行在一个线程池上
await UniTask.SwitchToThreadPool();

/* 工作在线程池上的代码 */

// 转回主线程
await UniTask.SwitchToMainThread();

// 获取异步的 webrequest
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-wait,并通过元组语义轻松获取所有结果
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);

// WhenAll简写形式
var (google2, bing2, yahoo2) = await (task1, task2, task3);

// 返回一个异步值,或者你也可以使用`UniTask`(无结果), `UniTaskVoid`(协程,不可等待)
return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}

UniTaskTracker

对于检查(泄露的)UniTasks 很有用。您可以在Window -> UniTask Tracker中打开跟踪器窗口。

image
  • Enable AutoReload(Toggle) - 自动重新加载。
  • Reload - 重新加载视图(重新扫描内存中UniTask实例,并刷新界面)。
  • GC.Collect - 调用 GC.Collect。
  • Enable Tracking(Toggle) - 开始跟踪异步/等待 UniTask。性能影响:低。
  • Enable StackTrace(Toggle) - 在任务启动时捕获 StackTrace。性能影响:高。

UniTaskTracker 仅用于调试用途,因为启用跟踪和捕获堆栈跟踪很有用,但会对性能产生重大影响。推荐的用法是启用跟踪和堆栈跟踪以查找任务泄漏并在完成时禁用它们。

———- B站教程 ———-

URL:https://www.bilibili.com/video/BV1NG411s7hN/

P1 介绍

原生问题

image image

UniTask的优点

image

安装UniTask

https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask

image

基础使用

image

进阶使用

image image image

P2 空载性能测试

发布版:UniTask比协程快8~10倍

image

P3 基础用法讲解

1.Unity异步操作转换为UniTask

本质:和C#的Task/Await用法一致,只不过返回值从Task变更成了UniTask

1
2
3
4
5
6
private void Start()
{
LoadTextButton.onClick.AddListener(OnClickLoadText); //异步加载文本
LoadSceneButton.onClick.AddListener(OnClickLoadScene); //异步切换场景
WebRequestButton.onClick.AddListener(OnClickWebRequest); //异步发送Web请求
}
image

异步加载文本

MonoBehaviour里实现UniTask

1
2
3
4
5
6
private async void OnClickLoadText()
{
var loadOperation = Resources.LoadAsync<TextAsset>("test");
var text = await loadOperation;
TargetText.text = ((TextAsset) text).text;
}

非MonoBehaviour实现UniTask

  • UniTask本质是一个ValueTask
1
2
3
4
5
private async void OnClickLoadText()
{
UniTaskAsyncSample_Base asyncUnitaskLoader = new UniTaskAsyncSample_Base();
TargetText.text = ((TextAsset) (await asyncUnitaskLoader.LoadAsync<TextAsset>("test"))).text;
}
1
2
3
4
5
6
7
8
public class UniTaskAsyncSample_Base
{
public async UniTask<Object> LoadAsync<T>(string path) where T: Object
{
var asyncOperation = Resources.LoadAsync<T>(path);
return (await asyncOperation);
}
}

异步加载场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private async void OnClickLoadScene()
{
// .ToUniTask 可接收一个 progress 回调以及一些配置参数,Progress.Create是IProgress<T>的轻量级替代方案
await SceneManager.LoadSceneAsync("TargetLoadScene").ToUniTask(
(Progress.Create<float>(
(p) =>
{
LoadSceneSlider.value = p;
if (ProgressText != null)
{
ProgressText.text = $"读取进度{p * 100:F2}%";
}
})));
}

异步发送Web请求

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
private async void OnClickWebRequest()
{
var webRequest =
UnityWebRequestTexture.GetTexture(
"https://s1.hdslb.com/bfs/static/jinkela/video/asserts/33-coin-ani.png");
var result = (await webRequest.SendWebRequest()); //等待请求响应
var texture = ((DownloadHandlerTexture) result.downloadHandler).texture;
int totalSpriteCount = 24;
int perSpriteWidth = texture.width / totalSpriteCount;
Sprite[] sprites = new Sprite[totalSpriteCount];
for (int i = 0; i < totalSpriteCount; i++)
{
sprites[i] = Sprite.Create(texture,
new Rect(new Vector2(perSpriteWidth * i, 0), new Vector2(perSpriteWidth, texture.height)),
new Vector2(0.5f, 0.5f));
}

float perFrameTime = 0.1f;
while (true)
{
for (int i = 0; i < totalSpriteCount; i++)
{
await UniTask.Delay(TimeSpan.FromSeconds(perFrameTime)); //每0.1s播放一张图片
var sprite = sprites[i];
DownloadImage.sprite = sprite;
}
}
}

2.Delay与Wait

1
2
3
4
5
6
7
8
9
10
11
12
private void Start()
{
TestDelayButton.onClick.AddListener(OnClickTestDelay); //延迟秒执行
TestDelayFrameButton.onClick.AddListener(OnClickTestDelayFrame); //延迟帧执行
TestYieldButton.onClick.AddListener(OnClickTestYield); //等待Unity特定生命周期执行
TestNextFrameButton.onClick.AddListener(OnClickTestNextFrame); //等待至下帧执行
TestEndOfFrameButton.onClick.AddListener(OnClickTestEndOfFrame); //等待至帧末尾执行
ClearButton.onClick.AddListener(OnClickClear);

unitaskWaiter = new UniTaskAsyncSample_Wait();
InjectFunction();
}
image

延迟秒执行

1
2
3
4
5
6
private async void OnClickTestDelay()
{
Debug.Log($"执行Delay开始,当前时间{Time.time}");
await UniTask.Delay(TimeSpan.FromSeconds(1));
Debug.Log($"执行Delay结束,当前时间{Time.time}");
}

延迟帧执行

1
2
3
4
5
6
private async void OnClickTestDelayFrame()
{
Debug.Log($"执行DelayFrame开始,当前帧{Time.frameCount}");
await UniTask.DelayFrame(5);
Debug.Log($"执行DelayFrame结束,当前帧{Time.frameCount}");
}

等待Unity特定生命周期执行

1
public PlayerLoopTiming TestYieldTiming = PlayerLoopTiming.PreUpdate;
1
2
3
4
5
6
7
8
private async void OnClickTestYield()
{
_showUpdateLog = true;
Debug.Log($"执行yield开始{TestYieldTiming}");
await unitaskWaiter.WaitYield(TestYieldTiming);
Debug.Log($"执行yield结束{TestYieldTiming}");
_showUpdateLog = false;
}

等待时机详见PlayerLoopTiming

image

等待至下帧执行

1
2
3
4
5
6
7
8
private async void OnClickTestNextFrame()
{
_showUpdateLog = true;
Debug.Log($"执行NextFrame开始");
await unitaskWaiter.WaitNextFrame();
Debug.Log($"执行NextFrame结束");
_showUpdateLog = false;
}

等待至帧末尾执行

1
2
3
4
5
6
7
8
private async void OnClickTestEndOfFrame()
{
_showUpdateLog = true;
Debug.Log($"执行WaitEndOfFrame开始");
await unitaskWaiter.WaitEndOfFrame(this); //额外传一个this
Debug.Log($"执行WaitEndOfFrame结束");
_showUpdateLog = false;
}

3.WhenAll与WhenAny

https://www.bilibili.com/video/BV1PB4y1Y7dx?t=976.1

测试函数:小球会每帧移动一定距离,直到移动到特定位置后将ReachGoal设为true

1
2
3
4
5
6
7
8
9
private void Start()
{
FirstRunButton.onClick.AddListener(OnClickFirstRun); //第一个小球的Run函数
SecondRunButton.onClick.AddListener(OnClickSecondRun); //第二个小球的Run函数

WhenAllButton.onClick.AddListener(OnClickWhenAll);
WhenAnyButton.onClick.AddListener(OnClickWhenAny);
ResetButton.onClick.AddListener(OnClickReset);
}
image

测试函数 Run

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
private async void OnClickFirstRun()
{
await RunSomeOne(FirstRunner);
}

private async void OnClickSecondRun()
{
await RunSomeOne(SecondRunner);
}

private async UniTask RunSomeOne(Runner runner)
{
runner.Reset();
float totalTime = TotalDistance / runner.Speed;
float timeElapsed = 0;
while (timeElapsed <= totalTime)
{
timeElapsed += Time.deltaTime;
await UniTask.NextFrame(); //每帧移动一定距离
float runDistance = Mathf.Min(timeElapsed, totalTime) * runner.Speed;
runner.Target.position = runner.StartPos + Vector3.right * runDistance;
}

runner.ReachGoal = true;
}

WhenAll

WhenAll:等待所有任务满足条件后,继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
private async void OnClickWhenAll()
{
var firstRunnerReach = UniTask.WaitUntil(() => FirstRunner.ReachGoal);
// 注意还有个WaitUntilValueChanged,也很有用!
var secondRunnerReach = UniTask.WaitUntil(() => SecondRunner.ReachGoal);
await UniTask.WhenAll(firstRunnerReach, secondRunnerReach);
// 注意,whenAll可以用于平行执行多个资源的读取,非常有用!
// var (a, b, c) = await UniTask.WhenAll(
//LoadAsSprite("foo"),
//LoadAsSprite("bar"),
//LoadAsSprite("baz"));
CompleteText.text = "双方都抵达了终点,比赛结束";
}

WhenAny

WhenAny:等待其中一个任务满足条件后,继续执行

1
2
3
4
5
6
7
8
private async void OnClickWhenAny()
{
var firstRunnerReach = UniTask.WaitUntil(() => FirstRunner.ReachGoal);
var secondRunnerReach = UniTask.WaitUntil(() => SecondRunner.ReachGoal);
await UniTask.WhenAny(firstRunnerReach, secondRunnerReach);
string winner = FirstRunner.ReachGoal ? "蓝色小球" : "黄色小球";
WinnerText.text = $"{winner}率先抵达了终点,获得了胜利";
}

4.取消

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
private void Start()
{
FirstRunButton.onClick.AddListener(OnClickFirstRun);
SecondRunButton.onClick.AddListener(OnClickSecondRun);

FirstCancelButton.onClick.AddListener(OnClickFirstCancel); //1号球取消
SecondCancelButton.onClick.AddListener(OnClickSecondCancel); //2号球取消

ResetButton.onClick.AddListener(OnClickReset);
_firstCancelToken = new CancellationTokenSource(); //取消Token初始化
// 注意这里可以直接先行设置多久以后取消
// _firstCancelToken = new CancellationTokenSource(TimeSpan.FromSeconds(1.5f));
_secondCancelToken = new CancellationTokenSource();
_linkedCancelToken =
CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token); //LinkToken:其中一个Token取消则取消
}

//使用UniTask<int>作为返回值,实际返回int
private async UniTask<int> RunSomeOne(Runner runner, CancellationToken cancellationToken)
{
runner.Reset();
float totalTime = TotalDistance / runner.Speed;
float timeElapsed = 0;
while (timeElapsed <= totalTime)
{
timeElapsed += Time.deltaTime;
await UniTask.NextFrame(cancellationToken);


float runDistance = Mathf.Min(timeElapsed, totalTime) * runner.Speed;
runner.Target.position = runner.StartPos + Vector3.right * runDistance;
}

runner.ReachGoal = true;
return 0;
}
image

法1:使用异常取消

性能一般

1
2
3
4
5
6
7
8
9
10
11
private async void OnClickFirstRun()
{
try
{
await RunSomeOne(FirstRunner, _firstCancelToken.Token);
}
catch (OperationCanceledException e)
{
FirstText.text = ("1号跑已经被取消");
}
}

法2:使用布尔返回值取消

性能更好

第一个返回值是是否取消

1
2
3
4
5
6
7
8
9
private async void OnClickSecondRun()
{
var (cancelled, _) =
await RunSomeOne(SecondRunner, _linkedCancelToken.Token).SuppressCancellationThrow();
if (cancelled)
{
SecondText.text = ("2号跑已经被取消");
}
}

生成取消信号

需要使用Dispose来重新生成Token

LinkToken:任何一个Token停掉,LinkToken都会自动停掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void OnClickSecondCancel()
{
_secondCancelToken.Cancel();
_secondCancelToken.Dispose();
_secondCancelToken = new CancellationTokenSource();
_linkedCancelToken =
CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token);
}

private void OnClickFirstCancel()
{
_firstCancelToken.Cancel();
_firstCancelToken.Dispose();
_firstCancelToken = new CancellationTokenSource();
_linkedCancelToken =
CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token);
}

P4 基础用法扩展

1.超时处理

1
2
3
4
private void Start()
{
TestButton.onClick.AddListener(UniTask.UnityAction(OnClickTest));
}
image

UniTask解释

async void是一个原生的 C# 任务系统,因此它不能在 UniTask 系统上运行。也最好不要使用它。async UniTaskVoidasync UniTask的轻量级版本,因为它没有等待完成并立即向UniTaskScheduler.UnobservedTaskException报告错误. 如果您不需要等待(即发即弃),那么使用UniTaskVoid会更好。不幸的是,要解除警告,您需要在尾部添加Forget().

要使用注册到事件的异步 lambda,请不要使用async void. 相反,您可以使用UniTask.ActionUniTask.UnityAction,两者都通过async UniTaskVoid lambda 创建委托。

1
2
3
4
5
6
7
8
9
10
Action actEvent;
UnityAction unityEvent; // UGUI特供

// 这样是不好的: async void
actEvent += async () => { };
unityEvent += async () => { };

// 这样是可以的: 通过lamada创建Action
actEvent += UniTask.Action(async () => { await UniTask.Yield(); });
unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });

测试:如果async()=>{ … } 如果内容部分不含await,则不会有异步操作。

  • 上述例子举的很差,因为async lambda里面没有await任何内容,这个反例根本就没可比性
image

测试:如果async()=>{ … } 如果内容部分含有await,依旧会正常运行UniTask,只是会给出警告。因此实际这样书写没毛病,如果想把警告去了就老老实实使用UniTask.UnityAction吧

image image

超时取消

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
private async UniTaskVoid OnClickTest()
{
UniTask<string>[] waitTasks = new UniTask<string>[SearchURLs.Length];
for (int i = 0; i < SearchURLs.Length; i++)
{
waitTasks[i] = GetRequest(SearchURLs[i], 2f);
}

var tasks = await UniTask.WhenAll(waitTasks);
for (int i = 0; i < tasks.Length; i++)
{
Texts[i].text = tasks[i];
}
}

private async UniTask<string> GetRequest(string url, float timeout)
{
var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout)); // 设置超时时间

var (cancelOrFailed, result) = await UnityWebRequest.Get(url).SendWebRequest().WithCancellation(cts.Token).SuppressCancellationThrow();
if (!cancelOrFailed)
{
return result.downloadHandler.text.Substring(0, 100);
}

return "取消或超时";
}

2.同步方法调异步

无等待FireAndForget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void Start()
{
StartButton.onClick.AddListener(OnClickStart);
}

private void OnClickStart()
{
FallTarget(FirstTarget.transform, FirstFallTime).Forget();
FallTarget(SecondTarget.transform, SecondFallTime).Forget();
}

private async UniTaskVoid FallTarget(Transform targetTrans, float fallTime)
{
float startTime = Time.time;

Vector3 startPosition = targetTrans.position;
while (Time.time - startTime <= fallTime)
{
float elapsedTime = Mathf.Min(Time.time - startTime, fallTime);
float fallY = 0 + 0.5f * G * elapsedTime * elapsedTime;
targetTrans.position = startPosition + Vector3.down * fallY;
await UniTask.Yield(this.GetCancellationTokenOnDestroy());
}
}
image

精细控制回调

1
2
3
4
private void Start()
{
CallbackButton.onClick.AddListener(UniTask.UnityAction(OnClickCallback));
}

使用UniTaskCompletionSource能够对UniTask执行时的过程做到精细化控制

  • source执行了TryGetResult,那么source就完成了
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
private async UniTaskVoid OnClickCallback()
{
float time = Time.time;
UniTaskCompletionSource source = new UniTaskCompletionSource();
FallTarget(Target.transform, FallTime, OnTargetHalf, source).Forget();
await source.Task;// UniTaskCompletionSource产生的UnitTask是可以复用的
Debug.Log($"当前缩放{Target.transform.localScale} 耗时 {Time.time - time}秒");
}

private void OnTargetHalf()
{
Target.transform.localScale *= 1.5f;
}

private async UniTask FallTarget(Transform targetTrans, float fallTime, System.Action onHalf, UniTaskCompletionSource source)
{
float startTime = Time.time;

Vector3 startPosition = targetTrans.position;
float lastElapsedTime = 0;
while (Time.time - startTime <= fallTime)
{
float elapsedTime = Mathf.Min(Time.time - startTime, fallTime);
if (lastElapsedTime < fallTime * 0.5f && elapsedTime >= fallTime * 0.5f)
{
onHalf?.Invoke();
source.TrySetResult();
// 失败
// source.TrySetException(new SystemException());
// 取消
// source.TrySetCanceled(someToken);

// 泛型类UniTaskCompletionSource<T> SetResult是T类型,返回UniTask<T>
}

lastElapsedTime = elapsedTime;
float fallY = 0 + 0.5f * G * elapsedTime * elapsedTime;
targetTrans.position = startPosition + Vector3.down * fallY;
await UniTask.Yield(this.GetCancellationTokenOnDestroy());
}
}

3.异步切换线程

1
2
3
4
5
private void Start()
{
StandardRun.onClick.AddListener(UniTask.UnityAction(OnClickStandardRun));
YieldRun.onClick.AddListener(UniTask.UnityAction(OnClickYieldRun));
}
image

一旦调用UniTask.Yield后,执行线程就会返回到主线程中(个人还是偏向使用 UniTask.SwitchToMainThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async UniTaskVoid OnClickStandardRun()
{
int result = 0;
await UniTask.RunOnThreadPool(() => { result = 1; });
await UniTask.SwitchToMainThread();
Text.text = $"计算结束,当前结果是{result}";
}

private async UniTaskVoid OnClickYieldRun()
{
string fileName = Application.dataPath + "/UniTaskTutorial/BaseUsingNext/test.txt";
await UniTask.SwitchToThreadPool();
string fileContent = await File.ReadAllTextAsync(fileName);
await UniTask.Yield(PlayerLoopTiming.Update);
Text.text = fileContent;
}

P5 进阶用法

image

1.UI事件与UnitaskAsyncEnumerable

演示:https://www.bilibili.com/video/BV1kd4y1U7Yg?t=153.1

1
2
3
4
5
6
void Start()
{
CheckSphereClick(SphereButton.GetCancellationTokenOnDestroy()).Forget();
CheckDoubleClickButton(DoubleClickButton, this.GetCancellationTokenOnDestroy()).Forget();
CheckCooldownClickButton(this.GetCancellationTokenOnDestroy()).Forget();
}
image

Sphere三次点击

使用异步可迭代器,但每次迭代是同步调用

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
private async UniTaskVoid CheckSphereClick(CancellationToken token)
{
var asyncEnumerable = SphereButton.OnClickAsAsyncEnumerable(); //获取按钮的异步可迭代器
// 等待前3次点击
await asyncEnumerable.Take(3).ForEachAsync((_, index) =>
{
if (token.IsCancellationRequested) return;
if (index == 0)
{
SphereTweenScale(2, SphereButton.transform.localScale.x, 20, token).Forget();
}
else if (index == 1)
{
SphereTweenScale(2, SphereButton.transform.localScale.x, 10, token).Forget();
}
}, token);
GameObject.Destroy(SphereButton.gameObject);
}

//缩放
private async UniTaskVoid SphereTweenScale(float totalTime, float from, float to, CancellationToken token)
{
var trans = SphereButton.transform;
float time = 0;
while (time < totalTime)
{
time += Time.deltaTime;
trans.localScale = (from + (time / totalTime) * (to - from)) * Vector3.one;
await UniTask.Yield(PlayerLoopTiming.Update, token);
}
}

双击按钮

WhenAny判断双击和超时哪个先来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async UniTaskVoid CheckDoubleClickButton(Button button, CancellationToken token)
{
while (true)
{
var clickAsync = button.OnClickAsync(token);
await clickAsync;
DoubleEventText.text = $"按钮被第一次点击";
var secondClickAsync = button.OnClickAsync(token);
int resultIndex = await UniTask.WhenAny(secondClickAsync, UniTask.Delay(TimeSpan.FromSeconds(DoubleClickCheckTime), cancellationToken: token));
if (resultIndex == 0)
{
DoubleEventText.text = $"按钮被双击了";
}
else
{
DoubleEventText.text = $"超时,按钮算单次点击";
}
}
}

冷却按钮

C#8.0:lambda支持async(2020.3之后)

使用异步可迭代器,但每次迭代是异步调用

使用asyncEnumerable.Queue可以对请求进行排队,否则在await过程中不接收任何请求

1
2
3
4
5
6
7
8
9
10
11
12
private int clickCount = 1;
private async UniTaskVoid CheckCooldownClickButton(CancellationToken token)
{
var asyncEnumerable = CoolDownButton.OnClickAsAsyncEnumerable();
// await asyncEnumerable.Queue().ForEachAwaitAsync(async (_) =>
await asyncEnumerable.ForEachAwaitAsync(async (_) =>
{
CoolDownEventText.text = "被点击了,冷却中……" + clickCount++;
await UniTask.Delay(TimeSpan.FromSeconds(CooldownTime), cancellationToken: token);
CoolDownEventText.text = "冷却好了,可以点了……";
}, cancellationToken: token);
}

2.AsyncReactiveProperty

演示:https://www.bilibili.com/video/BV1kd4y1U7Yg?t=818.4

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
private void Start()
{
// 设置AsyncReactiveProperty
currentHp = new AsyncReactiveProperty<int>(maxHp);
HpSlider.maxValue = maxHp;
HpSlider.value = maxHp;

currentHp.Subscribe(OnHpChange); //每次血量变化,触发OnHpChange函数
CheckHpChange(currentHp).Forget();
CheckFirstLowHp(currentHp).Forget();

currentHp.BindTo(ShowHpText);

HealButton.onClick.AddListener(OnClickHeal);
HurtButton.onClick.AddListener(OnClickHurt);

_linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, this.GetCancellationTokenOnDestroy());
}

private void OnClickHeal()
{
ChangeHp(Random.Range(0, maxHeal));
}

private void OnClickHurt()
{
ChangeHp(-Random.Range(0, maxHurt));
}

private void ChangeHp(int deltaHp)
{
currentHp.Value = Mathf.Clamp(currentHp.Value + deltaHp, 0, maxHp);
}
image

Subscribe

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
private async UniTaskVoid OnHpChange(int hp)
{
if (_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, this.GetCancellationTokenOnDestroy());
}
await SyncSlider(hp, _linkedTokenSource.Token);
}

private async UniTask SyncSlider(int hp, CancellationToken token)
{
var sliderValue = HpSlider.value;
float needTime = Mathf.Abs((sliderValue - hp) / maxHp * totalChangeTime);
float useTime = 0;
while (useTime < needTime)
{
useTime += Time.deltaTime;
bool result = await UniTask.Yield(PlayerLoopTiming.Update, token)
.SuppressCancellationThrow();
if (result)
{
return;
}

var newValue = (sliderValue + (hp - sliderValue) * (useTime / needTime));
SetNewValue(newValue);
}
}

private void SetNewValue(float newValue)
{
if (!HpSlider) return;
HpSlider.value = newValue;
HpBarImage.color = HpSlider.value / maxHp < 0.4f ? Color.red : Color.white;
}

使用异步可迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private async UniTaskVoid CheckHpChange(AsyncReactiveProperty<int> hp)
{
int hpValue = hp.Value;
await hp.WithoutCurrent().ForEachAsync((_, index) =>
{
ChangeText.text = $"血量发生变化 第{index}次 变化{hp.Value - hpValue}";
hpValue = hp.Value;
}, this.GetCancellationTokenOnDestroy());
}

private async UniTaskVoid CheckFirstLowHp(AsyncReactiveProperty<int> hp)
{
await hp.FirstAsync((value) => value < maxHp * 0.4f, this.GetCancellationTokenOnDestroy());
StateText.text = "首次血量低于界限,请注意!";
}

绑定组件

1
currentHp.BindTo(ShowHpText);

3.自然逻辑异步流

演示:https://www.bilibili.com/video/BV1kd4y1U7Yg?t=1174.2

PlayerControlSample

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
using System;
using UnityEngine;
using UnityEngine.Events;

namespace UniTaskTutorial.Advance.Scripts
{
public class PlayerControlSample : MonoBehaviour
{
[Header("玩家")]
[SerializeField]
private Transform playerRoot;

[Header("控制参数")]
[SerializeField]
private ControlParams controlParams;


[SerializeField] private UnityEvent onFireEvent;

private void Start()
{
PlayerControl playerControl = new PlayerControl(playerRoot, controlParams);
playerControl.OnFire = onFireEvent;
playerControl.Start();
}
}
}

PlayerControl

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using System;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
using UnityEngine.Events;

namespace UniTaskTutorial.Advance.Scripts
{
[Serializable]
public struct ControlParams
{
[Header("旋转速度")] public float rotateSpeed;
[Header("移动速度")] public float moveSpeed;
[Header("开枪最小间隔")] public float fireInterval;
}

public class PlayerControl
{
private Transform _playerRoot;
private ControlParams _controlParams;

public UnityEvent OnFire;

private float _lastFireTime;

public PlayerControl(Transform playerRoot, ControlParams controlParams)
{
_playerRoot = playerRoot;
_controlParams = controlParams;
}

// 启动输入检测
private void StartCheckInput()
{
CheckPlayerInput().ForEachAsync((delta) =>
{
_playerRoot.position += delta.Item1;
_playerRoot.forward = Quaternion.AngleAxis(delta.Item2, Vector3.up) * _playerRoot.forward;
if (delta.Item3 - _lastFireTime > _controlParams.fireInterval)
{
OnFire?.Invoke();
_lastFireTime = delta.Item3;
}
},
_playerRoot.GetCancellationTokenOnDestroy()).Forget();
}

private IUniTaskAsyncEnumerable<(Vector3, float, float)> CheckPlayerInput()
{
return UniTaskAsyncEnumerable.Create<(Vector3, float, float)>(async (writer, token) =>
{
await UniTask.Yield();
while (!token.IsCancellationRequested)
{
await writer.YieldAsync((GetInputMoveValue(), GetInputAxisValue(), GetIfFired()));
await UniTask.Yield();
}
});
}

private float GetIfFired()
{
if (Input.GetMouseButtonUp(0))
{
return Time.time;
}

return -1;
}

private Vector3 GetInputMoveValue()
{
var horizontal = Input.GetAxis("Horizontal");
var vertical = Input.GetAxis("Vertical");
Vector3 move = (_playerRoot.forward * vertical + _playerRoot.right * horizontal) *
(_controlParams.moveSpeed * Time.deltaTime);
return move;
}

private float GetInputAxisValue()
{
if (!Input.GetMouseButton(1)) return default;
var result = Input.GetAxis("Mouse X") * _controlParams.rotateSpeed;
return Mathf.Clamp(result, -90, 90);
}

public void Start()
{
StartCheckInput();
}
}
}

FireBulletSample

子弹发射碰撞销毁逻辑,全部整合到OnClickFire中

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using System;
using System.Collections;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using Cysharp.Threading.Tasks.Triggers;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;


[Serializable]
public struct ScoreCollider
{
public Collider Collider;
public float Score;
}

public class FireBulletSample : MonoBehaviour
{
public Transform FirePoint;

[SerializeField]
private GameObject bulletTemplate;

[Header("射速")]
[SerializeField]
private float flySpeed;

[Header("自动回收时间")]
[SerializeField]
private float bulletAutoDestroyTime;

[Header("分数显示文本")]
[SerializeField]
private Text currentScoreText;

[Header("分数配置")]
[SerializeField]
private ScoreCollider[] scoreColliders;

[Header("命中效果")]
[SerializeField]
private GameObject hitEffect;

private float totalScore = 0;


// Start is called before the first frame update
void Start()
{
CheckScoreChange().Forget();
}

public void Fire()
{
(UniTask.UnityAction(OnClickFire)).Invoke();
}

async UniTaskVoid CheckScoreChange()
{
while (true)
{
await UniTask.WaitUntilValueChanged(this, (target)=> target.totalScore);
currentScoreText.text = $"总分:{totalScore}";
}
}

private async UniTaskVoid OnClickFire()
{
var bullet = Object.Instantiate(bulletTemplate);
bullet.transform.position = FirePoint.position;
bullet.transform.forward = FirePoint.forward;

// 先飞出去
var bulletToken = bullet.transform.GetCancellationTokenOnDestroy();
FlyBullet(bullet.transform, flySpeed).Forget();
// 等待时间到,或者碰到了任意物体。获取子弹本身的token来当作取消token

var waitAutoDestroy = UniTask.Delay(TimeSpan.FromSeconds(bulletAutoDestroyTime), cancellationToken: bulletToken);

var source = new UniTaskCompletionSource<Collision>();
// 注意可以使用where take(1)或FirstAsync来简化操作
bullet.transform.GetAsyncCollisionEnterTrigger().ForEachAsync((collision) =>
{
if (collision.collider.CompareTag("Target"))
{
source.TrySetResult(collision);
}
}, cancellationToken: bulletToken);
int result = await UniTask.WhenAny(waitAutoDestroy, source.Task);
if (result == 0)
{
}
else if (result == 1)
{
var collision = source.GetResult(0);
Collider getCollider = collision.collider;
var go = Object.Instantiate(hitEffect, bullet.transform.position, Quaternion.identity);
Object.Destroy(go, 4f);
foreach (ScoreCollider scoreCollider in scoreColliders)
{
if (getCollider == scoreCollider.Collider)
{
totalScore += scoreCollider.Score;
}
}
}
Object.Destroy(bullet);
}

private async UniTaskVoid FlyBullet(Transform bulletTransform, float speed)
{
float startTime = Time.time;
Vector3 startPosition = bulletTransform.position;
while (true)
{
await UniTask.Yield(PlayerLoopTiming.Update, bulletTransform.GetCancellationTokenOnDestroy());
bulletTransform.position = startPosition + (speed * (Time.time - startTime)) * bulletTransform.forward;
}
}

// Update is called once per frame
void Update()
{

}
}

 评论