内容来源:up主游戏石匠,仅作笔记,推荐关注该up主。
UniTask是Github上的开源库,为Unity提供一个高性能异步方案,可以代替协程实现异步操作,中文文档
优点:

通过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();
}
public async void DelayTest()
{
//性能最好,可以设置等待时机,PlayerLoopTiming 对应Unity中playerloop的更新时机
await UniTask.Yield(PlayerLoopTiming.LastUpdate);
//等待1秒,类似 yield return new WaitForSeconds(1),可以设置 ignoreTimeScale
await UniTask.Delay(TimeSpan.FromSeconds(1), false);
//执行在下一帧的update之后,类似 yield return null,和 UniTask.Yield() 效果一样
await UniTask.NextFrame();
//这一帧的最后,类似 yield return new WaitForEndOfFrame(),this是一个MonoBehaviour
await 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
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包装了OnClickTest
TestButton.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 "超时";
}
}
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()
{
//不需要等待时候就调用Forget
FallTarget(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());
}
}
}
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操作可以很简单地组合起来,还支持时间操作。

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

按钮双击处理

点击按钮后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()
{
// 设置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(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来当作取消token
var 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;
}
}
}
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c
TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是
关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭6年前。Improvethisquestion是否有任何用Ruby或Python编写的生产就绪的开源Twitter克隆?我对功能丰富的实现更感兴趣,而不仅仅是简单的Twitter消息(例如:API、FBconnect、通知等)谢谢!
文章目录写在前面1、下载与安装(windows)1.1、idea中配置gradle2、基础知识(Gradle6.9为例)2.1、Gradle脚本语法2.1.1、dependsOn2.1.2、创建动态任务2.1.3、增加任务行为2.1.4、参数2.1.5、Ant任务2.1.6、方法2.1.7、默认任务2.1.6、依赖任务的不同输出3、java项目中使用3.1、在已有项目中构建gradle3.2、在新建项目时构建gradle(idea)3.3、gradle项目目录结构3.4、build.gradle3.4.1、plugins3.4.2、repositories3.4.3、dependencies3
TCP是面向连接的协议,连接的建立和释放是每一次面向连接的通信中必不可少的过程。TCP连接的管理就是使连接的建立和释放都能正常地进行。三次握手TCP连接的建立—三次握手建立TCP连接①若主机A中运行了一个客户进程,当它需要主机B的服务时,就发起TCP连接请求,并在所发送的分段中用SYN=1表示连接请求,并产生一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x。主机B收到A的连接请求报文,就完成了第一次握手。客户端发送SYN=1表示连接请求客户端发送一个随机发送序号x,如果连接成功,A将以x作为其发送序号的初始值:seq=x②主机B如果同意建立连接,则向主机A发送确认报
VXLAN简介定义RFC定义了VLAN扩展方案VXLAN(VirtualeXtensibleLocalAreaNetwork,虚拟扩展局域网)。VXLAN采用MACinUDP(UserDatagramProtocol)封装方式,是NVO3(NetworkVirtualizationoverLayer3)中的一种网络虚拟化技术。目的随着网络技术的发展,云计算凭借其在系统利用率高、人力/管理成本低、灵活性/可扩展性强等方面表现出的优势,已经成为目前企业IT建设的新趋势。而服务器虚拟化作为云计算的核心技术之一,得到了越来越多的应用。服务器虚拟化技术的广泛部署,极大地增加了数据中心的计算密度;同时,为
Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统,支持封闭车场和路边车场,支持微信支付宝多种支付渠道,支持多种硬件,涵盖了停车场管理系统的所有基础功能。技术栈Springboot,MybatisPlus,Beetl,Mysql,Redis,RabbitMQ,UniApp功能云端功能序号模块功能描述1系统管理菜单管理配置系统菜单2系统管理组织管理管理组织机构3系统管理角色管理配置系统角色,包含数据权限和功能权限配置4系统管理用户管理管理后台用户5系统管理租户管理多租户管理6系统管理公众号配置租户公众号配置7系统管理操作日志审计日志8系统
我有兴趣了解使用nosql将如何影响rails应用程序的架构/设计/代码。有人知道使用nosql持久性的开源rails应用程序的一个好例子吗?谢谢 最佳答案 看看这些项目:卡桑德拉用法atDigg。卡桑德拉用法atTwitter。Friendly用法atFetLife(nsfw)。最后,MyNoSQL是一个提供nosql相关信息的好网站。 关于ruby-on-rails-有没有很好的引用(开源)RailsNoSQL应用程序?,我们在StackOverflow上找到一个类似的问题:
目录一、原理部分1、什么是串行通信(1)并行通信与串行通信(2)串行通信的制式(3)串行通信的主要方式 2、配置串口(1)SCON和PCON:串行口1的控制寄存器(2)SBUF:串行口数据缓冲寄存器 (3)AUXR:辅助寄存器编辑(4)ES、PS:与串行口1中断相关的寄存器(5)波特率设置 3、串口框架编写二、程序案例一、原理部分1、什么是串行通信(1)并行通信与串行通信微控制器与外部设备的数据通信,根据连线结构和传送方式的不同,可以分为两种:并行通信和串行通信。并行通信:数据的各位同时发送与接收,每个数据位使用一条导线,这种方式传输快,但是需要多条导线进行信号传输。串行通信:数据一位一