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 using Cysharp.Threading.Tasks;async UniTask<string > DemoAsync (){ var asset = await Resources.LoadAsync<TextAsset>("foo" ); var txt = (await UnityWebRequest.Get("https://..." ).SendWebRequest()).downloadHandler.text; await SceneManager.LoadSceneAsync("scene2" ); var asset2 = await Resources.LoadAsync<TextAsset>("bar" ).WithCancellation(this .GetCancellationTokenOnDestroy()); var asset3 = await Resources.LoadAsync<TextAsset>("baz" ).ToUniTask(Progress.Create<float >(x => Debug.Log(x))); await UniTask.DelayFrame(100 ); await UniTask.Delay(TimeSpan.FromSeconds(10 ), ignoreTimeScale: false ); await UniTask.Yield(PlayerLoopTiming.PreLateUpdate); await UniTask.Yield(); await UniTask.NextFrame(); await UniTask.WaitForEndOfFrame(this ); await UniTask.WaitForFixedUpdate(); await UniTask.WaitUntil(() => isActive == false ); await UniTask.WaitUntilValueChanged(this , x => x.isActive); await FooCoroutineEnumerator(); await Task.Run(() => 100 ); await UniTask.SwitchToThreadPool(); await UniTask.SwitchToMainThread(); 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" )); var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3); var (google2, bing2, yahoo2) = await (task1, task2, task3); return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found" ); }
UniTaskTracker 对于检查(泄露的)UniTasks 很有用。您可以在Window -> UniTask Tracker
中打开跟踪器窗口。
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 介绍 原生问题
UniTask的优点
安装UniTask https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
基础使用
进阶使用
P2 空载性能测试 发布版:UniTask比协程快8~10倍
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); }
异步加载文本 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
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 (){ 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)); 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); TestNextFrameButton.onClick.AddListener(OnClickTestNextFrame); TestEndOfFrameButton.onClick.AddListener(OnClickTestEndOfFrame); ClearButton.onClick.AddListener(OnClickClear); unitaskWaiter = new UniTaskAsyncSample_Wait(); InjectFunction(); }
延迟秒执行 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
等待至下帧执行 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 ); 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); SecondRunButton.onClick.AddListener(OnClickSecondRun); WhenAllButton.onClick.AddListener(OnClickWhenAll); WhenAnyButton.onClick.AddListener(OnClickWhenAny); ResetButton.onClick.AddListener(OnClickReset); }
测试函数 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); var secondRunnerReach = UniTask.WaitUntil(() => SecondRunner.ReachGoal); await UniTask.WhenAll(firstRunnerReach, secondRunnerReach); 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); SecondCancelButton.onClick.AddListener(OnClickSecondCancel); ResetButton.onClick.AddListener(OnClickReset); _firstCancelToken = new CancellationTokenSource(); _secondCancelToken = new CancellationTokenSource(); _linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token); } 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 ; }
法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)); }
UniTask解释 async void
是一个原生的 C# 任务系统,因此它不能在 UniTask 系统上运行。也最好不要使用它。async UniTaskVoid
是async UniTask
的轻量级版本,因为它没有等待完成并立即向UniTaskScheduler.UnobservedTaskException
报告错误. 如果您不需要等待(即发即弃),那么使用UniTaskVoid
会更好。不幸的是,要解除警告,您需要在尾部添加Forget()
.
要使用注册到事件的异步 lambda,请不要使用async void
. 相反,您可以使用UniTask.Action
或 UniTask.UnityAction
,两者都通过async UniTaskVoid
lambda 创建委托。
1 2 3 4 5 6 7 8 9 10 Action actEvent; UnityAction unityEvent; actEvent += async () => { }; unityEvent += async () => { }; actEvent += UniTask.Action(async () => { await UniTask.Yield(); }); unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });
测试:如果async()=>{ … } 如果内容部分不含await,则不会有异步操作。
上述例子举的很差,因为async lambda里面没有await任何内容,这个反例根本就没可比性
测试:如果async()=>{ … } 如果内容部分含有await,依旧会正常运行UniTask,只是会给出警告。因此实际这样书写没毛病,如果想把警告去了就老老实实使用UniTask.UnityAction吧
超时取消 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()); } }
精细控制回调 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; 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(); } 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)); }
一旦调用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 进阶用法
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(); }
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(); 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.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 (){ currentHp = new AsyncReactiveProperty<int >(maxHp); HpSlider.maxValue = maxHp; HpSlider.value = maxHp; currentHp.Subscribe(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); }
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 ; 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(); var waitAutoDestroy = UniTask.Delay(TimeSpan.FromSeconds(bulletAutoDestroyTime), cancellationToken: bulletToken); var source = new UniTaskCompletionSource<Collision>(); 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; } } void Update () { } }