前言
由于最近编写一下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()
{
//设置AsyncReactiveProperty
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(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自带的协程高不少。