开源库UniTask笔记

内容来源: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),可以设置 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

在这里插入图片描述
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 "超时";
    }
}

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()
    {
        //不需要等待时候就调用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());
        }
    }
}

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()
    {
        // 设置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;
        }
    }
}