Unity 实现一个特定动画状态切换树

news2025/2/28 3:24:01

前言

  • 今天在工作中接到一需求,要求人物摆在不同的9个格子上,在哪个格子上,就走哪个格子动画播放逻辑;
    打个比方:
  • 第一个格子上有台电脑,我将角色放上去,角色就玩电脑,每播完一次动画,就根据概率判断是否需要去喝水,最终实现的效果,就是将角色放上去,并且随机时间进行喝水;
  • 第二个格子上有个沙发, 角色摆上去之后,就先站一会,每次动画结束判断是否要坐下,坐下以后翘二郎腿,还要判断什么时候走出这个状态,重新站起来;
    等类似的效果
  • 策划会将所有的角色动画,以及相关信息全部给到我,那么该怎么实现较为高效的动画播放状态机?
  • 在这里我构建了一棵带循环的树结构,专门用于动画信息流的切换以及数据存储.

一.构建树

  • 这个功能结构比较简单,但是实现比较繁琐,且与战斗系统并不使用同一套动画状态,即:所有行为根据概率决定,严格向下,且到了该行为结束有可能返回头部节点,重新开始行为逻辑,如下图所示:
    结构图
  • 注意:这个结构中,节点的信息并不一定是不同的,也就是说,某动画a的信息有可能同时出现在多个位置上; 而策划要求必须严格按照层级关系递进,于是构建树是一个比较合适的方案
  • 所以我们在做数据处理的时候需要比较细心,尽量考虑到更多的情况

1.节点

  • 策划的数据主要包括三个信息:角色动画名称, 场景互动物体, 该物体的对应动画名称
    因为该信息可能会大量复用,且不会发生改变,所以我将他们封装起来,做成类来储存
  • 同时需要存储的还有该动画的时间长度,因为众所周知,Unity的动画长度获取比较麻烦,也没有动画结束的回调函数,且动画时长是固定的,所以这个值是有必要存的
    于是:
//编队动画结点
public class TeamDeployAnimaInfo
{
    //角色动画
    public string RoleAnima;
    //动画时间
    public float Time;
    //场景对应物件
    public Animator Animator;
    //物件动画
    public string ObjAnima;
    
    public TeamDeployAnimaInfo(string roleAnima, float time, Animator animator = null, string objAnima = null)
    {
        RoleAnima = roleAnima;
        Animator = animator;
        ObjAnima = objAnima;
        Time = time;
    }
}

上面的这个只是信息,构建树结构我们是需要有指向的节点的,那么下面给出节点信息

public class TeamDeployAnimaNode
{
    //动画信息
    public TeamDeployAnimaInfo Info;
    //子节点 概率/ 对应动画结点
    public List<KeyValuePair<int, TeamDeployAnimaNode>> Children;

    public TeamDeployAnimaNode(TeamDeployAnimaInfo info)
    {
        Info = info;
        Children = new List<KeyValuePair<int, TeamDeployAnimaNode>>();
    }
}

2.Trie树如何构建

构建树的主要原理分为两大块:拆解字符串,通过所有字符串进行构建树,这里写出伪代码:\

public class TeamDeployAnimaTrie
{
    //根据动画名存储动画信息
    Dictionary<string, TeamDeployAnimaInfo> Info_Dic = new Dictionary<string, TeamDeployAnimaInfo>();
    //根节点
    TeamDeployAnimaNode root = null;
    //重置
    public void Reset()
    {
        curNode = root;
    }
    //分割字符串
    public TeamDeployAnimaTrie(string coordiStr, string aiStr, Animator animator)
    {
        //分解字符串
        //分解coordiStr(人物position和rotation信息)
        //分解aiStr(人物行为树信息)
        //获取动画组件中所有动画时间长度
        //传入CreateNodes()函数,递归构建树
    }
    //递归构建树
    TeamDeployAnimaNode CreateNodes()
    {
        
    }
}
  • 具体的构建实现方式根据需求来定,这里给出我的主要实现方式:
  • 首先先构建info信息,可以在字典中查找,没有查到就插入一份进去
  • 通过info信息建立节点Node
  • 在下一层中查找该信息的所有存在分支,这里需要注意,分支有可能指向自己,或者(当下一层不存在或者下一层没有其他动画信息)指向队伍根节点,需要额外进行判断
  • 递归新建节点,将新建的节点分支插入Children列表中
  • 返回本层Node节点.
  • 如果之前有学过树的相关知识这里会比较好理解
    这里给出策划的字符串模板信息:
string coordiStr = 2.517,-0.017,1.466;0,44.8,0;
string aiStr = home2_0-0-0/30-home2_1/70-home2_0;home2_1-0-0/100-home2_2,home2_0-0-0/100-home2_0;home2_2-0-0/10-home2_3/90-home2_2;home2_2-0-0/100-home2_2,home2_3-0-0/100-home2_0;

位置信息中:

  • 前三个为position,后三个为rotation

ai信息中

  • 层级按;划分
  • 同层级动画按,划分
  • 动画信息与衔接动作按/划分
  • 动画信息和衔接动作 的小节点按 - 划分

每一层中:

  • 开启动作-交互场景预制名-场景动作名/权重-衔接角色动作名/ 权重-衔接角色动作名;

// 在某个位置上时角色的所有动画,位置信息
public class TeamDeployAnimaTrie
{
    //根据动画名存储动画信息
    Dictionary<string, TeamDeployAnimaInfo> Info_Dic = new Dictionary<string, TeamDeployAnimaInfo>();
    //根节点
    TeamDeployAnimaNode root = null;
    //位置
    public Vector3 Pos;
    //旋转
    public Vector3 Rotat;


    /// <summary>
    /// 初始化时分割字符串
    /// </summary>
    /// <param name="coordiStr"></param>
    /// <param name="aiStr"></param>
    /// <param name="animator"></param>
    public TeamDeployAnimaTrie(string coordiStr, string aiStr, Animator animator)
    {
        //分解字符串
        //位置旋转
        List<string[]> coordiStrs = new List<string[]>();
        foreach (string i in coordiStr.Split(';').ToArray())
        {
            coordiStrs.Add(i.Split(',').ToArray());
        }
        Pos = new Vector3(float.Parse(coordiStrs[0][0]), float.Parse(coordiStrs[0][1]), float.Parse(coordiStrs[0][2]));
        Rotat = new Vector3(float.Parse(coordiStrs[1][0]), float.Parse(coordiStrs[1][1]), float.Parse(coordiStrs[1][2]));
        //AI树
        List<List<List<string[]>>> aiStrs = new List<List<List<string[]>>>();
        foreach(string i in aiStr.Split(';').ToArray())
        {
            aiStrs.Add(new List<List<string[]>>());
            foreach(string j in i.Split(',').ToArray())
            {
                aiStrs.Last().Add(new List<string[]>());
                foreach(string k in j.Split('/').ToArray())
                {
                    aiStrs.Last().Last().Add(k.Split('-').ToArray());
                }
            }
        }
        //获取人物所有动画资源
        if (animator == null || animator.runtimeAnimatorController == null || animator.runtimeAnimatorController.animationClips.Length < 1)
            Log.Error("动画组件为空或资源为空");
        AnimationClip[] clips = animator.runtimeAnimatorController.animationClips;
        Dictionary<string, float> times = new Dictionary<string, float>();
        foreach(AnimationClip clip in clips)
        {
            if (clip != null)
                times[clip.name] = clip.length;
        }
        CreateNodes(aiStrs, times);
    }

    /// <summary>
    /// 递归构建树
    /// </summary>
    /// <param name="aiStrs">ai树字符串分割</param>
    /// <param name="times">动画资源时长信息</param>
    /// <param name="ix">第ix层</param>
    /// <param name="jx">第jx个动画的信息</param>
    /// <returns></returns>
    TeamDeployAnimaNode CreateNodes( List<List<List<string[]>>> aiStrs, Dictionary<string, float> times, int ix = 0, int jx = 0)
    {
        if (ix >= aiStrs.Count || jx >= aiStrs[ix].Count) return null;
        //当前信息
        List<string[]> list = aiStrs[ix][jx];
        string info = list[0][0];
        //新建节点
        TeamDeployAnimaNode node;
        //找到基础信息
        if(!Info_Dic.ContainsKey(info))
        {
            float time = (times.ContainsKey(info) ? times[info] : -1);
            //找场景物件
            GameObject obj = GameObject.Find(list[0][1]);
            if (obj)
                Info_Dic[info] = new TeamDeployAnimaInfo(info, time, obj.GetComponent<Animator>(), list[0][2]);
            else
                Info_Dic[info] = new TeamDeployAnimaInfo(info, time);
        }
        node = new TeamDeployAnimaNode(Info_Dic[info]);
        if(ix == 0 && jx == 0)
        {
        	//查找根节点之所以不写在外面,是因为其他节点有可能返回根节点,所以必须在根节点构建时就赋值
            root = node;
        }

        //下一层下标
        int nextLayer = ix + 1;
        //构建子节点
        for (int i = 1; i < list.Count; ++i)
        {
            if(info == list[i][1])
            {
                // 是自己,自我循环
                node.Children.Add(new KeyValuePair<int, TeamDeployAnimaNode>(int.Parse(list[i][0]), node));
                continue;
            }

            //查找在下一层中的位置
            int index = FindIndex(aiStrs, list[i][1], nextLayer);
            if(index == -1)
            {
                //不在下一层中,查看是否是根节点
                if(list[i][1] == root.Info.RoleAnima)
                    node.Children.Add(new KeyValuePair<int, TeamDeployAnimaNode>(int.Parse(list[1][0]), root));
            }
            else
            {
                //在下一层中,则新建子节点
                node.Children.Add(new KeyValuePair<int, TeamDeployAnimaNode>(int.Parse(list[i][0]), CreateNodes(aiStrs, times, nextLayer, index)));
            }
        }
        return node;
    }
    // 查找层级中是否存在相同字符串
    int FindIndex(List<List<List<string[]>>> aiStrs, string roleAnima, int nextLayer)
    {
        if(nextLayer < aiStrs.Count)
        {
            for (int i = 0; i < aiStrs[nextLayer].Count; ++i)
                if (roleAnima == aiStrs[nextLayer][i][0][0])
                    return i;
        }
        return -1;
    }

}

3.节点移动

已经构建好所有节点后,我们只需要给出外部接口,使节点根据概率移动即可
这里写出添加进去的信息和方法:

    //当前节点
    TeamDeployAnimaNode curNode = null;
    //重置
    public void Reset()
    {
        curNode = root;
    }
    //节点向下移动
    public void MoveNode()
    {
        if (curNode == null) return;
        int rand = Random.Range(0, 100);
        if(curNode.Children.Count == 0)
        {
            Debug.Log("信息错误,不存在子节点");
            return;
        }
        TeamDeployAnimaNode randChild = curNode.Children[0].Value;
        int count = 0;
        for(int i = 0; i < curNode.Children.Count && count < rand; ++i)
        {
            count += curNode.Children[i].Key;
            randChild = curNode.Children[i].Value;
        }
        curNode = randChild;
    }

这个树结构只存储了所有信息,想要播动画需要在外部再实现控制器

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

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

相关文章

TiDB 底层存储结构 LSM 树原理介绍

随着数据量的增大&#xff0c;传统关系型数据库越来越不能满足对于海量数据存储的需求。对于分布式关系型数据库&#xff0c;我们了解其底层存储结构是非常重要的。本文将介绍下分布式关系型数据库 TiDB 所采用的底层存储结构 LSM 树的原理。 1 LSM 树介绍 LSM 树&#xff08…

测试开发基础 mvn test | 利用 Maven Surefire Plugin 做测试用例基础执行管理

一、需求在测试工作场景中&#xff0c;经常会遇到下面的问题&#xff1a;1、执行自动化测试用例的时候&#xff0c;只想指定某个测试类&#xff0c;或者某个方法&#xff0c;又或者某一类用例等&#xff0c;怎么办&#xff1f;2、想要和 Jenkins 一起进行持续集成&#xff0c;可…

C语言_文件操作(下)

目录 8. 文件的随机读写 8.1 fseek 8.2 ftell 8.3 rewind ​9. 文件结束判定 10. perror 8. 文件的随机读写 假设文件中存放的是abcdef&#xff0c;如下图&#xff0c;通常在读文件时&#xff0c;是先读取首元素地址&#xff0c;也就是文件指针指向a&#xff0c;每读一…

【Linux进程信号】

Linux进程信号技术应用角度的信号信号的发送与记录信号处理常见方式产生信号通过终端按键产生信号通过系统函数向进程发信号由软件条件产生信号由硬件异常产生信号阻塞信号信号其他相关常见概念在内核中的表示sigset_t信号集操作函数sigprocmasksigpending捕捉信号内核空间与用…

three.js 之 入门篇 5之几何体的认知( 顶点创建矩阵、炫酷三角形科技物体、基础网格材质 material )

目录three.js 之 入门篇 5之几何体的认知01BufferGeometry设置顶点创建矩阵02 生产炫酷三角形科技物体03 常见的网格几何体 geometry04 基础网格材质 material04-1 初识别材质与纹理04-2 初识别材质与纹理 &#xff08; 平移、旋转 &#xff09;04-3 纹理显示设置&#xff08; …

C语言——指针面试题详解

&#x1f412;个人主页&#xff1a;平凡的小苏&#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情目录 1. 指针和数组笔试题解析 一维数组 字符数组 二维数组 2. 指针笔试题 笔试题1&#xff1a; 笔试题2&#xff1a; 笔试题…

ffmpeg 颜色空间转换分析

颜色空间转换有很多相关标准&#xff1a; https://docs.opencv.org/3.4.0/de/d25/imgproc_color_conversions.html https://www.itu.int/rec/R-REC-BT.601-4-199407-S/en ffmpeg命令行颜色空间转换是通过调用vf_scale中的swscale来进行转码。 我们通过gdb来调试ffmpeg. 首先编译…

《Getting Started with NLP》chap11:Named-entity recognition

《Getting Started with NLP》chap11&#xff1a;Named-entity recognition 最近需要做一些NER相关的任务&#xff0c;来学习一下这本书的第十一章 文章目录《Getting Started with NLP》chap11&#xff1a;Named-entity recognition11.1 Named entity recognition: Definition…

jar转成dex文件

jar转成dex文件 dx 可以利用android studio中的dx工具。 可以看到android的tool安装位置: 在此路径下的如下目录有dx.bat,这个正是我们需要使用的工具。 D:\sdk\build-tools\30.0.3将dx.bat添加到环境变量 基本指令 > dx --dex --output 输出路径 待转化的jar包C

高性能网络模式:Reactor 和 Proactor

文章目录演进多 Reactor 多进程 / 线程Proactor总结演进 如果要让服务器服务多个客户端&#xff0c;那么最直接的方式就是为每一条连接创建线程。其实创建进程也是可以的&#xff0c;原理是一样的&#xff0c;进程和线程的区别在于线程比较轻量级些&#xff0c;线程的创建和线…

【Dash搭建可视化网站】项目12:全球恐怖主义数据大屏制作步骤详解

全球恐怖主义数据大屏制作步骤详解1 项目效果图2 项目架构3 文件介绍和功能完善3.1 assets文件夹介绍3.2 app.py和index.py文件完善3.3 header.py文件完善3.4 filteritem.py文件完善3.5 api.py文件和api.ipynb文件完善3.6 staclbarline.py文件完善3.7 piechart.py文件完善3.8 m…

IO多路复用之select、poll、epoll之间的区别总结

一、IO多路复用基本概念 select、poll、epoll都是IO多路复用的机制。IO多路复用就是通过一种机制&#xff0c;让一个进程/线程可以监视多个描述符&#xff0c;一旦某个描述符就绪&#xff08;一般是读写就绪&#xff09;&#xff0c;能够通知应用程序进行相应的读写操作。 I/…

并网逆变器学习笔记5---三电平DPWM

参考文献&#xff1a;《中压三电平全功率风电变流器关键技术研究---任康乐》 1、调制策略分析 DPWM由于其在任意时刻均有一相钳位在某个电平&#xff0c;使得该相的功率器件不发生开关动作&#xff0c;因而可以大大降低开关损耗&#xff08;平均降低1/3&#xff09;&#xff…

Java多线程案例——定时器

一&#xff0c;定时器1.定时器的概念定时器是Java开发中一个重要的组件&#xff08;功能类似于闹钟&#xff09;&#xff0c;可以指定一个任务在多长时间后执行&#xff08;尤其在网络编程的时候&#xff0c;如果网络卡顿很长时间没有响应用户的需求&#xff0c;此时可以使用定…

分享|UWB使用频段大幅收窄,新标准对于行业发展是好是坏?

近日&#xff0c;工信部无线电管理局发布了《超宽带&#xff08;UWB&#xff09;设备无线电管理规定&#xff08;征求意见稿&#xff09;》&#xff08;以下简称“新版《规定》”&#xff09;。 根据新版《规定》&#xff0c;未来国内UWB技术的使用频段为&#xff1a;7235-875…

seo的基本知识(概述网站内部优化和外部优化)

了解网站外部优化的4大重点 网站优化的时候都会重视网站的外部优化&#xff0c;所以网站外部优化的4大重点&#xff01;今天就来和大家说一说&#xff01; 1.高质量的内容和外链 未来的SEO道路高质量的有价值的内容是非常重要的&#xff0c;还有就是高质量的外链也是重要之…

北大硕士LeetCode算法专题课-查找相关问题

黑马算法面试专题 北大硕士LeetCode算法专题课-字符串相关问题 北大硕士LeetCode算法专题课-数组相关问题_​​​​​​ 北大硕士LeetCode算法专题课-基础算法查找_ 北大硕士LeetCode算法专题课-基础算法之排序_客 北大硕士LeetCode算法专题课---算法复杂度介绍_…

Neo4j框架学习之一安装和使用

文章目录1、何为Neo4j2、安装和使用2.1 安装2.2 基础概念1、何为Neo4j ​ Neo4j是一个高性能的NOSQL图形数据库&#xff0c;是一个嵌入式的、基于磁盘的&#xff0c;数据结果为网格(图)、具备完全的事务特性的Java持久化引擎。 数据结构 ​ 在一个图中包含两种基本的数据类型…

从浏览器里输入URL构建你的前端知识体系

嗨&#xff01;我是团子&#xff0c;好久不见~ 记得22年寒假复习八股的时候&#xff0c;一直在苦恼怎样才能把八股的内容真正的转换为自己的知识。毕竟光靠死记硬背每个知识点&#xff0c;是不能在面试中给面试官留下不错的印象的。后面在整理《浏览器里输入URL后发生了什么》…

Stellarium 1.2 正式发布

导读Stellarium 1.2 已发布。Stellarium 是一款免费开源 GPL&#xff08;自由软件基金会 GNU 通用公共许可证&#xff09;软件&#xff0c;它使用 OpenGL 图形接口对星空进行实时渲染。 软件可以模拟肉眼、双筒望远镜和小型天文等观察天空&#xff0c;根据观测者所处时间和位置…