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

前言

  • 今天在工作中接到一需求,要求人物摆在不同的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;
    }

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

猜你喜欢

转载自blog.csdn.net/KamikazePilot/article/details/128633369