留个档,Unity Animator state节点的Motion动态替换AnimationClip

news2024/11/26 16:49:12

前言

·由于Unity没有提供直接替换的API,所以在仅限的API下进行逻辑操作。

·替换的原理是差不多的,利用AnimatorOverrideController,进行运行时的覆盖。

·网上搜索很多文章是利用 名字字符串作为hash的key来进行替换。不满足我自己项目中的需求,于是利用GetOverrides 和 ApplyOverrides,封装了这个功能。

思考过程

·因为Animator的操作是UnityeEditor级别的,所以需要AnimatorOverrideController来辅助操作。

· AnimatorOverrideController的两个接口的特性会针对一个数据结构List<KeyValuePair<AnimationClip, AnimationClip>>。 查API就可以知道, 是from to的概念。
我自己做了测试,理解上可以认为, KeyValuePair里的key指的是原本Animator中的动画文件。 value是需要替换的AnimationClip。

·因为需要进行替换,节点里的动画不能为空。不然就没法拿来替换了。

·我要实现的模块,可以提供接口,是针对state节点的名字,进行操作的,需要一个映射关系。


所以,这个模块的设计,那么就会是Animator一开始会有一套默认的AnimationClip存在于各个state中。这一步我认为是静态过程即可。
我还需要一个state名字和默认clip之间的对应关系, 这个静态即可, 这在UnityEditor下很容易处理。并且序列化就行了。
之后就是写逻辑了


/*
 * Create by fox.huang 黄文叶
 * time: 2023.6.24
 * 
 * UnityAPI提供的替换逻辑,只能通过指定Clip的名字,或者指定Clip对象进行替换
 * 针对State的修改,是在UnityEditor下的,也就是runTime不能用。所以这个类的目的是实现State映射clip的修改。
 *
 * 注意:为了区分唯一性,同layer内,不能出现重名的节点(在使用子状态机的时候,会出现同名情况)
 */
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor.Animations;
using UnityEditor;
using System.IO;
#endif

/// <summary>
/// Animator节点上的AnimationClip替换组件。
/// </summary>
public class AnimatorClipReplaceComponent : MonoBehaviour
{
    [System.Serializable]
    public class Pack
    {
        //这边的序列化应该灰显的 懒得写了,就这么用呗。
        [SerializeField] public string m_strName = null;        // 节点的名字
        [SerializeField] public int m_nLayer = 0;
        [SerializeField] public AnimationClip m_aniClipDefault = null; //默认的动画clip

        /// <summary>
        /// 构造
        /// </summary>
        /// <param name="strName">节点名字</param>
        /// <param name="aniClipDefault">默认动画名字</param>
        public Pack(string strName, int nLayer, AnimationClip aniClipDefault)
        {
            m_strName = strName;
            m_nLayer = nLayer;
            m_aniClipDefault = aniClipDefault;
        }
    }

#if UNITY_EDITOR
    [Tooltip("创建填充用AnimationClip的路径(空节点也认为是有效节点,创建默认clip用作替换依据)")]
    [SerializeField] string m_strClipAssetCreatePath = "Assets/Art/Animation";
    /// <summary>
    /// 刷新当前的列表
    /// </summary>
    [ContextMenu("Refresh Default State Node List")]
    private void RefreshList()
    {
        Animator ani = this.GetComponent<Animator>();
        if (ani == null)
        {
            Debug.LogError("AnimatorClipReplaceHelper RefreshList, can not find component Animator in "
                + this.gameObject.name);
            return;
        }
        
        string strAssetPath = AssetDatabase.GetAssetPath(ani.runtimeAnimatorController);
        AnimatorController animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(strAssetPath);
        if (animatorController == null)
        {
            Debug.LogError("AnimatorClipReplaceHelper RefreshList, can not load asset : "
                + strAssetPath);
            return;
        }

        //创建默认clip的文件夹
        CreateFolder(m_strClipAssetCreatePath);

        //开始遍历AnimatorCrontroller
        var layers = animatorController.layers;
        int nLayerCount = layers.Length;
        m_listInfos = new List<Pack>();
        for (int i = 0; i < nLayerCount; i++)
        {
            var oneLayer = layers[i];
            CollectStates(oneLayer.stateMachine.states, i);
            CollectStateMachines(oneLayer.stateMachine.stateMachines, i);
        }

        //重新保存一次AnimatorController
        EditorUtility.SetDirty(animatorController);
        AssetDatabase.SaveAssets();
    }

    private void CreateFolder(string strPath)
    {
        string strFullPath = strPath.Replace("Assets", Application.dataPath);
        if (!Directory.Exists(strFullPath))
        {
            Directory.CreateDirectory(strFullPath);
        }
    }

    /// <summary>
    /// 递归收集节点们的信息
    /// </summary>
    private void CollectStates(ChildAnimatorState[] states, int nLayer)
    {
        int nCount = states.Length;
        for (int i = 0; i < nCount; i++)
        {
            var oneState = states[i].state;
            string strName = oneState.name;

            if (oneState.motion == null)
            {
                AnimationClip newClip = new AnimationClip();
                string strLocalPath = m_strClipAssetCreatePath + "aniclip_def_" +
                    strName.ToLower() + "_" + i.ToString() +
                    ".anim";
                AssetDatabase.CreateAsset(newClip, strLocalPath);
                AssetDatabase.ImportAsset(strLocalPath);
                oneState.motion = AssetDatabase.LoadAssetAtPath<AnimationClip>(strLocalPath);
            }

            m_listInfos.Add(new Pack(strName, nLayer, (AnimationClip)oneState.motion));
        }
    }

    /// <summary>
    /// 递归收集子状态机的内容
    /// </summary>
    private void CollectStateMachines(ChildAnimatorStateMachine[] group, int nLayer)
    {
        if (group == null)  // 安全判断
        {
            return;
        }
        int nCount = group.Length;
        if (nCount == 0)    // 安全判断
        {
            return;
        }
        for (int i = 0; i < nCount; i++)
        {
            var one = group[i].stateMachine;
            CollectStates(one.states, nLayer);
            CollectStateMachines(one.stateMachines, nLayer);
        }
    }

#endif


    /// <summary>
    /// Animator节点名字对应的信息集合
    /// </summary>
    [SerializeField] List<Pack> m_listInfos = null;
    private Dictionary<string, AnimationClip> m_dicDefaultClips = null;

    [SerializeField] Animator m_animator = null;
    private List<KeyValuePair<AnimationClip, AnimationClip>> m_listRuntime
        = new List<KeyValuePair<AnimationClip, AnimationClip>>();
    private AnimatorOverrideController m_aoc = null;

    private void Awake()
    {
        //创建一个AnimatorOverrideController作为替换的容器
        if (m_animator == null)
        {
            m_animator = this.GetComponent<Animator>();
        }
        if (m_animator == null)
        {
            Debug.LogError("AnimatorClipReplaceHelper Awake, no Animator : " + this.gameObject.name);
            return;
        }
        m_aoc = new AnimatorOverrideController(m_animator.runtimeAnimatorController);
        m_aoc.name = "aoc_" + this.gameObject.name;
        m_animator.runtimeAnimatorController = m_aoc;

        //获取到当前clip的列表
        m_aoc.GetOverrides(m_listRuntime);

        m_dicDefaultClips = new Dictionary<string, AnimationClip>();
        //序列化信息 转换成 键值对
        int nCount = m_listInfos.Count;
        for (int i = 0; i < nCount; i++)
        {
            var pack = m_listInfos[i];
            string strKeyName = pack.m_strName + pack.m_nLayer.ToString();
            if (m_dicDefaultClips.ContainsKey(strKeyName))
            {
                Debug.LogWarning("AnimatorClipReplaceHelper Awake, already has node : " + pack.m_strName);
                continue;
            }
            m_dicDefaultClips.Add(strKeyName, pack.m_aniClipDefault);
        }
    }

    /// <summary>
    /// 对一个节点的AnimationCLip进行替换
    /// </summary>
    /// <param name="strStateName">节点名</param>
    /// <param name="aniClip">动画Clip(为null的时候还原到默认)</param>
    public void MarkReplace(string strStateName, AnimationClip aniClip, int nLayer = 0)
    {
        strStateName += nLayer.ToString();
        AnimationClip aniClipDefault;
        if (!m_dicDefaultClips.TryGetValue(strStateName, out aniClipDefault))
        {
            Debug.LogWarning("AnimatorClipReplaceHelper MarkReplace, no state be find " +
                strStateName + " in layer: " + nLayer);
            return;
        }

        if (aniClip == null)
        {
            aniClip = aniClipDefault;
        }

        int nCount = m_listRuntime.Count;
        for (int i = 0; i < nCount; i++)
        {
            if (m_listRuntime[i].Key == aniClipDefault)
            {
                m_listRuntime[i] = new KeyValuePair<AnimationClip, AnimationClip>(aniClipDefault, aniClip);
                break;  // 这里break,因为获取到的kv list是去重的
            }
        }
    }

    /// <summary>
    /// 把之前记录的修改,刷新到控件上
    /// </summary>
    public void Flush()
    {
        m_aoc.ApplyOverrides(m_listRuntime);
    }

}


使用

在用的时候在这里插入图片描述
· 我会先通过UnityEditor ContextMenu按钮的方式把 映射关系和默认clip保存写到序列化。那么这个prefab就可以作为一个单独的对象加载了。 他可以是一个骨骼的prefab也可以是其他。

·Runtime时,MarkReplace进行state名字对应的AnimationClip的设定,最后Flush,进行批量应用刷新。


程序学无止尽。
欢迎大家沟通,有啥不明确的,或者不对的,也可以和我私聊
我的QQ 334524067 神一般的狄狄

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/665532.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【系统开发】尚硅谷 - 谷粒商城项目笔记(五):分布式缓存

文章目录 分布式缓存缓存使用场景redis作缓存中间件引入redis依赖配置redis堆外内存溢出 缓存失效问题缓存穿透缓存雪崩缓存击穿 Redisson分布式锁导入依赖redisson配置类可重入锁读写锁缓存一致性解决 缓存-SpringCache简介Cacheable自定义缓存配置CacheEvictCachePut原理与不…

【网络】协议的定制与Json序列化和反序列化

文章目录 应用层初识TCP协议通讯流程定制协议再谈协议网络版本计算器Protocal.hppCalServerCalClient Json的安装 应用层 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层 初识TCP协议通讯流程 建立链接和断开链接 基于TCP协议&#xff0c…

六、使用深度学习构建人脸识别模型

本章介绍机器学习中人脸识别的历史以及从零开始如何构建一个人脸识别模型,含所有训练数据,源代码,不强制要求GPU。使用 docker 来管理库依赖项,提供与平台无关的一致环境。使用 Dlib 进行预处理,使用 Tensorflow + Scikit-learn 训练能够根据图像预测身份的分类器。 1、人…

《吉林省教育学院学报》简介及投稿邮箱

《吉林省教育学院学报》简介&#xff1a; 主管单位 吉林省教育厅 主办单位 吉林省教育学院 出版周期&#xff1a;月刊 国际刊号&#xff1a;ISSN&#xff1a;1671-1580&#xff1b;国内刊号&#xff1a;CN&#xff1a;22-1296/G4&#xff1b;邮发代号&#xff1a;12-223 出…

创建线程三种方法

创建和运行线程 方法一&#xff0c;直接使用 Thread // 创建线程对象 Thread t new Thread() { public void run() {// 要执行的任务} }; // 启动线程 t.start(); 例如&#xff1a; // 构造方法的参数是给线程指定名字&#xff0c;推荐Thread t1 new Thread("t1"…

Doris的安装

Doris的安装 文章目录 Doris的安装写在前面Linux 操作系统版本需求软件需求操作系统安装要求设置系统最大打开文件句柄数时钟同步关闭交换分区&#xff08;swap&#xff09; 开发测试环境生产环境 安装下载安装包默认端口集群部署前置准备安装部署FE安装部署BE在 **FE** 中添加…

2.1C++派生

C派生概述 C中的派生允许从一个已有的类中创建一个新的类&#xff0c;该新类继承了原有类的属性和方法。 派生类可以增加新的属性和方法&#xff0c;也可以重写原有类的方法以改变其行为。 C中的派生类可以通过公有、私有和保护继承来继承基类的成员。 公有继承允许派生类访…

网络协议驱动互联网

在分布式系统中&#xff0c;数据通过各种网络协议在网络中传输。作为应用程序开发者&#xff0c;这往往在问题出现之前似乎是一个黑盒子。 在本文中&#xff0c;我们将解释常见网络协议的工作原理&#xff0c;它们在分布式系统中的应用以及我们如何解决常见问题。后续还会介绍一…

开源一键拥有你自己的ChatGPT+Midjourney网页服务,用不用是另一回事,先收藏!

功能支持 原ChatGPT-Next-Web所有功能 midjourney imgine 想象 midjourney upscale 放大 midjourney variation 变幻 midjourney describe 识图 midjourney blend 混图 midjourney 垫图 绘图进度百分比、实时图像显示 自身支持midjourney-api 参数说明 MIDJOURNEY_PROXY_URL …

组态王与多台PLC之间无线以太网通信

在实际系统中&#xff0c;同一个车间里分布多台PLC&#xff0c;通过上位机集中控制。通常所有设备距离在几十米到上百米不等。在有通讯需求的时候&#xff0c;如果布线的话&#xff0c;工程量较大耽误工期&#xff0c;这种情况下比较适合采用无线通信方式。 本方案以组态王和2…

java -D详解

官方文档对 -D 有明确的解释&#xff0c;具体看 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 简单解释一下 首先&#xff0c;java 命令是这么用的 其次&#xff0c;-D 是属于 [options] 这一块的。而 [options] 又分为如下几类 -D 就属于标准选…

企业级医疗项目:码猿慢病云管理系统来了!

大家好&#xff0c;我是不才陈某~ 不知不觉转行到医疗领域已经两年多了&#xff0c;前后服务过三百多家医疗机构&#xff0c;深耕于医疗领域&#xff0c;不知道在座的各位有从事医疗领域工作的吗&#xff1f; 上个月和朋友搞了一套的企业级的医疗实战项目&#xff1a;码猿慢病云…

Arthas线上故障案例分析——内存使用率上升,负载突然变高

使用经验分享 线上故障排查思路&#xff1a; 1、紧急处理&#xff0c;优先保障服务可用&#xff08;如切换vip&#xff0c;主备容灾&#xff09; 2、保留第一现场&#xff0c;通过jstack -l {pid} > jvmtmp.txt &#xff0c;打印栈信息 &#xff08;后续可以在gceasy官网上…

Maven插件开发及Demo演示

引言 maven不仅仅只是项目的依赖管理工具&#xff0c;其强大的核心来源自丰富的插件&#xff0c;可以说插件才是maven工具的灵魂。本篇文章将对如何自定义maven插件进行讲解&#xff0c;希望对大家有所帮助。 背景 讲如何开发maven插件之前&#xff0c;不妨先来聊一下什么是…

STM32速成笔记—定时器

文章目录 一、什么是定时器二、定时器有什么用三、通用定时器详细介绍3.1 时钟来源3.2 预分频器&#xff0c;计数器&#xff0c;自动重装载寄存器3.2.1 预分频器3.2.2 计数器3.2.3 自动重装载寄存器 3.3 触发控制器 四、PWM4.1 什么是PWM4.2 什么是占空比4.3 STM32F1 PWM介绍4.…

【Python 随练】打印菱形图案

题目&#xff1a; 打印出如下图案&#xff08;菱形&#xff09; ********* ****************简介&#xff1a; 在本篇博客中&#xff0c;我们将使用Python代码打印一个菱形图案。我们将提供问题的解析&#xff0c;并给出一个完整的代码示例来生成这个菱形图案。 问题分析&am…

阿里云开源离线同步工具DataX3.0,用于数据仓库、数据集市、数据备份

DataX是阿里云开源的一款离线数据同步工具&#xff0c;支持多种数据源和目的地的数据同步&#xff0c;包括但不限于MySQL、Oracle、HDFS、Hive、ODPS等。它可以通过配置文件来定义数据源和目的地的连接信息、数据同步方式、数据过滤等&#xff0c;从而实现数据的高效、稳定、可…

C++初阶—完善适配器(反向迭代器)

目录 0. 前言 1、反向迭代器定义 2、反向迭代器需要实现的相关函数 3、反向迭代器分析 4、针对vector物理空间结构分析 5、针对list物理空间结构分析 6、反向迭代器适配器的实现及测试 0. 前言 本篇文章主要根据前面所实现的STL中支持迭代器的容器进行完善&#xff0c;使…

Mysql数据库日志和数据的备份恢复(去看一看海吧)

文章目录 一、数据库备份的重要性二、数据库备份的分类1.物理备份&#xff1a;对数据库操作系统的物理文件&#xff08;如数据文件、日志文件等)的备份。2.逻辑备份&#xff1a;对数据库逻辑组件&#xff08;如表等数据库对象&#xff09;的备份&#xff0c;导出sql文件。3.完全…

被卖到 2w 的 ChatGPT 提示词 Prompt 你确定不想要吗?

有朋友说&#xff0c;用 ChatGPT 生成的文案刻板化&#xff0c;格式化&#xff0c;而且往往也不是我想要的。‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍ 想要用好 ChatGPT 人工智能工具太难了&#xff0c;想一个好的提示词&#xff0c;也太不容易了&#xff0c;ChatGPT 就像一个宝藏…