内容来源:up主游戏石匠,仅作笔记,推荐关注该up主。

UniTask是Github上的开源库,为Unity提供一个高性能异步方案,可以代替协程实现异步操作,中文文档
优点:

  1. 不需要依赖于MonoBehaviour,性能比协程好
  2. 可以进行 try catch,取消操作
  3. 默认使用主线程,与Unity协同,而C#得Task是在另一个线程中运行
  4. 0GC

安装


通过Package Manager安装,输入https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask

导入后可能的报错

如果导入后报错提示 ‘ArrayPool’ does not contain a definition for ‘Shared’
那是因为项目中使用了tolua,而tolua附带了一个CString.dll的库,这个库自己定义了一个全局的ArrayPool,就会导致其他地方用的ArrayPool都指向了CString.dll中的ArrayPool,这与Unitask源码中的Cysharp.Threading.Tasks.Internal.ArrayPool冲突了。

反编译CString.dll可以看到其中定义的这个ArrayPool

把Unitask文件夹移动到 项目名/Packages 目录下,并修改源码,在报错的地方加上命名空间前缀

var pool = Cysharp.Threading.Tasks.Internal.ArrayPool<TSource>.Shared;

这样就可以解决报错,尽量不要修改 CString.dll,不然打包可能报错

异步加载资源

加载文本

using Cysharp.Threading.Tasks;
using UnityEngine;/// <summary>
/// 不需要继承自MonoBehaviour
/// </summary>
public class UniTaskLoadAsync
{/// <summary>/// 返回UniTask<Object>类型,这种类型事为Unity定制的,作为替代原生Task<T>的轻量级方案/// </summary>public async UniTask<Object> LoadAsync<T>(string path) where T : Object{var asyncOperation = Resources.LoadAsync<T>(path);//这个await会将ResourceRequest(class)封装到UniTask的ResourceRequestAwaiter(struct)中return await asyncOperation;}
}
public class UniTaskTest : MonoBehaviour
{/// <summary>/// 加载文本/// </summary>private async void LoadTextAsync(){UniTaskLoadAsync loader = new UniTaskLoadAsync();//Test是Resources目录下的文本文件var textObj = await loader.LoadAsync<TextAsset>("Test");string str = ((TextAsset)textObj).text;Debug.LogError(str);}
}

加载场景

/// <summary>
/// 加载场景过程中显示进度值
/// </summary>
private async void LoadSceneAsync()
{var progress = Progress.Create<float>(x =>{//这里可以修改界面上的进度条Debug.Log("进度值:" + x);});//ToUniTask创建一个进度相关的回调await SceneManager.LoadSceneAsync("Scenes/TestScene1").ToUniTask(progress);
}

加载网络图片

public Image Image;/// <summary>
/// 加载网络图片
/// </summary>
private async void LoadWebPictureAsync()
{var webRequest = UnityWebRequestTexture.GetTexture("https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png");var result = await webRequest.SendWebRequest();var texture = ((DownloadHandlerTexture)result.downloadHandler).texture;Sprite sprite = Sprite.Create(texture, new Rect(Vector2.zero, new Vector2(texture.width, texture.height)), new Vector2(0.5f, 0.5f));Image.sprite = sprite;Image.SetNativeSize();
}

Delay操作

public async void DelayTest()
{//性能最好,可以设置等待时机,PlayerLoopTiming 对应Unity中playerloop的更新时机await UniTask.Yield(PlayerLoopTiming.LastUpdate);//等待1秒,类似 yield return new WaitForSeconds(1),可以设置 ignoreTimeScaleawait UniTask.Delay(TimeSpan.FromSeconds(1), false);//执行在下一帧的update之后,类似 yield return null,和 UniTask.Yield() 效果一样await UniTask.NextFrame();//这一帧的最后,类似 yield return new WaitForEndOfFrame(),this是一个MonoBehaviourawait UniTask.WaitForEndOfFrame(this);//类似 yield return new WaitForFixedUpdate,和 await UniTask.Yield(PlayerLoopTiming.FixedUpdate)效果一样await UniTask.WaitForFixedUpdate();//延迟5帧await UniTask.DelayFrame(5);//类似 yield return new WaitUntil(() => count > 10),当count > 10时才执行后面逻辑await UniTask.WaitUntil(() => count > 10);
}

联动操作 WhenAll和WhenAny


WhenAll

WhenAny

using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;public class UniTaskWhen : MonoBehaviour
{public Button FirstButton;public Button SecondButton;public TextMeshProUGUI Text;private bool firstClick = false;private bool secondClick = false;private void Start(){//这里用两个按钮的点击模拟两种操作FirstButton.onClick.AddListener(OnClickFirst);SecondButton.onClick.AddListener(OnClickSecond);// WhenAllTest();WhenAnyTest();}private void OnClickFirst(){firstClick = true;}private void OnClickSecond(){secondClick = true;}/// <summary>/// 当两个按钮都点击了才执行后面操作/// </summary>private async void WhenAllTest(){var firstOperation = UniTask.WaitUntil(() => firstClick);var secondOperation = UniTask.WaitUntil(() => secondClick);await UniTask.WhenAll(firstOperation, secondOperation);// 注意,whenAll可以用于平行执行多个资源的读取,非常有用!// var (a, b, c) = await UniTask.WhenAll(//LoadAsSprite("foo"),//LoadAsSprite("bar"),//LoadAsSprite("baz"));Text.text = "两个按钮都点击了";}/// <summary>/// 当其中一个按钮点击了就执行后面操作/// </summary>private async void WhenAnyTest(){var firstOperation = UniTask.WaitUntil(() => firstClick);var secondOperation = UniTask.WaitUntil(() => secondClick);await UniTask.WhenAny(firstOperation, secondOperation);Text.text = firstClick ? "first按钮点击了" : "second按钮点击了";}
}

取消操作

public class UniTaskCancel : MonoBehaviour
{public Transform FirstTransform;public Transform SecondTransform;public Button FirstRunButton;public Button SecondRunButton;public Button FirstCancelButton;public Button SecondCancelButton;public TextMeshProUGUI Text;//做取消时需要创建这个对象private CancellationTokenSource _firstCancelToken;private CancellationTokenSource _secondCancelToken;private CancellationTokenSource _linkedCancelToken;private void Start(){FirstRunButton.onClick.AddListener(OnClickFirstMove);SecondRunButton.onClick.AddListener(OnClickSecondMove);FirstCancelButton.onClick.AddListener(OnClickFirstCancel);SecondCancelButton.onClick.AddListener(OnClickSecondCancel);_firstCancelToken = new CancellationTokenSource();// 注意这里可以直接先行设置多久以后取消// _firstCancelToken = new CancellationTokenSource(TimeSpan.FromSeconds(1.5f));_secondCancelToken = new CancellationTokenSource();//用两个token创建新的linkedCancelToken,当其中一个取消后,linkedCancelToken也会取消,_linkedCancelToken =CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token);}/// <summary>/// 移动first,使用try catch监听取消信号/// </summary>private async void OnClickFirstMove(){try{await MoveTransform(FirstTransform, _firstCancelToken.Token);}catch (OperationCanceledException e){//发出取消信号,这里会抛异常Text.text = "first已经被取消";}}/// <summary>/// 移动second,忽略异常的抛出,返回一个值元组,这种方式性能更好/// </summary>private async void OnClickSecondMove(){//第一个参数表示是否取消,第二个参数时await的返回值var (cancelled, _) = await MoveTransform(SecondTransform, _secondCancelToken.Token).SuppressCancellationThrow();// 使用LinkedToken,当first取消后,second也会取消// var (cancelled, _) = await MoveTransform(SecondTransform, _linkedCancelToken.Token).SuppressCancellationThrow();if (cancelled){Text.text = "second已经被取消";}}private async UniTask<int> MoveTransform(Transform tf, CancellationToken cancellationToken){float totalTime = 20;float timeElapsed = 0;while (timeElapsed <= totalTime){timeElapsed += Time.deltaTime;await UniTask.NextFrame(cancellationToken);tf.transform.localPosition += Vector3.right * Time.deltaTime * 100;}return 0;}/// <summary>/// 取消first移动,Token使用后就不能再次使用,得创建新的Token/// </summary>private void OnClickFirstCancel(){_firstCancelToken.Cancel();_firstCancelToken.Dispose();_firstCancelToken = new CancellationTokenSource();_linkedCancelToken =CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token);}private void OnClickSecondCancel(){_secondCancelToken.Cancel();_secondCancelToken.Dispose();_secondCancelToken = new CancellationTokenSource();_linkedCancelToken =CancellationTokenSource.CreateLinkedTokenSource(_firstCancelToken.Token, _secondCancelToken.Token);}private void OnDestroy(){_firstCancelToken.Dispose();_secondCancelToken.Dispose();_linkedCancelToken.Dispose();}
}

超时操作

public class TimeoutTest : MonoBehaviour
{public Button TestButton;private void Start(){//使用UniTask.UnityAction包装了OnClickTestTestButton.onClick.AddListener(UniTask.UnityAction(OnClickTest));}private async UniTaskVoid OnClickTest(){var res = await GetRequest("https://www.baidu.com/", 2f);Debug.LogError(res);}private async UniTask<string> GetRequest(string url, float timeout){//这个token会在timeout之后发出取消信号var cts = new CancellationTokenSource();cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout));var (failed, result) = await UnityWebRequest.Get(url).SendWebRequest().WithCancellation(cts.Token).SuppressCancellationThrow();if (!failed){//成功了返回网页内容的开头return result.downloadHandler.text.Substring(0, 100);}return "超时";}
}

Fire and Forget

public class ForgetSample : MonoBehaviour
{public Button StartButton;public GameObject Target;public const float G = 9.8f;private void Start(){StartButton.onClick.AddListener(OnClickStart);}/// <summary>/// 同步方法中调用异步方法/// </summary>private void OnClickStart(){//不需要等待时候就调用ForgetFallTarget(Target.transform).Forget();}/// <summary>/// 使目标掉落,async UniTaskVoid是async UniTask的轻量级版本/// </summary>private async UniTaskVoid FallTarget(Transform targetTrans){Vector3 startPosition = targetTrans.position;float fallTime = 20f;float elapsedTime = 0;while (elapsedTime <= fallTime){elapsedTime += Time.deltaTime;float fallY = 0.5f * G * elapsedTime * elapsedTime;targetTrans.position = startPosition + Vector3.down * fallY;//GetCancellationTokenOnDestroy 表示获取一个依赖对象生命周期的Cancel句柄,//当对象被销毁时,将会调用这个Cancel句柄,从而实现取消的功能await UniTask.Yield(this.GetCancellationTokenOnDestroy());}}
}

UniTask运行中执行回调

public class CallbackSample : MonoBehaviour
{public Button CallbackButton;public GameObject Target;public const float G = 9.8f;private void Start(){CallbackButton.onClick.AddListener(UniTask.UnityAction(OnClickCallback));}private async UniTaskVoid OnClickCallback(){float time = Time.time;UniTaskCompletionSource source = new UniTaskCompletionSource();FallTarget(Target.transform, source).Forget();await source.Task;// UniTaskCompletionSource产生的UnitTask是可以复用的Debug.Log($"耗时 {Time.time - time}秒");}/// <summary>/// UniTask运行中执行回调/// UniTaskCompletionSource是对UniTask和CancellationToken的封装/// </summary>private async UniTask FallTarget(Transform targetTrans, UniTaskCompletionSource source){Vector3 startPosition = targetTrans.position;float fallTime = 20f;float elapsedTime = 0;while (elapsedTime <= fallTime){elapsedTime += Time.deltaTime;//当下落时间超过1秒时设置操作if (elapsedTime > 1f){// 表示操作完成source.TrySetResult();// 失败// source.TrySetException(new SystemException());// 取消// source.TrySetCanceled(someToken);// 泛型类UniTaskCompletionSource<T> SetResult是T类型,返回UniTask<T>}float fallY = 0.5f * G * elapsedTime * elapsedTime;targetTrans.position = startPosition + Vector3.down * fallY;await UniTask.Yield(this.GetCancellationTokenOnDestroy());}}
}

切换到线程

public class ThreadSample : MonoBehaviour
{public Button StandardRun;public Button YieldRun;private void Start(){StandardRun.onClick.AddListener(UniTask.UnityAction(OnClickStandardRun));YieldRun.onClick.AddListener(UniTask.UnityAction(OnClickYieldRun));}/// <summary>/// 线程中计算/// </summary>private async UniTaskVoid OnClickStandardRun(){int result = 0;//切换到其他线程await UniTask.RunOnThreadPool(() => { result = 1; });//切换回主线程await UniTask.SwitchToMainThread();Debug.LogError($"计算结束,当前结果是{result}");}/// <summary>/// 线程中读取文件/// </summary>private async UniTaskVoid OnClickYieldRun(){string fileName = Application.dataPath + "/Resources/test.txt";await UniTask.SwitchToThreadPool();string fileContent = await File.ReadAllTextAsync(fileName);//调用 UniTask.Yield 会自动切换会主线程await UniTask.Yield(PlayerLoopTiming.Update);Debug.LogError(fileContent);}
}

响应式编程

响应式编程(Reactive programming)简称Rx,他是一个使用LINQ风格编写基于观察者模式的异步编程模型。简单点说Rx = Observables + LINQ + Schedulers。Rx将事件转化为响应式的序列,通过LINQ操作可以很简单地组合起来,还支持时间操作。

UI事件,连点,双击,冷却


球体三次点击, 执行不同操作

按钮双击处理

点击按钮后CD时间

using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UnityEngine;
using UnityEngine.UI;public class UIEventsSample : MonoBehaviour
{public Button SphereButton;public Button DoubleClickButton;public Button CoolDownButton;public Text DoubleEventText;public Text CoolDownEventText;public float DoubleClickCheckTime = 0.5f;public float CooldownTime = 3f;void Start(){CheckSphereClick(SphereButton.GetCancellationTokenOnDestroy()).Forget();CheckDoubleClickButton(DoubleClickButton, this.GetCancellationTokenOnDestroy()).Forget();CheckCooldownClickButton(this.GetCancellationTokenOnDestroy()).Forget();}/// <summary>/// 球体连点/// </summary>private async UniTaskVoid CheckSphereClick(CancellationToken token){//将按钮的点击转换为异步可迭代器var asyncEnumerable = SphereButton.OnClickAsAsyncEnumerable();//ForEachAsync处理每一次点击时的操作,index表示第几次点击,Take(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();}else if (index == 2){//第三次点击销毁GameObject.Destroy(SphereButton.gameObject);}}, token);//三次点击后,await完成,可以进行后面的逻辑Debug.LogError("done");}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);}}/// <summary>/// 双击按钮/// </summary>private async UniTaskVoid CheckDoubleClickButton(Button button, CancellationToken token){while (true){//将点击转换为异步的UniTask,然后等待第一次点击var clickAsync = button.OnClickAsync(token);await clickAsync;DoubleEventText.text = $"按钮被第一次点击";var secondClickAsync = button.OnClickAsync(token);//第二次点击和等待时间谁先到,WhenAny返回那个先执行int resultIndex = await UniTask.WhenAny(secondClickAsync, UniTask.Delay(TimeSpan.FromSeconds(DoubleClickCheckTime), cancellationToken : token));DoubleEventText.text = resultIndex == 0 ? $"按钮被双击了" : $"超时,按钮算单次点击";}}/// <summary>/// 按钮冷却时间/// </summary>private async UniTaskVoid CheckCooldownClickButton(CancellationToken token){var asyncEnumerable = CoolDownButton.OnClickAsAsyncEnumerable();await asyncEnumerable.ForEachAwaitAsync(async (_) =>{CoolDownEventText.text = "被点击了,冷却中……";//Delay过程中不会再响应点击操作await UniTask.Delay(TimeSpan.FromSeconds(CooldownTime), cancellationToken : token);CoolDownEventText.text = "冷却好了,可以点了……";}, cancellationToken: token);}
}

响应式属性

属性值变化时,监听的进度条,文本就会同步变化

public class AsyncReactivePropertySample: MonoBehaviour
{public int maxHp = 100;public float totalChangeTime = 1f;public Text ShowHpText;public Text StateText;public Text ChangeText;public Slider HpSlider;public Image HpBarImage;public Button HealButton;public Button HurtButton;private AsyncReactiveProperty<int> currentHp;private int maxHeal = 10;private int maxHurt = 10;private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();private CancellationTokenSource _linkedTokenSource;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(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);}/// <summary>/// currentHp变化时修改提示信息/// </summary>private async UniTaskVoid CheckHpChange(AsyncReactiveProperty<int> hp){int hpValue = hp.Value;// WithoutCurrent 忽略初始值await hp.WithoutCurrent().ForEachAsync((_, index) =>{ChangeText.text = $"血量发生变化 第{index}次 变化{hp.Value - hpValue}";hpValue = hp.Value;}, this.GetCancellationTokenOnDestroy());}/// <summary>/// currentHp低于临界值,显示提示信息/// </summary>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){_cancellationTokenSource.Cancel();_cancellationTokenSource = new CancellationTokenSource();_linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, this.GetCancellationTokenOnDestroy());await SyncSlider(hp, _linkedTokenSource.Token);}/// <summary>/// 同步血条/// </summary>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;}
}

创建自定义异步可迭代器

[Serializable]
public struct ControlParams
{[Header("旋转速度")] public float rotateSpeed;[Header("移动速度")] public float moveSpeed;[Header("开枪最小间隔")] public float fireInterval;
}public class PlayerControl
{public UnityEvent OnFire;private Transform _playerRoot;private ControlParams _controlParams;private float _lastFireTime;public void Start(){StartCheckInput();}/// <summary>/// 通过MonoBehaviour将参数传进来/// </summary>public PlayerControl(Transform playerRoot, ControlParams controlParams){_playerRoot = playerRoot;_controlParams = controlParams;}/// <summary>/// 启动输入检测/// </summary>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();}/// <summary>/// 创建自定义异步迭代器/// </summary>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();}});}/// <summary>/// 范围玩家的移动/// </summary>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;}/// <summary>/// 返回旋转,根据鼠标水平方向移动距离计算/// </summary>private float GetInputAxisValue(){if (!Input.GetMouseButton(1)) return default;var result = Input.GetAxis("Mouse X") * _controlParams.rotateSpeed;return Mathf.Clamp(result, -90, 90);}/// <summary>/// 点击鼠标左键的时间/// </summary>private float GetIfFired(){if (Input.GetMouseButtonUp(0)){return Time.time;}return -1;}
}

子弹相关逻辑整合

public class FireBulletSample : MonoBehaviour
{public Transform FirePoint;[SerializeField]private GameObject bulletTemplate;[Header("射速")][SerializeField]private float flySpeed;[Header("自动回收时间")][SerializeField]private float bulletAutoDestroyTime;[Header("命中效果")][SerializeField]private GameObject hitEffect;public void Fire(){(UniTask.UnityAction(OnClickFire)).Invoke();}/// <summary>/// 开火,将子弹飞行,销毁,碰撞,创建特效等逻辑整合到一个方法中/// </summary>private async UniTaskVoid OnClickFire(){var bullet = Object.Instantiate(bulletTemplate);bullet.transform.position = FirePoint.position;bullet.transform.forward = FirePoint.forward;// 先飞出去,获取子弹本身的token来当作取消tokenvar bulletToken = bullet.transform.GetCancellationTokenOnDestroy();FlyBullet(bullet.transform, flySpeed).Forget();//到达设定时间销毁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 resultIndex = await UniTask.WhenAny(waitAutoDestroy, source.Task);if (resultIndex == 1){var collision = source.GetResult(0);Collider getCollider = collision.collider;//在子弹击中位置创建特效var go = Object.Instantiate(hitEffect, bullet.transform.position, Quaternion.identity);Object.Destroy(go, 4f);}Object.Destroy(bullet);}/// <summary>/// 子弹飞行/// </summary>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;}}
}

开源库UniTask笔记相关推荐

  1. 【转】DCMTK开源库的学习笔记3:dcmtk文件中数据元的修改

    转自:https://blog.csdn.net/zssureqh/article/details/8804736 dcm文件是医学领域DICOM3.0标准所对应的主要的文件格式.前两篇学习笔记中,学 ...

  2. 【转】DCMTK开源库的学习笔记4:利用ini配置文件对dcm影像进行归档

    转自:https://blog.csdn.net/zssureqh/article/details/8846337 背景介绍: 医学影像PACS工作站的服务端需要对大量的dcm文件进行归档,写入数据库 ...

  3. 【转】DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组)

    转自:https://blog.csdn.net/zssureqh/article/details/8784980 DCMTK开源库介绍: DCMTK是目前最全面实现DICOM3.0标准的开源库,通过 ...

  4. C++跨平台开源库POCO学习笔记

    原文链接:http://grow.sinaapp.com/?p=1271 POCO(pocoproject.org)看起来是很不错的C++跨平台开源库,包含网络(HTTP.FTP等).正则.XML.Z ...

  5. 『NLP学习笔记』工业级自然语言处理spaCy开源库的使用

    工业级自然语言处理spaCy开源库的使用 文章目录 一. spaCy介绍 1.1. 什么是spaCy 1.2. spaCy的优势 1.3. spaCy的处理过程(Processing Pipeline ...

  6. 点云入门笔记(二):点云常用软件、开源库以及数据集

    1.常用软件 (1)开源软件: CloudCompare 特点:         开源,且支持多平台(Windows, Mac,Linux),支持常见的点云数据格式,简单的点云编辑,支持用户自己添加插 ...

  7. 33个2017年必须了解的iOS开源库

    原文 本文翻译自Medium,原作者为 Paweł Białecki 照片版权:(Unsplash/Markus Pe) 你好,iOS 开发者们!我的名字叫 Paweł,我是一个独立 iOS 开发者, ...

  8. 33 个 2017 年必须了解的 iOS/swift 开源库第三方库

    本文翻译自Medium,原作者为 Paweł Białecki &lt;img src="https://pic3.zhimg.com/v2-c786777447261347b0d9 ...

  9. 【杂谈】如何应对烦人的开源库版本依赖-做一个心平气和的程序员?

    平时做开发时最烦人的除了项目不会做,恐怕就是环境依赖的问题了吧. 一个好好的项目,换个环境总是出问题,开源库一升级就挂掉,总有"坏人"搞掉自己的系统库,今天就来随便聊聊吧. 作者 ...

最新文章

  1. IP白名单添加了当前IP,获取access_token时依然报出错误码40164的坑
  2. Python培训班适合哪些人报名学习
  3. 设置编码格式为utf8
  4. FlashBuilder(FB/eclipse) 打开多个无效
  5. 提取指定的PDF表格保存到Excel
  6. java epoll select_Java后端常见面试题
  7. Android 自定义viewpager 三张图片在同一屏幕轮播的效果
  8. python中的try...except...finally函数
  9. 数据库命名规范(转)
  10. SAP Kyma(Extension Factory on SAP Cloud Platform)的架构简介
  11. cf1562 C. Rings
  12. C++ STL学习笔记 : 1. template 模板函数
  13. 【HDU - 5777】domino(贪心)
  14. 关于注册规划师的点点碎碎
  15. n个人围成一圈指针c语言,C语言N个人围成一圈报数用二级指针实现,不用数组!...
  16. PHP100视频解压密码以及目录
  17. redis 存 JSONObject 报 nested exception is java.io.NotSerializableException:
  18. 游戏攻略 一 天堂W(韩)
  19. 文华软件登录显示请选择服务器,文华随身行 请先登入云服务器
  20. 去除PDF文件中的斜体文字水印

热门文章

  1. win10如何安装.NET3.5
  2. conda upgrade --all 中途退出后失败如何解决
  3. 微博第三方+海外国际版+V2EX第三方
  4. 1114 计算营业额
  5. 蔡颖-《APS走向实践》书解读之四:供应链、系统模型、APS软件
  6. 2022年,计算机er保研选计科还是电信?
  7. 2、金融量化分析简介
  8. 一个计步器源代码的研究
  9. 冯森林:手机淘宝中的那些Web技术
  10. asm磁盘头自动备份19c-au11