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
}
相关推荐
Thomas_YXQ2 小时前
Unity3D Huatuo技术原理剖析详解
unity·unity3d·游戏开发·性能调优·热更新
火云洞红孩儿4 小时前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
虾球xz5 小时前
游戏引擎学习第59天
学习·游戏引擎
zh路西法5 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
橘子遇见BUG9 小时前
Unity Shader学习日记 part 3 线性代数--矩阵变换
学习·线性代数·unity·矩阵·图形渲染
神洛华11 小时前
Y3编辑器教程8:资源管理器与存档、防作弊设置
编辑器·游戏引擎·游戏程序
Moweiii11 小时前
SDL3 GPU编程探索
c++·游戏引擎·图形渲染·sdl·vulkan
Artistation Game12 小时前
一、c#基础
游戏·unity·c#·游戏引擎
成都渲染101云渲染666612 小时前
云渲染,Enscape、D5、Lumion渲染提速教程
运维·服务器·unity·电脑·图形渲染·blender·houdini
超龄魔法少女1 天前
[Unity] ShaderGraph动态修改Keyword Enum,实现不同效果一键切换
unity·技术美术·shadergraph