文章目录
- 前言
- 一、Unitask插件Github路径
- 二、基本使用方法记录
- 1.文本异步加载
- Mono托管
- 非Mono托管
- 2.加载场景的运用
- 3.请求下载图片并且切换成Sprite动画
- 4.UniTask.Delay
- I.简单的按秒数延时时间
- II.简单按帧数延时时间
- 5.UniTask.NextFrame\WaitForEndOfFrame\Yield
- 6.Unitask WhenAll和WhenAny使用方法
- 7.UniTask取消
- 二、UniTask扩展
- 1.网络请求超时操作
- 2.小球掉落案例
- I.同步方法使用异步方法的方案(Forget方法使用)
- II.UniTask回调添加(手动完成UniTask的任务)
- 3.异步切换线程
- 三、UniTask进阶提升(编程之路提升)
- 1.Unity特有事件转换为Unitask,异步可迭代器
- I.点击按钮,第一次变大,第二次变小,第三次消失
- II.按钮双击点击,进行计时,如果超过规定时间超时。
- III.按钮冷却
- 2.AsyncReactiveProperty的使用
- 总结
前言
由于最近编写一下unity的游戏框架,使用了unity自带的协程去做一些异步操作,发现有很多限制,也需要提升下代码质量,所以为稍微学习一下UniTask的使用。UniTask插件使用了懒加载的方式实现,在第一次运行一些异步操作的时候,效率会稍微比协程慢,但是之后性能消耗会是协程消耗的十分之一上下。
一、Unitask插件Github路径
https://github.com/Cysharp/UniTask?tab=readme-ov-file#install-via-git-url
使用之前需要在对应的CS文件上面引用命名空间using Cysharp.Threading.Tasks不然会没办法扩展C# await/aysnc的功能
二、基本使用方法记录
1.文本异步加载
代码如下(示例):
Mono托管
public class UniTaskBaseTest : MonoBehaviour
{private Text textTest;private async void LoadTextTest(){var loadOperation = Resources.LoadAsync<TextAsset>("test");var text = await loadOperation;textTest.text = ((TextAsset)text).text;}
}
非Mono托管
public class UniTaskBaseTest : MonoBehaviour
{private Text textTest;private async void LoadTextTest(){UniTaskBaseTest01 uniTaskBaseTest01 = new UniTaskBaseTest01();var textAsset = await uniTaskBaseTest01.LoadAsync<TextAsset>("TextAsset");textTest.text = ((TextAsset)textAsset).text;}
}public class UniTaskBaseTest01
{public async UniTask<Object> LoadAsync<T>(string path){var loadOperation = Resources.LoadAsync<Object>(path);return await loadOperation;}
}
2.加载场景的运用
代码如下(示例):
public class UniTaskBaseTest : MonoBehaviour
{private async void LoadSceneAsync(){await SceneManager.LoadSceneAsync("Scene/Map01").ToUniTask((Progress.Create<float>((p) =>{Debug.Log(p * 100);})));}
}
3.请求下载图片并且切换成Sprite动画
public class UniTaskBaseTest : MonoBehaviour
{private async void WebTextureDownload(){try{var webRequest = UnityWebRequestTexture.GetTexture("https://i0.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 perFrame = 0.1f;while (true){for (var i = 0; i < totalSpriteCount; i++){await UniTask.Delay(TimeSpan.FromSeconds(perFrame));var sprite = sprites[i];// todo ..}}}catch (Exception e){throw; // TODO handle exception}}
}
4.UniTask.Delay
I.简单的按秒数延时时间
public class UniTaskBaseTest : MonoBehaviour
{public async void Start(){Debug.Log($"执行Delay前时间{Time.time}");await UniTask.Delay(TimeSpan.FromSeconds(2));Debug.Log($"执行Delay后时间{Time.time}");}
}
II.简单按帧数延时时间
public class UniTaskBaseTest : MonoBehaviour
{public async void Start(){Debug.Log($"执行Delay前时间{Time.frameCount}");await UniTask.DelayFrame(5);Debug.Log($"执行Delay后时间{Time.frameCount}");}
}
5.UniTask.NextFrame\WaitForEndOfFrame\Yield
写入注入代码的案例,测试下这些函数的执行时机
public class UniTaskBaseTest : MonoBehaviour
{public bool showUpdateLog = false;public List<PlayerLoopSystem.UpdateFunction> injectUpdateFunctions = new List<PlayerLoopSystem.UpdateFunction>();private UniTaskAsyncSmaple_Wait uniTaskAsyncWaiter = new UniTaskAsyncSmaple_Wait();public PlayerLoopTiming playerLoopTiming;private void InjectFunction(){PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop();var subSystem = playerLoop.subSystemList;playerLoop.updateDelegate += OnUpdate;for (int i = 0; i < subSystem.Length; i++){int index = i;PlayerLoopSystem.UpdateFunction injectFunction = () =>{if (!showUpdateLog){return;}Debug.Log($"执行子系统{showUpdateLog} {subSystem} 当前帧数 {Time.frameCount}");};injectUpdateFunctions.Add(injectFunction);subSystem[index].updateDelegate += injectFunction;}PlayerLoop.SetPlayerLoop(playerLoop);}private async void TestNextFrame(){showUpdateLog = true;Debug.Log("执行NextFrame开始");await uniTaskAsyncWaiter.WaitNextFrame();Debug.Log("执行NextFrame开始");showUpdateLog = false;}private async void TestEndOfFrame(){showUpdateLog = true;Debug.Log("执行EndOfFrame开始");await uniTaskAsyncWaiter.WaitEndOfFrame();Debug.Log("执行EndOfFrame开始");showUpdateLog = false;}private async void TestYield(){showUpdateLog = true;Debug.Log("执行Yield开始");await uniTaskAsyncWaiter.WaitYield(playerLoopTiming);Debug.Log("执行Yield开始");showUpdateLog = false;}private void OnUpdate(){}
}public class UniTaskAsyncSmaple_Wait
{public async UniTask<int> WaitYield(PlayerLoopTiming loopTiming){await UniTask.Yield(loopTiming);return 0;}public async UniTask<int> WaitNextFrame(){await UniTask.NextFrame();return 0;}public async UniTask<int> WaitEndOfFrame(){await UniTask.WaitForEndOfFrame();return 0;}
}
测试打印
经过测试得知,NextFrame会等待到下一帧Update时机结束之后,而EndOfFrame会到下一帧初始化(Initialization)之前,Yield可以自行更改时机
6.Unitask WhenAll和WhenAny使用方法
假设有个监测1为check1,监测2为check2,则下面就是等待如下代码check1和check2都完成的时机的代码
public class UniTaskBaseTest : MonoBehaviour
{private async void WhenAllTest(){var check1 = UniTask.WaitUntil(() => true);var check2 = UniTask.WaitUntil(() => true);await UniTask.WhenAll(check1,check2); Debug.Log("条件完成");}
}
当只需要其中一个条件完成的情况下就可以的情况下,直接使用WhenAny方法就可以了
public class UniTaskBaseTest : MonoBehaviour
{private bool c1 = true;private bool c2 = true;private async void WhenAllTest(){var check1 = UniTask.WaitUntil(() => c1);var check2 = UniTask.WaitUntil(() => c2);await UniTask.WhenAny(check1,check2); Debug.Log($"任意一个完成就行c1:{c1},c2:{c2}");}
}
7.UniTask取消
UniTask给我们设计了一种非常好用的取消方式,使用对应的token进行取消。
public class UniTaskBaseTest : MonoBehaviour
{private CancellationTokenSource cts1 = new CancellationTokenSource(); public async void Task1(){try{await TestTask1(cts1.Token);}catch (OperationCanceledException e){throw;}}private async UniTask TestTask1(CancellationToken token){while (true){await UniTask.NextFrame(token);} } private void CtsCancel(){cts1.Cancel();cts1.Dispose();}
}
这里通过捕获异常的时候进行取消的操作,当然这样会有些性能的消耗。这样可以通过SuppressCancellationThrow的方式来取消掉异常捕获即可
public class UniTaskBaseTest : MonoBehaviour
{private CancellationTokenSource cts2 = new CancellationTokenSource(); public async void Task2(){var (cancelled,_) = await TestTask2(cts2.Token).SuppressCancellationThrow();if (cancelled){//todo ..}}private async UniTask<int> TestTask2(CancellationToken token){while (true){await UniTask.NextFrame(token);} }private void CtsCancel(){cts2.Cancel();cts2.Dispose();}
}
使用token结束后记得手动调用Dipose~。
二、UniTask扩展
1.网络请求超时操作
使用UniTask来处理一些网络超时问题,设置一个期望时间,如果超过这个期望时间就使用token取消操作
public class TimeOutTest : MonoBehaviour
{public string SearchWorld = "Unity";public string[] SerachURLs = new string[]{"https://www.baidu.com/s?wd=","https://www.bing.com/search?q=","https://www.google.com/search?wd=",};private Button TestButton;private void Start(){TestButton = GameObject.Find("TestButton").GetComponent<Button>();TestButton.onClick.AddListener(UniTask.UnityAction(OnClickTest));}private async UniTask<string> GetRequest(string url,float timeout){var cts = new CancellationTokenSource();cts.CancelAfter(TimeSpan.FromSeconds(timeout));var (cancelOrFailed, result) = await UnityWebRequest.Get(url).SendWebRequest().WithCancellation(cts.Token).SuppressCancellationThrow();if (!cancelOrFailed){return result.downloadHandler.text;}return "取消或超时操作";}private async UniTaskVoid OnClickTest(){UniTask<string>[] awaitTasks = new UniTask<string>[SerachURLs.Length];for (int i = 0; i < SerachURLs.Length; i++) {awaitTasks[i] = GetRequest(SerachURLs[i],2f);}var tasks = await UniTask.WhenAll(awaitTasks);for (int i = 0; i < awaitTasks.Length; i++) {Debug.Log(tasks[i]);}}
2.小球掉落案例
I.同步方法使用异步方法的方案(Forget方法使用)
代码和场景如下
public class UniTaskTest : MonoBehaviour
{public float G = 9.8f;public Transform prefab1;public Transform prefab2;public float FallTime = 2f;public Button StartButton;private void Start(){StartButton.onClick.AddListener(OnClickStart);}private void OnClickStart(){FallTarget(prefab1,FallTime).Forget();FallTarget(prefab2,FallTime).Forget();}private async UniTaskVoid FallTarget(Transform targetTransform, float fallTime){float startTime = Time.time;Vector3 startPos = targetTransform.position;while (Time.time - startTime < fallTime){float elapsedTime = Mathf.Min(Time.time - startTime, fallTime);float fallY = 0 + 0.5f * G * elapsedTime * elapsedTime;targetTransform.position = Vector3.Lerp(startPos, startPos + Vector3.down * fallY, elapsedTime);await UniTask.Yield(this.GetCancellationTokenOnDestroy());}}
}
点击开始掉落按钮,两个小球会按照自由落体公式进行掉落。在OnClickStart方法里面使用了Forget方法在同步方法进行异步调用。
II.UniTask回调添加(手动完成UniTask的任务)
这里稍微修改一下上面的代码,代码设计成,当小球掉落到一半的时间的时候,缩放变成原来的1.5倍。这里使用UniTaskCompletionSource进行回调设置,同时OnClickStart修改成异步方法,并且实现下回调方法OnHalf
public class UniTaskTest : MonoBehaviour
{public float G = 9.8f;public Transform prefab1; public float FallTime = 1f;public Button StartButton;private void Start(){StartButton.onClick.AddListener(UniTask.UnityAction(OnClickStart));}private async UniTaskVoid OnClickStart(){UniTaskCompletionSource source = new UniTaskCompletionSource();FallTarget(prefab1,FallTime,OnHalf,source).Forget();await source.Task;}private void OnHalf(){prefab1.localScale *= 1.5f;}private async UniTaskVoid FallTarget(Transform targetTransform, float fallTime,System.Action onHalf,UniTaskCompletionSource source){float startTime = Time.time;Vector3 startPos = targetTransform.position;float lastElapsedTime = 0.0f;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 System.Exception()); //手动失败//source.TrySetCanceled(); //手动取消}float fallY = 0 + 0.5f * G * elapsedTime * elapsedTime;targetTransform.position = Vector3.Lerp(startPos, startPos + Vector3.down * fallY, elapsedTime);lastElapsedTime = elapsedTime;await UniTask.Yield(this.GetCancellationTokenOnDestroy());}}
}
3.异步切换线程
当我们需要使用其他线程来完成一些Action的操作的时候,我们可以如下进行代码编写。
public class UniTaskTest : MonoBehaviour
{private async UniTaskVoid StandardStart(){int result = 0;await UniTask.RunOnThreadPool(() => { result = 1; });await UniTask.SwitchToMainThread();Debug.Log(result);}
}
上面代码我们使用了SwitchToMainThread手动切换回主线程。那么我们就可以手动使用SwitchToThreadPool来切换至其他线程来执行下面的任务,然后使用UniTask.yield来直接切换回来主线程。这里编写一个文本读取的例子,代码如下
private async UniTaskVoid YieldSwitchThreadTest(){string fileName = Application.dataPath + "test.txt";await UniTask.SwitchToThreadPool();string fileContent = await File.ReadAllTextAsync(fileName);await UniTask.Yield(PlayerLoopTiming.Update);Debug.Log(fileContent);}
三、UniTask进阶提升(编程之路提升)
1.Unity特有事件转换为Unitask,异步可迭代器
I.点击按钮,第一次变大,第二次变小,第三次消失
使用一个异步可迭代器实现
public class UniTaskTest : MonoBehaviour
{public Button sphereButton; private void Start(){CheckSphereClick(sphereButton.GetCancellationTokenOnDestroy()).Forget();}private async UniTaskVoid CheckSphereClick(CancellationToken token){var asyncEnumerable = sphereButton.OnClickAsAsyncEnumerable();await asyncEnumerable.Take(3).ForEachAsync((_, index) =>{if (token.IsCancellationRequested){return;}if (index == 0){SphereTweenScale(1,sphereButton.transform.localScale.x, 4,token).Forget();}if (index == 1){SphereTweenScale(1,sphereButton.transform.localScale.x, 2,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);}}
}
II.按钮双击点击,进行计时,如果超过规定时间超时。
public class UniTaskTest : MonoBehaviour
{public Button button; public Text text;private void Start(){CheckDoubleClickButton(button,button.GetCancellationTokenOnDestroy()).Forget();}private async UniTaskVoid CheckDoubleClickButton(Button button, CancellationToken token){while (true){var clickAsync = button.OnClickAsync(token);await clickAsync;text.text = "按钮点击了第一次";var secondClickAsync = button.OnClickAsync(token);int resultIndex = await UniTask.WhenAny( secondClickAsync,UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token));if (resultIndex == 0){text.text = "按钮点击了第二次";}else{text.text = "按钮点击超时";}}}
}
III.按钮冷却
举一反三我们也能推出按钮冷却如何进行编写,不过这里需要使用可迭代器的ForEachAwaitAsync,这样编写的话,我们的代码都变得非常精简,不需要添加额外的字段来保存状态。
public class UniTaskTest : MonoBehaviour
{public Button button; public Text text;private void Start(){CheckCoolClickButton(button,button.GetCancellationTokenOnDestroy()).Forget();}private async UniTaskVoid CheckCoolClickButton(Button button, CancellationToken token){var asyncEnumerable = button.OnClickAsAsyncEnumerable();await asyncEnumerable.ForEachAwaitAsync(async (_) =>{text.text = "正在进行冷却";await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token);text.text = "冷却完毕";},token);}
}
2.AsyncReactiveProperty的使用
使用这个AsyncReactiveProperty用到基础类型上面,可以将每次基础类型的变化做成异步流,这样可以大大增加扩展性。
比如实现一个血条变化的功能
代码如下
using System;
using System.Threading;
using UnityEngine;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine.UI;
using UnityEngine.Windows;
using File = System.IO.File;
using Random = UnityEngine.Random;public class AyyncReactivePropertySample : MonoBehaviour
{private AsyncReactiveProperty<int> currentHp;public int maxHp = 100;public float totalChangeTime = 1.0f;public Text ShowHpText;public Text StateText;public Text ChangeText;public Slider HpSlider;public Image HpBarImage;public Button HealButton;public Button HurtButton;private int maxHeal = 10;private int maxHurt = 10;private CancellationTokenSource cts = new CancellationTokenSource();private CancellationTokenSource linkCts;private void Start(){//设置AsyncReactivePropertycurrentHp = 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(OnClickHealButton);HurtButton.onClick.AddListener(OnClickHurtButton);}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 void OnClickHealButton(){ChangeHp(-Random.Range(0,maxHeal));}private void OnClickHurtButton(){ChangeHp(-Random.Range(0,maxHurt));}private void ChangeHp(int delta){currentHp.Value = Mathf.Clamp(currentHp.Value + delta, 0, maxHp);}private async UniTaskVoid CheckFirstLowHp(AsyncReactiveProperty<int> hp){await hp.FirstAsync((value) => value < maxHp * 0.4f, this.GetCancellationTokenOnDestroy());StateText.text = "首次血条低于界限,请注意!";}private async UniTaskVoid OnHpChange(int hp){cts.Cancel();cts = new CancellationTokenSource();linkCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, this.GetCancellationTokenOnDestroy());await SyncSlider(hp,cts.Token);}private async UniTask SyncSlider(int hp,CancellationToken token){var sliderValue = HpSlider.value;float needTime = Mathf.Abs(sliderValue - hp) / maxHp * totalChangeTime;float useTime = 0.0f;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 = newValue / maxHp < 0.4f ? Color.red : Color.white;}
}
再熟悉一下下UniTask的可迭代器方面的运用,简单实现一下一个玩家控制器,使用这些代码的好处是,介绍其他字段增加代码的可读性和扩展性,代码如下
public struct ControlParam
{[Header("旋转速度")] public float rotateSpeed;[Header("移动速度")] public float moveSpeed; [Header("摄像机")] public float cameraDistance;
}public class PlayerControl
{public Transform playerRoot;private ControlParam controlParams;public float lastFireTime;public Transform cameraTransform;public PlayerControl(Transform playerRoot, ControlParam controlParams,Transform cameraTrans){this.playerRoot = playerRoot;this.controlParams = controlParams;this.cameraTransform = cameraTrans;}private void StartCheckInput(){CheckPlayerInput().ForEachAsync((delta) =>{playerRoot.position += delta.Item1;var cameraToPlayer = (playerRoot.forward - cameraTransform.forward).normalized;cameraTransform.forward = cameraToPlayer;cameraTransform.position = playerRoot.position - cameraToPlayer * controlParams.cameraDistance;playerRoot.forward = Quaternion.AngleAxis(delta.Item2, Vector3.up) * playerRoot.forward; },playerRoot.GetCancellationTokenOnDestroy()).Forget();} 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 IUniTaskAsyncEnumerable<(Vector3, float)> CheckPlayerInput(){return UniTaskAsyncEnumerable.Create<(Vector3, float)>(async (writer, token) =>{await UniTask.Yield();while (!token.IsCancellationRequested){await writer.YieldAsync((GetInputMoveValue(), GetInputAxisValue()));await UniTask.Yield();}});}private float GetInputAxisValue(){ var result = Input.GetAxis("Mouse X") * controlParams.rotateSpeed;return Mathf.Clamp(result, -90, 90);}public void Start(){StartCheckInput();}
}
总结
简单贴下await在Unitask的扩展源码
public struct ResourceRequestAwaiter : ICriticalNotifyCompletion{ResourceRequest asyncOperation;Action<AsyncOperation> continuationAction;public ResourceRequestAwaiter(ResourceRequest asyncOperation){this.asyncOperation = asyncOperation;this.continuationAction = null;}public bool IsCompleted => asyncOperation.isDone;public UnityEngine.Object GetResult(){if (continuationAction != null){asyncOperation.completed -= continuationAction;continuationAction = null;var result = asyncOperation.asset;asyncOperation = null;return result;}else{var result = asyncOperation.asset;asyncOperation = null;return result;}}public void OnCompleted(Action continuation){UnsafeOnCompleted(continuation);}public void UnsafeOnCompleted(Action continuation){Error.ThrowWhenContinuationIsAlreadyRegistered(continuationAction);continuationAction = PooledDelegate<AsyncOperation>.Create(continuation);asyncOperation.completed += continuationAction;}}
Unitask可以很好的解决Unity C#大部分的异步写法的问题,用同步的写法写出异步的性能,大量精简异步操作的代码,底层使用结构体0GC,性能效率也不Unity自带的协程高不少。