前言
- 今天在工作中接到一需求,要求人物摆在不同的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;
}
这个树结构只存储了所有信息,想要播动画需要在外部再实现控制器