浅谈不同骨骼的动画的动画逻辑共用
对于不同角色或者是不同职业不同性别,策划和美术都会规划不同体型,不同的骨骼和骨骼结构。不同骨骼的动画管理一直是个比较麻烦的事情。项目一般喜欢把这种动画管理的解决方案叫做动画集AnimationSet。但是实际开发中不同骨骼的情况下又会要求它们要共用动画逻辑,比如说人物的Locomotion逻辑共用,各种攀爬运动逻辑共用等,而UE并没有一个完善的共用机制。本文目的是探讨一些共用动画蓝图的方法。
Linked Graph和Linked Layer
Linked Anim Layer和Linked Anim Graph说白了就是类似函数封装,把部分动画逻辑封装到单独的内联蓝图,或者是独立的蓝图中。Linked Anim Layer是通过实现抽象接口,让被共用部分的动画蓝图实现此接口,并且在共用的动画蓝图指定Instance Class,或者在逻辑蓝图调用Linked Anim Class Layers来实现逻辑的共用;Linked Anim Graph则是直接指定被共用动画蓝图来实现共用。两者在实现共用的方面大差不差,具体使用方法可以参考 官方文档。
在面对多骨骼时这种方案在UE5中是没什么问题的,因为UE5有动画蓝图模板。被共用的动画蓝图使用动画蓝图模板则可以被不同骨骼的动画蓝图使用。而UE4没有这项特性,需要修改引擎源码,把Linked Anim Layer和Linked Anim Graph的Instance Class对骨骼不同的动画蓝图的过滤逻辑去掉。
这方案相对来说比较直接,也比较直观(UE4实现相对比较麻烦)。但是这样结构性不会很好,Linked节点有可能会被其他ABP共同维护者滥用。在面对量产型动画,如武器、跟班等骨骼都各不相同的动画,每次导入新骨骼也需要手动链接节点,繁琐且显得工业化程度不足。
动画蓝图继承
UE的动画蓝图本身是支持继承,通过继承的方法可以把共用逻辑都写在父对象动画蓝图中。然后不同骨骼把此动画蓝图作为父对象,并“替换”掉父对象上动画节点所播放的动画为自身骨骼的动画。这种“替换”有各种方案,比较简单直接的就是使用UE自带的Anim Graph Overrides方案,或者父对象使用AnimationSequence变量作为Play Animation Sequence输入,如图:
上图是使用变量作为动画输入,下图则是继承后子蓝图的Anim Graph Overrides
在面对不同骨骼的时候,UE4和UE5都可以直接通过Reparent的方式重新继承到骨骼不一样的父动画蓝图上,并且通过父动画蓝图实现所有动画逻辑。不过UE4有个问题,Anim Graph Overrides指定的覆盖动画时,编辑器会过滤非本骨骼的动画,导致无法选择覆盖的动画资源。需要修改一下引擎源码,过滤代码如下图,可以直接整个函数return false来解决。
UE5因为有动画蓝图模板,所以父动画蓝图设置成动画蓝图模板就不需要如此处理了。
这种方案也有不少问题,其中一个是调试子动画蓝图时,因为子动画蓝图的Anim Graph是没法编辑预览,只能调试父动画蓝图的Anim Graph。而调试父动画蓝图的Anim Graph时,状态机状态,变量的值,动画的流程线都是错的,这是UE的一个Bug,目前在UE4中必现。
另外因为是子动画蓝图的Anim Graph是无法编辑的,所以动画蓝图无法多层继承,也就是说子动画蓝图无法在自己子对象里实现自己的特殊逻辑,只能把逻辑都堆在父动画蓝图里并通过开关的形式来实现动画逻辑差异化。
子动画蓝图只有EventGraph,没有Anim Graph
Native Anim Graph Node
这方案顾名思义就是使用C++并且通过源码来实现动画节点的链接和逻辑运作,个人比较推荐这种方案,或者以这方案为主,配套上面两个方案使用,因为这样能完全掌控动画的共用和继承逻辑,解决无法多层继承问题或者是骨骼不一样导致的过滤等问题,同时也不用考虑动画没有的Fast Path问题,也就是性能问题。同时也不用忍受动画蓝图的调试的各种问题(甚至动画蓝图也可以打Log了),调试可以自己完全掌控。当然缺点是没有用动画蓝图来做节点连接那么直观,迭代效率也没有蓝图节点高。
要使用此方案,首先要创建一个继承于UAnimInstance的自定义AnimInstance类,再创建一个继承于FAnimInstanceProxy的动画代理类。AnimInstance让动画蓝图继承,AnimInstanceProxy在创建Proxy时替换掉默认的类,如下:
1
2
3
4
FAnimInstanceProxy* UWeaponAnimInstance::CreateAnimInstanceProxy()
{
return new FSMGWeaponAnimInstanceProxy(this);
}
这里稍稍提及一个事,AnimInstanceProxy框架是支持自己实现动画Evaluate的,也就是说你可以不使用节点,不用动画蓝图来实现自己的动画逻辑,同时也能借助它的多线程框架和Skeleton框架的优势,但这个相当于自己开发动画运动实现了。见源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// "Animation/AnimInstanceProxy.h"
// Evaluate native code if implemented, otherwise evaluate the node graph
void FAnimInstanceProxy::EvaluateAnimation_WithRoot(FPoseContext& Output, FAnimNode_Base* InRootNode)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_FUNC()
ANIM_MT_SCOPE_CYCLE_COUNTER(EvaluateAnimInstance, !IsInGameThread());
if(InRootNode == RootNode)
{
// Call the correct override point if this is the root node
CacheBones();
}
else
{
CacheBones_WithRoot(InRootNode);
}
// 此处可见,实现Evaluate_WithRoot并返回true即可自行实现动画Evaluate,否则走动画蓝图逻辑
if (!Evaluate_WithRoot(Output, InRootNode))
{
EvaluateAnimationNode_WithRoot(Output, InRootNode);
}
}
要实现动画逻辑共用,有点类似于类继承逻辑:先调用父对象逻辑,再调用子对象逻辑;或者先调用子对象逻辑,再调用父对象逻辑。我这里取后者,实现方法是在执行完子动画节点后,插入父对象添加的节点逻辑。下面是一个武器动画的例子:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void FSMGWeaponAnimInstanceProxy::InitializeObjects(UAnimInstance* InAnimInstance)
{
FAnimInstanceProxy::InitializeObjects(InAnimInstance);
if (!HasInitNode)
{
auto Root = CastStruct<FAnimNode_Root>(GetRootNode());
if (Root != nullptr && Root->Result.GetLinkNode() != &Node)
{
HasInitNode = true;
//在根节点插入节点Node
auto LastLink = Root->Result.GetLinkNode();
Root->Result.SetLinkNode(&Node);
FireSlotNode.SlotName = "Fire";
FireSlotNode.Source.SetLinkNode(&AdditiveIdentityNode);
AdditiveIdentityNode.RefPoseType = ERefPoseType::EIT_Additive;
ApplyAdditiveNode.Base.SetLinkNode(LastLink);
ApplyAdditiveNode.Additive.SetLinkNode(&FireSlotNode);
Node.BlendTime.Reset();
Node.BlendPose.Reset();
Node.BlendTime.Add(0.1f);
Node.BlendTime.Add(0.1f);
new (Node.BlendPose) FPoseLink();
new (Node.BlendPose) FPoseLink();
if (USMGWeaponAnimInstance* AnimInstance = CastChecked<USMGWeaponAnimInstance>(InAnimInstance))
{
Node.bActiveValue = AnimInstance->bReloading;
ReloadPlayerNode.Sequence = AnimInstance->ReloadAnim;
}
Node.BlendPose[0].SetLinkNode(&ReloadPlayerNode);
Node.BlendPose[1].SetLinkNode(&ApplyAdditiveNode);
//所有节点都需要手动初始化
FAnimationInitializeContext Context(this);
FireSlotNode.Initialize_AnyThread(Context);
AdditiveIdentityNode.Initialize_AnyThread(Context);
ApplyAdditiveNode.Initialize_AnyThread(Context);
ReloadPlayerNode.Initialize_AnyThread(Context);
Node.Initialize_AnyThread(Context);
}
}
}
这里的代码是实现了武器的Reload和Fire,Fire是叠加动画,而Reload是覆盖独立动画。节点代码我放在InitializeObjects中执行而不是Initialize执行是因为初始化时Root节点的LinkPose还是空的,而要注意InitializeObjects是循环执行,需要添加一个已初始化标志。
为了更好解释例子,我这里做了一个动画蓝图,上面C++代码是跟下面动画蓝图等价的:
不过InputPose的输入是子类的动画蓝图,这样能方便子类动画蓝图实现自己不一样的动画。
另外节点的Expose出来的Pin,像是例子的Reloading,也需要在Proxy的Update函数中进行赋值。
个人是比较推荐在逻辑已经基本定型,结构不会大改的情况下,使用Native Anim Graph Node,或者把已经定型的共用动画节点逻辑,翻译成C++代码。这样既能实现较为灵活的共用,性能也不需要担心。但是如果动画逻辑包含大量的状态机转换逻辑,Native Anim Graph Node不是一个很好的选择,如果共用也是刚性需要,考虑自己开发一个C++的状态机代码框架管理复杂的状态转换逻辑。