动画系统已有重构版本,可前往 朝花夕拾·动画系统的重构 进行查看

前言

基于行为树的Moba技能系统系列文章总目录:https://www.lfzxb.top/nkgmoba-totaltabs/

这一篇来谈谈动画系统部分,别的先不说,先给大伙来个开幕雷击

img

相信很多读者都受过这个蜘蛛网的荼毒+迫害,事实上我并不是很能知道Unity出这个蜘蛛网有什么用,折磨我们吗?

其实我们想一想,我们真的需要这种可视化吗?

我对于这个问题的答案是否定的,在游戏开发中我们会有自己的状态系统,而动画系统往往就是与我们自己的状态系统相绑定的,根据状态来播放相应的动画,所以我们的需求就是切换状态然后播放动画,不需要知道,也不需要想象哪个动画可以切换到另一个动画,哪个不可以,这都是由我们状态系统控制的,所以完全不需要这个可视化工具,也不需要每次都一大堆的SetXXX,只需要几个能用的API就行。

所以众多开发者也是不堪其辱,干脆不用这个蜘蛛网方案,退而求其次,使用Animation自己控制播放,但是这样的话就没有办法使用Animator的新功能,比如动画重定向、Blend Tree、Avatar Mask等功能

好在官方似乎意识到自己的不足,推出了一个Playable系统,简而言之这个系统可以让我们不需要受蜘蛛网的折磨就可以享受Animator的新功能,【Playable API】不用Animator如何播放动画?

当然了,构建PlayableGraph虽说不麻烦,但是挺烦人的,幸运的是,有一个插件帮我们做好了所有的工作,我们可以真正的只关心PlayAPI了,那就是大名鼎鼎的Animancer,它基于Playable,可以运行时自动动态构建PlayableGraph,我们开发者完全不用关心,只需要准备好AnimationClip和一些必须的数据(比如你需要使用Avatar Mask,就需要准备好Avatar Mask),然后调用API就好了。

与状态系统的联动

对于Animancer的使用,这里就不多说了,看一下Animancer自带的Demo就可以秒懂,这里主要说一下和状态系统的联动设计,关于状态系统的内容我会单独写一篇文章,这里不再赘述

先来看动画组件中的两个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 管理所有的动画文件
/// </summary>
public Dictionary<string, AnimationClip> AnimationClips = new Dictionary<string, AnimationClip>();
/// <summary>
/// 运行时所播放的动画文件,会动态变化
/// 例如移动速度快到一定程度将会播放另一种跑路动画,这时候就需要动态替换RuntimeAnimationClips的Run所对应的VALUE
/// KEY:外部调用的名称
/// VALEU:对应AnimationClips中的KEY,可以取得相应的动画文件
/// </summary>
public Dictionary<string, string> RuntimeAnimationClips = new Dictionary<string, string>
{
{ StateTypes.Run.GetStateTypeMapedString(), "Anim_Run1" },
{ StateTypes.Idle.GetStateTypeMapedString(), "Anim_Idle1" },
{ StateTypes.CommonAttack.GetStateTypeMapedString(), "Anim_Attack1" }
};

AnimationClips是动画名和动画资源的映射

RuntimeAnimationClips是状态名和状态名对应的动画资源的映射,可以看到RuntimeAnimationClips字段它的Key是字符串类型,并且有几个初始数据,分别是移动,默认和攻击状态的枚举字符串(这里顺带提一嘴,对于使用枚举做Key,或者对枚举进行ToString操作都是非常消耗性能的操作,需要注意优化,参照:Unity优化记录(3)——C#(如何解决使用enum和struct作为Dictionary的TKey带来的GC)也谈用反射实现Enum→String映射:一种重视性能的方法 ,但是本Demo代码没有做。)

为什么这里要使用string,而不是直接使用状态枚举作为key呢?因为我们的状态会非常多,简单的如移动,攻击,复杂的如某些特殊状态,比如一些技能的二段,三段对应的动画不一样,所以状态也不一样,但是我们没有办法将游戏里所有的状态都用枚举表示出来,所以就选择在状态类中加一个string,作为状态的名称,然后用状态的名称,而不是枚举作为Key,这样就解决了这个问题。

嗯,基本上就这一个需要注意的点,需要切换状态然后播放动画的时候只需要进行以下调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//尝试切换到寻路状态,切换成功返回true,否则返回false
if (!ChangeState<NavigateState>(StateTypes.Run, "Navigate", 1)) return;
//因为我们切换状态成功就意味着寻路状态已经位于状态栈顶,所以可以直接使用这个API进行播放寻路动画
Entity.GetComponent<AnimationComponent>().PlayAnimByStackFsmCurrent(0.3f, animSpeed);
------------------------------------------------------------------------------------------------------------------
/// <summary>
/// 根据栈式状态机来自动播放动画
/// </summary>
public void PlayAnimByStackFsmCurrent(float fadeDuration = 0.3f, float speed = 1.0f)
{
//先根据StateType进行动画播放
if (this.RuntimeAnimationClips.ContainsKey(this.StackFsmComponent.GetCurrentFsmState().StateTypes.ToString()))
{
PlayAnim(this.StackFsmComponent.GetCurrentFsmState().StateTypes, fadeDuration, speed);
}
//如果没有的话就根据StateName进行动画播放
else if (this.RuntimeAnimationClips.ContainsKey(this.StackFsmComponent.GetCurrentFsmState().StateName))
{
PlayAnim(this.StackFsmComponent.GetCurrentFsmState().StateName, fadeDuration, speed);
}
//否则播放默认动画
else
{
this.PlayIdelFromStart();
}
}

如果我们想在不切换状态的情况下仍然播放另一个动画的话,就自己控制

1
2
3
4
//先配备好状态名以及其对应的要播放的AnimationClip名
unit.GetComponent<AnimationComponent>().RuntimeAnimationClips[playAnimInfo.StateName] = playAnimInfo.AnimationClipName;
//手动调用PlayAnim进行播放
unit.GetComponent<AnimationComponent>().PlayAnim(playAnimInfo.StateName, fadeDuration);

有时我们会有边播放技能动画,变播放移动动画的需求,使用Animancer的AvatarMask功能即可(由于Demo使用的模型在设置为人形时会出现鬼畜+骨骼错乱,就没有演示了)