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

今天也是冒着猪脑过载的风险想出了这么个像那么回事的标题,我命令看这个文章的所有人都狠狠的夸标题

2021.6.1,随着我战斗系统系列文章的发布,初版的动画系统也是第一次进入了大家的视野,文章中阐述了Unity动画状态机的缺陷,以及使用Playable API的理由,并且最后使用了 Animancer 作为Playable API的封装,然后在技能配置时进行指定一些动画过渡,Avatar,混合相关的参数

image-20221105104436280

随后在代码里以这种方式进行处理

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
27
public AnimancerState PlaySkillAnim(string stateTypes,
PlayAnimInfo.AvatarMaskType avatarMaskType = PlayAnimInfo.AvatarMaskType.None,
float fadeDuration = 0.25f, float speed = 1.0f, FadeMode fadeMode = FadeMode.FixedDuration)
{
AnimancerState animancerState = null;
// 当目前的状态为Run时才会考虑Avatar混合
if (avatarMaskType == PlayAnimInfo.AvatarMaskType.AnimMask_DownNotAffect &&
this.StackFsmComponent.GetCurrentFsmState().StateTypes == StateTypes.Run)
{
animancerState = AnimancerComponent.Layers[(int) avatarMaskType]
.Play(this.AnimationClips[RuntimeAnimationClips[stateTypes]], fadeDuration, fadeMode);
this.Avatar_UpOnlyAnimState = animancerState;
}
else // 否则直接按无AvatarMask播放
{
animancerState = PlayCommonAnim_Internal(stateTypes, PlayAnimInfo.AvatarMaskType.None, fadeDuration,
speed, fadeMode);
this.Avatar_NoneAnimState = animancerState;
}
m_SkillAnimInfo.LayerIndex = (int) avatarMaskType;
m_SkillAnimInfo.SkillAnimancerState = animancerState;
m_SkillAnimInfo.SkillAnimancerState.Events.OnEnd = () =>
{
m_SkillAnimInfo.SkillAnimancerState.StartFade(0, 0.1f);
};
return animancerState;
}

最终构建出战斗中的动画系统。

只能说,确实能用,但是也仅仅是能用,其实不难发现这样几个问题

  1. 各个动画过渡时间的配置离散且不直观,无法直观得知不同动画之间的过渡时长
  2. 上层数据和逻辑配置需要关注动画所使用的AvatarMask这种底层逻辑,这是很可怕的事情,目前是只有AvatarMask,后续如果需要BlendTree等其他动画相关功能,都需要上层来关心,只能说完美违背了开闭的设计原则
  3. 无法实时预览效果,需要每次进游戏才能看到配置数据的表现

这几个问题其实当时我也意识到,但一个是时间问题,一个是确实没什么好的想法来解决这些问题,就暂且搁置了

恰巧前阵子同好 @无双草泥马 发布了 基于Playable API结合蓝图驱动的动画方案 ,学习之后,倍受启发,脑海中所有的动画系统相关技术全都被串联起来,感觉似乎已经掌握了真相,所以也是迫不及待地写下这篇文章和大家分享下我所理解和实现的动画系统

方案制定 && RoadMap

  1. 基于已有的节点编辑器框架继承出一套动画可视化配置工具,并支持导出二进制数据供运行时数据驱动控制动画播放
  2. 基于Animancer封装的Playable API再封装业务层动画播放API,之所以选择Animancer是因为它作为动画播放API是非常优秀的,在下文的Timeline拓展中也会提到这一点
  3. 配合已有的状态管理系统对动画切换进行逻辑控制
  4. 基于Animancer提供的动画编辑预览功能开发周边工具用于实时预览动画配置效果
  5. 基于GraphView制作Playable Debug工具
  6. Timeline编辑形式的拓展,并非指Unity官方的Timeline插件,而是泛指所有类型的Timeline编辑器

整体架构如下

动画配置可视化工具开发

设计的大致思路是,一个单位所有动画配置为一个Graph,主要有两种节点

  • 动画配置节点,例如过渡配置,AvatarMask配置,等其他个性化配置(例如速度配置)
  • 配置收集节点,表示一个状态动画的所有配置,即搜集输入到此节点所有配置

image-20221105111324506

配置好之后,直接进行导出即可

image-20221105111606430

过渡节点开发思路

动画过渡情况分为两种情况,假设当前播放动画为A,那么一种过渡是A->B,一种过渡是B->A,所以过渡节点也有两类配置

特殊的,如果只想为某些特定状态配置过渡时长,则可以使用Any关键字配置一个默认的过渡时间,可以省去很多配置量

动画在过渡时优先从过渡发起方读取配置进行设置,例如A->B,则优先从A的过渡配置中取得A->B的过渡配置,如果取不到,再去B的过渡配置中取得A->B的配置

Avatar Mask节点开发思路

一般而言,用到AvatarMask的场景需求是这样的:如果处于某个特定状态(比如Idle),则全身播放指定动画,如果处于某个特定状态(比如Run)则只在动画配置的AvatarMask上播放

AvatarMask的配置思路有两种,这里以动画A需要使用UpperBody AvatarMask为例(即动画A只在上半身播放)

  1. 直接配置动画A需要使用UpperBody AvatarMask,用来保证A只在上半身播放,但是需要额外配置哪个状态会全身播放动画A
  2. 只配置某个特定状态(B State)使用动画A相反的AvatarMask(B AvatarMask),意为当播放A时,如果播放了B,则对B使用B AvatarMask进行播放,在这个例子中就是配置Run状态使用DownBody AvatarMask(即Run动画只在下半身播放)

两种方式都可以,当然逻辑上实现起来的方式是完全不一样的,配置上而言,第二种方式需要配置的内容更少,并且更加符合直觉,所以我们选择第二种配置方式

速度控制节点开发思路

游戏中动画速度往往与各种属性进行绑定,例如移动动画速度和移速绑定,攻击动画速度和攻速进行绑定,那么我们可以把这些属性绑定配置出来,运行时做解析设置动画速度

其他拓展

目前只演示了三种常用节点,其余的像BlendTree的配置一样可以单独做个节点进行配置,然后进行数据导出,最终在运行时进行数据解析对相应动画进行设置

思路总结

可以看到,动画配置可视化的核心思路就是尽可能将所有动画播放相关的配置都集中起来,减少业务层心智负担

运行时动画系统设计

由于我们上一步导出的是纯数据,所以需要在运行时开发一套配置解析流程做数据驱动,我使用了常见的流水线模式对配置进行解析并应用

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
27
28
29
AnimancerPipelineExecutor animancerPipelineExecutor =
ReferencePool.Acquire<AnimancerPipelineExecutor>();
// 预处理,数据准备
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<PrepareBaseDataProcessor>());

if (result.AvatarControlSets != null)
{
// 如果配置了AvatarMask,则进行设置
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<AvatarControlProcessor>());
}

// 应用自己的AvatarMask
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<AvatarMaskApplyProcessor>());
// 应用过渡
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<TransitionSetProcessor>());
// 播放
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<FinalPlayProcessor>());

if (result.SpeedControl != null)
{
// 速度控制
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<SpeedControlProcessor>());
}

// 后处理,数据刷新
animancerPipelineExecutor.AddProcessor(ReferencePool.Acquire<PostProcess>());

// 执行
animancerPipelineExecutor.Execute(this, stateName);

AvatarMask功能验证

上面提到的AvatarMask配置其实实现起来颇为复杂,因为整个AvatarMask应用过程中会有很多情况

所以我们尤其需要注意在整个流水线配置过程中保留一些上下文数据,用于动画状态迁移和重置,来确保正确的表现

下面依次演示效果

Idle->Run

Idle_run

Idel->Skill

Idle_skill

Run->Skill->Idle

run_skill

Skill->Run->Idle

skill-run

Skill-Run(Interrupt)-Idle

重要,此环节是验证AvatarMask系统的关键一步,它的打断处理最为复杂,Skill打断Idle,Run融合Skill,停止播放Run,恢复Idle

skill-run(inter)-idle

技能配置

框架层功能都开发完毕,剩下的就是配置层的内容了,非常的简单,配置一个StateName即可

image-20221105145305459

Timeline拓展

现代的游戏开发中Timeline是一种非常常见的编辑模式,由于其简单直观易于配置和预览的特性深受策划,美术的喜爱,策划喜欢用它作为技能编辑器,美术喜欢用它作为CG编辑器

不管是技能编辑器还是CG编辑器,动画编辑和预览都是非常重要的一环,所以大家也大都选择现成的编辑器工具,例如Unity官方的Timeline插件,以及下文重点介绍的精品插件商ParadoxNotion出品的Slate插件,这些现成的插件都提供了完善的动画编辑和预览工具

当然也有一些团队由于老项目的积累沉淀或者出于高度个性化的定制需求,有一套自己的Timeline形式的编辑工具,那么这种时候,动画系统能否和自研的Timeline编辑器工具良好的结合起来是一个无法忽视的重要问题,而如果我们使用Animancer作为底层动画播放工具,可以非常方便的进行动画编辑和预览,核心代码如下,非常简单直接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected override void OnEnter()
{
if (track.Animancer != null)
{
// 使用Track绑定的Aniancer组件进行动画播放,可设置Blend FadeIn/Out,AvatarMask等参数
track.Animancer.Play(animationClip);
// 暂停PlayableGraph的自动播放,手动进行Tick,方便单帧预览
track.Animancer.Playable.PauseGraph();
}
}
protected override void OnUpdate(float time, float previousTime)
{
if (track.Animancer != null)
{
// 计算出deltaTime,为正则前进播放,为负则往后退播放
track.Animancer.Evaluate(time - previousTime);
}
}

Slate

在见识过Unity Timeline的恶心至极的各种潜规则和小Bug以及距离工业生产还有很长一段路要走的基建后,Slate编辑器是我非常钟爱的的一个Timeline编辑器

Slate本身非常强大,可以用作CG编辑器,也可以基于其界面拓展Timeline形式的技能编辑器

不过其实对于Slate来说,拓展支持Animancer没有什么必要,因为它有写好的AnimatorTrack,也是基于Playable进行开发的,如果还是要基于Animancer进行个性化定制的话也很简单,参照以下文件进行重写即可

1
2
3
4
5
6
7
ParadoxNotion
----SLATE Cinematic Sequencer
--------Directables
------------Tracks/Runtime/AnimatorTrack.cs // Track逻辑
------------Tracks/Runtime/AnimatorTrack_5_6.cs // Track逻辑
------------Tracks/Runtime/AnimatorTrack_2017.cs // Track逻辑
------------Clips/Runtime/AnimatorTrack/PlayAnimatorClip.cs // Clip逻辑

image-20221106152814353

总结 && 展望

可以看到,把所有动画相关内容通过可视化配置内聚到动画模块后,业务层仅仅需要填写一个状态名即正常的进行动画的过渡和播放,非常的方便

现代化的动画系统

值得一提的是,本文所实现的动画系统在应对超级复杂的人物控制器动画时会非常力不从心,比如 神秘海域4的物理动画GDC分享星球大战的物理动画GDC分享,因为这种等级的动画系统不是简单的动画切换,过渡,混合所能满足的,需要有大量的参数和Root Motion,IK,Ragdoll,基于物理的混合等需要支持。

当然了,因为目前这套动画系统也是基于可视化节点的,这些复杂的动画系统需求要做也能做,只不过不管是编辑器还是运行时都要进行超大规模的重写,而且这种量级的动画系统不是一两个月能搞定的,甚至需要1,2年进行沉淀,我就不在这夜郎自大,自欺欺人了。比如一个IK系统的加入,就需要引入一个Post Task Node的概念

image-20221106105701081

一个Ragdoll的加入,就额外需要为Ragdoll做一系列适配

image-20221106110015636

更全面一点的看待整个动画系统,这里以一个简单的跳跃 + 落地动画为例:

需求是在跳跃的不同阶段需要有不同的动画融合表现,并且动画会根据跳跃距离进行适配

image-20221106103638180

那么就需要在动画蓝图中配置出这些逻辑

image-20221106103836046

这还只是个很简单的跳跃动画需求,没有涉及到空中2段跳,空中受击,空中技能,甚至是不同的跳跃距离会有不同的落地动画混合等

说到这里,想必有聪明的朋友已经知道动画系统最终的解决方案是什么了

那就是可编程的动画蓝图系统,通过蓝图强大的可视化编辑和协同编辑功能,可以让整个动画系统和业务配合的严丝合缝,架构就像下面这张图所示(来自 Animation Graph,非常推荐大家去学习,可以对工业化的动画蓝图系统有个大概的认知)

image-20221106101305516

由于我的项目用不到这么复杂的动画系统,所以目前就停留在够用就行的阶段,大家有兴趣的话可以自己参考这些资料去实现一套强大的,且具有普适性的动画系统

结语

可能大家看文章觉得整套动画系统开发起来如何如何复杂,但其实核心点就是动画蓝图的思想,其余的都是数据驱动逻辑的体力活,唯一要注意和小心的就是AvatarMask的处理

此外这套动画蓝图思想是非常上层的实现,并不局限于Animancer,Animancer在这套框架里担任的仅仅是一个底层播放动画的API而已,你也可以自己基于Playable API从零开始封装一个"Animancer",甚至你可以基于Unity状态机来实现一个"Animancer",都没有问题

引用

初版的动画系统

Animancer

基于Playable API结合蓝图驱动的动画方案

playable visualizer with GraphView

UE的动画蓝图系统

Animation Graph

神秘海域4的物理动画分享

星球大战的物理动画GDC分享