Unity中的异步编程【7】——在一个异步方法里播放了animation动画,取消任务时,如何停止动画播放

用一个异步方法来播放一个动画,正常情况是:动画播放结束时,异步方法宣告结束。那如果我提前取消这个异步任务,那在这个异步方法里面,我要怎么停止播放呢?!

一、播放animation动画的异步实现

  • 1、用play播放动画片段
  • 2、await一段时间,等动画播放结束
  • 3、用stop停止动画播放

二、两种实现方式

1 、纯多任务模式的实现

实现原理:

定义了两个结束的事件(或者Task):

(1)第一个是播放时长到点了

(2)第二个是用户取消了异步任务

(3)用whenAny等待

csharp 复制代码
    /// <summary>
        /// 等待一个动画播放完毕
        /// 中间如果任务被取消,则停止播放动画
        /// </summary>
        /// <param name="Anim"></param>
        /// <param name="startTime"></param>
        /// <param name="endTime"></param>
        /// <param name="speed"></param>
        /// <param name="ctk">任务取消标志</param>
        /// <returns></returns>

        public static async UniTask<bool> PlayAnim(Animation Anim, float startTime, float endTime, float speed, CancellationToken ctk)
        {
            Debug.Log($"当前Time.timeScale = {Time.timeScale}");
            float t = (endTime - startTime) * Time.timeScale; //考虑到动画时间倍率
            Debug.Log($"动画的时长为:{t}秒");
            Anim[Anim.clip.name].time = startTime;//跳过第几帧
            Anim[Anim.clip.name].speed = speed;
            Anim.Play(Anim.clip.name); //Play()

            //如果时间到点,结束,并停止动画
            Func<UniTask> timeFn = async () =>
            { 
                await UniTask.Delay(TimeSpan.FromSeconds(t), cancellationToken: ctk);
                Anim.Stop();
            };

            //用户取消任务,结束,并停止动画
            Func<UniTask> cancelFn = async () =>
            {
                Debug.Log("开始执行cancelFn的循环:");
                while (true)
                {
                    Debug.Log($"ctk.IsCancellationRequested = {ctk.IsCancellationRequested}");
                    if (ctk.IsCancellationRequested)
                    {
                        Debug.Log($"任务取消:{ctk.IsCancellationRequested}");
                        Anim.Stop();
                        break;
                    };
                    
                    await UniTask.Yield();        //注意,这里不能随意加ctk,不然不能停,直接跳出了
                    //await UniTask.Yield(ctk);   
                }
                Debug.Log("结束cancelFn的循环");
            };

            //等待结束
            var idx = await UniTask.WhenAny(timeFn(), cancelFn()).AttachExternalCancellation(ctk);
            Debug.Log($"任务结束:结束方式为:{idx} 备注:0 = 动画播放结束,1 = 用户取消任务");
            return true;
        }

2 、手工启动一个循环,每帧检查结束条件

csharp 复制代码
        /// <summary>
        /// 等待一个动画播放完毕
        /// 中间如果任务被取消,则停止播放动画
        /// 改进了结束的判断方式
        /// </summary>
        /// <param name="Anim"></param>
        /// <param name="startTime"></param>
        /// <param name="endTime"></param>
        /// <param name="speed"></param>
        /// <param name="ctk">任务取消标志</param>
        /// <returns></returns>

        public static async UniTask<bool> PlayAnim2(Animation Anim, float startTime, float endTime, float speed, CancellationToken ctk)
        {
            Debug.Log($"当前Time.timeScale = {Time.timeScale}");
            float t = (endTime - startTime) * Time.timeScale; //考虑到动画时间倍率
            float elapse = 0f;
            Debug.Log($"动画的时长为:{t}秒");
            Anim[Anim.clip.name].time = startTime;//跳过第几帧
            Anim[Anim.clip.name].speed = speed;
            Anim.Play(Anim.clip.name); //Play()

            //每帧进行结束判断
            while (true)
            {
                elapse += Time.deltaTime; 

                //任务被取消
                Debug.Log($"ctk.IsCancellationRequested = {ctk.IsCancellationRequested}");
                if (ctk.IsCancellationRequested)
                {
                    Debug.Log($"任务取消:{ctk.IsCancellationRequested}");
                    //Anim.Stop();
                    break;
                };

                //动画播放完毕
                if (elapse >= t)
                {
                    break;
                }

                await UniTask.Yield();        //注意,这里不能随意加ctk,不然不能停,直接return了
                //await UniTask.Yield(ctk);   
            }

            Anim.Stop();
            return true;
        }

三、测试流程

  • 1、启动一个"线程(异步任务)"------播放动画
  • 2、等待2秒后,停止任务
  • 3、停止【播放动画】的"线程"
csharp 复制代码
//获取animation组件
if (anim == null) anim = this.GetComponent<Animation>();
var cti = TaskSignal.CreatCts();

//启动一个"线程"------播放动画
PlayAnim2(anim, 0f, 5f, 1, cti.cts.Token).Forget();

//等待2秒后,停止任务
await UniTask.Delay(1500);

Debug.Log("停止任务......");
//停止【播放动画】的"线程"
TaskSignal.CancelTask(cti.id);

四、效果

1、等待全部播放完毕

2、播放2秒后取消任务(同时停止播放)

五、附录:测试用的代码

为了样例完整性,我把三个脚本并在一个脚本里,请忽略杂乱的代码组织

csharp 复制代码
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using System;
using System.Linq;

public class TestPlayAnimation : MonoBehaviour
{
    public Animation anim;

    private async UniTask TestPlay()
    {
        //获取animation组件
        if(anim == null) anim = this.GetComponent<Animation>();
        var cti = TaskSignal.CreatCts();

        //启动一个"线程"------播放动画
        PlayAnim(anim, 0f, 5f, 1,cti.cts.Token).Forget();

        //等待2秒后,停止任务
        await UniTask.Delay(1500);

        Debug.Log("停止任务......");
        //停止【播放动画】的"线程"
        TaskSignal.CancelTask(cti.id);
    }

    private async UniTask TestPlay2()
    {
        //获取animation组件
        if (anim == null) anim = this.GetComponent<Animation>();
        var cti = TaskSignal.CreatCts();

        //启动一个"线程"------播放动画
        PlayAnim2(anim, 0f, 5f, 1, cti.cts.Token).Forget();

        //等待2秒后,停止任务
        await UniTask.Delay(1500);

        Debug.Log("停止任务......");
        //停止【播放动画】的"线程"
        TaskSignal.CancelTask(cti.id);
    }

#if UNITY_EDITOR
    [ContextMenu("播放整个动画")]
#endif
    void test1()
    {
        PlayAnim2(anim, 0f, 5f, 1,this.GetCancellationTokenOnDestroy()).Forget();
    }

#if UNITY_EDITOR
    [ContextMenu("停止测试")]
#endif
    void test2()
    {
        TestPlay().Forget();
    }

#if UNITY_EDITOR
    [ContextMenu("停止测试2")]
#endif
    void test3()
    {
        TestPlay2().Forget();
    }

    #region        =================用到的异步方法=======================        
    /// <summary>
    /// 等待一个动画播放完毕
    /// 中间如果任务被取消,则停止播放动画
    /// </summary>
    /// <param name="Anim"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <param name="speed"></param>
    /// <param name="ctk">任务取消标志</param>
    /// <returns></returns>

    public static async UniTask<bool> PlayAnim(Animation Anim, float startTime, float endTime, float speed, CancellationToken ctk)
    {
        Debug.Log($"当前Time.timeScale = {Time.timeScale}");
        float t = (endTime - startTime) * Time.timeScale; //考虑到动画时间倍率
        Debug.Log($"动画的时长为:{t}秒");
        Anim[Anim.clip.name].time = startTime;//跳过第几帧
        Anim[Anim.clip.name].speed = speed;
        Anim.Play(Anim.clip.name); //Play()

        //如果时间到点,结束,并停止动画
        Func<UniTask> timeFn = async () =>
        {
            await UniTask.Delay(TimeSpan.FromSeconds(t), cancellationToken: ctk);
            Anim.Stop();
        };

        //用户取消任务,结束,并停止动画
        Func<UniTask> cancelFn = async () =>
        {
            Debug.Log("开始执行cancelFn的循环:");
            while (true)
            {
                //Debug.Log($"ctk.IsCancellationRequested = {ctk.IsCancellationRequested}");
                if (ctk.IsCancellationRequested)
                {
                    Debug.Log($"任务取消:{ctk.IsCancellationRequested}");
                    Anim.Stop();
                    break;
                };

                await UniTask.Yield();        //注意,这里不能随意加ctk,不然不能停,直接跳出了
                                              //await UniTask.Yield(ctk);   
            }
            Debug.Log("结束cancelFn的循环");
        };

        //等待结束
        var idx = await UniTask.WhenAny(timeFn(), cancelFn()).AttachExternalCancellation(ctk);
        Debug.Log($"任务结束:结束方式为:{idx} 备注:0 = 动画播放结束,1 = 用户取消任务");
        return true;
    }

    /// <summary>
    /// 等待一个动画播放完毕
    /// 中间如果任务被取消,则停止播放动画
    /// 改进了结束的判断方式
    /// </summary>
    /// <param name="Anim"></param>
    /// <param name="startTime"></param>
    /// <param name="endTime"></param>
    /// <param name="speed"></param>
    /// <param name="ctk">任务取消标志</param>
    /// <returns></returns>

    public static async UniTask<bool> PlayAnim2(Animation Anim, float startTime, float endTime, float speed, CancellationToken ctk)
    {
        Debug.Log($"当前Time.timeScale = {Time.timeScale}");
        float t = (endTime - startTime) * Time.timeScale; //考虑到动画时间倍率
        float elapse = 0f;
        Debug.Log($"动画的时长为:{t}秒");
        Anim[Anim.clip.name].time = startTime;//跳过第几帧
        Anim[Anim.clip.name].speed = speed;
        Anim.Play(Anim.clip.name); //Play()

        //每帧进行结束判断
        while (true)
        {
            elapse += Time.deltaTime;

            //任务被取消
            //Debug.Log($"ctk.IsCancellationRequested = {ctk.IsCancellationRequested}");
            if (ctk.IsCancellationRequested)
            {
                Debug.Log($"任务取消:{ctk.IsCancellationRequested}");
                break;
            };

            //动画播放完毕
            if (elapse >= t)
            {
                break;
            }

            await UniTask.Yield();        //注意,这里不能随意加ctk,不然不能停,直接return了
                                          //await UniTask.Yield(ctk);   
        }

        Anim.Stop();
        return true;
    }


    #endregion

    #region             ===================异步任务管理脚本===============

    /// <summary>
    /// 任务管理
    /// </summary>
    public static class TaskSignal
    {
        /// 任务信息
        /// <summary>
        /// </summary>
        [Serializable]
        public class CtsInfo
        {
            /// <summary>
            /// 任务id
            /// </summary>
            [SerializeField] public int id;

            /// <summary>
            /// cst实例
            /// </summary>
            [SerializeField] public CancellationTokenSource cts;
        }

        /// <summary>
        /// 任务池子
        /// </summary>
        public static List<CtsInfo> ctsInfos = new List<CtsInfo>();

        /// <summary>
        /// 任务编号【自增】
        /// </summary>
        private static int id = 0;

        /// <summary>
        /// 创建一个任务
        /// </summary>
        /// <returns></returns>
        public static CtsInfo CreatCts()
        {
            var cts = new CancellationTokenSource();
            var ci = new CtsInfo { cts = cts, id = id };
            id++;
            ctsInfos.Add(ci);
            return ci;
        }

        /// <summary>
        /// 取消所有的任务
        /// </summary>
        public static void CancelAllTask()
        {
            Debug.Log($"开始执行:取消所有的任务CancelAllTask()");
            ctsInfos.ForEach(ci =>
            {
                Debug.Log($"CancelAllTask() : cts总数量为:{ctsInfos.Count}");
                try
                {
                    Debug.Log($"ci.id = {ci.id},取消前 ci.cts = {ci.cts.IsCancellationRequested}");
                    if (ci.cts.IsCancellationRequested == false)
                    {
                        Debug.Log("开始执行ci.cts.Cancel()");
                        ci.cts.Cancel();
                        Debug.Log("执行完毕ci.cts.Cancel()");
                    }
                    else
                    {
                        //Debug.Log("ci.cts已经取消了");
                    }

                    Debug.Log($"ci.id = {ci.id},取消后 ci.cts = {ci.cts.IsCancellationRequested}");
                }
                catch (Exception e)
                {
                    Debug.Log($"TaskSingol.CancelAllTask():取消任务时报错:{e.Message}");
                }
            });
            Debug.Log($"结束执行:取消所有的任务CancelAllTask()");
        }


        /// <summary>
        /// 取消所有的任务
        /// </summary>
        public static void CancelAllTask10()
        {
            ctsInfos.ForEach(ci =>
            {
                if (ci.cts.Token.IsCancellationRequested == false) // if (ci.cts.IsCancellationRequested == false)
                {
                    ci.cts.Cancel();
                    Debug.Log($"取消了任务:index = {ci.id}");
                }
                else
                {
                    //Debug.Log("ci.cts已经取消了");
                }
            });
        }

        /// <summary>
        /// 取消指定的任务
        /// </summary>
        public static void CancelTask(int id)
        {
            ctsInfos.Where(ci => ci.id == id).ToList().ForEach(ci => ci.cts.Cancel());
        }
    }
    #endregion
}
相关推荐
天途小编5 分钟前
无人机操控模式解析:美国手、日本手、中国手
游戏引擎·无人机·cocos2d
90后小陈老师13 小时前
Unity教学 项目2 2D闯关游戏
游戏·unity·游戏引擎
噗噗夹的TA之旅14 小时前
Unity Shader 学习20:URP LitForwardPass PBR 解析
学习·unity·游戏引擎·图形渲染·技术美术
nnsix14 小时前
Unity ReferenceFinder插件 多选资源查找bug解决
unity·游戏引擎·bug
gzroy15 小时前
Unity Shader Graph实现全息瞄准器
unity·游戏引擎
90后小陈老师18 小时前
Unity教学 基础介绍
unity·游戏引擎
90后小陈老师18 小时前
Unity教学 项目3 3D坦克大战
3d·unity·游戏引擎
秦奈20 小时前
Unity复习学习随笔(五):Unity基础
学习·unity·游戏引擎
nnsix21 小时前
Unity ReferenceFinder插件 窗口中选择资源时 同步选择Assets下的资源
java·unity·游戏引擎
麷飞花1 天前
unity3d scene窗口选中物体, 在 hierarchy高光显示
unity·editor·unity3d·u3d·hierarchy