自虚幻引擎5发布以来,Epic 官方提供了很多实用的工具来处理动画资产,在初学者项目 Lyra 中不仅可以看到这些工具的具体应用,还提供了一个进阶版本的动画逻辑解决方案(相较于第三人称射击模板附带的动画蓝图)。本博文主要讨论这套方案的以下几个要点:
- 如何修改动画序列数据以满足该解决方案的资产要求;
- 如何减轻游戏主线程的负担,让动画逻辑不成为性能瓶颈;
- 如何在运行时灵活地替换部分动画,例如玩家装备武器时,将无持械的动画替换为该武器对应的动画。
为了使这套解决方案可以正确工作,以及方便动画资产的处理,需要项目启用Animation Locomotion Library
、Animation Warping
、Animation Modifier Library
插件。本解决方案对应的角色旋转设置如下:
// 角色的朝向跟随 ControllRotation
bUseControllerRotationYaw = true;
const auto& CharMovement = GetCharacterMovement();
CharMovement->bOrientRotationToMovement = false;
动画序列资产
在 Lyra 中,角色的地面运动主要分为站立步行(Walk)、站立奔跑(Jog)和蹲下行走(Crouch)三种姿态,每种姿态都有空闲(Idle)和前后左右的开始(Start)、运动(Cycle)、结束(Stop)、枢轴(Pivot)动画。在开始、结束和枢轴动画中要求资产有名为 Distance 的曲线,标志着在当前帧在动画里移动的距离。这会在运行时通过计算真实运动的距离来调整序列的播放进度,比如角色停下需要往前走 100 厘米才能将速度大小降为零,在动画蓝图中需要找到 100 厘米对应的帧是多少然后从那里开始播放,还会在每帧根据移动距离随时调整播放进度。枢轴主要处理在运动状态下加速度与速度方向角度大于九十度时的情况,例如在往前走的状态中,玩家输入了往后走时,需要表现出先成往前跑的姿势停下来,然后再开始往后跑的姿势。
在制作这样的曲线前,需要先定义一个Curve Compression Settings
,并设置编码方式为 Uniform Indexable,这是虚幻引擎在文档中的要求,以确保可以在运行时访问到曲线信息。具体是在内容浏览器中按下鼠标右键,依次选择 Animation / Advanced / Curve Compression Settings,双击打开新资产为 Codec 设置 Uniform Indexable。所有需要使用曲线信息的动画序列里,Asset Details / Compression / Curve Compression Settings 赋为新建的压缩设置。对于多个动画序列资产,可以选中它们然后右键 Asset Actions / Edit Selection in Property Matrix,以实现批量修改。
制作这样的曲线,并不需要我们手动绘制。在动画序列编辑器中 Window 菜单下有一个 Animation Data Modifier 选项,点击它就会出现一个窗口,在这个窗口下可以借助一系列的修改器生成符合满足需求的曲线、轨道(Track)和通知(Notify)。其中就有一个DistanceCurveModifier
,它就可以自动生成我们需要的曲线。在 Asset Details 中勾选Enable Root Motion
选项后就可以右键应用修改器了。同样如果多个序列都需要这样的设置时,可以先用上文提到的 Property Matrix 来勾选Enable Root Motion
后,对这些右键选择 Animation Modifier(s) / Add New Modifiers 来添加和应用修改器。
除了创建 Distance 曲线,推荐进一步应用SyncMarkerAnimModifier
,它会创建一个FootSyncMarkers
轨道并在里面创建若干个标记为 L 和 R 的通知,后期在动画蓝图中选择同步组后,就可以实现不同动画之间过渡时能匹配姿势,产生连贯地动画。
除了地面运动外,还有一些跳跃有关的动画,分别是跳跃开始(JumpStart)、跳跃开始循环(JumpStartLoop)、跳跃到极点(JumpApex)、下落循环(FallLoop)以及下落着陆(FallLand)。可以为这些资产加上 DistableFootIK
的曲线,确保腿部不会尝试寻找可以落脚的位置。而对于下落着陆动画中还需要添加一个GroundDistance
的曲线,表示每一帧到地面的距离,在实际下落时可以计算到地面的距离值来调整播放进度。这同样是通过DistanceCurveModifier
实现的,在配置中设置轴(Axis)为Z
即可。
动画蓝图与动画层
与本博文讨论话题有关的动画相关类有三个,它们分别是:
ABP_Mannequin_Base
:派生自 AnimInstance,作为角色骨骼网格组件的动画蓝图类。获取和计算 AnimGraph 使用的变量。而 AnimGraph 作为一个总的动画框架,包含运动(Locomotion)状态机、相加姿势(Additive Pose)融合、根旋转(Root Rotate)以及一些蒙太奇槽位的连接。但这里并没有具体的动画姿势,所有需要动画姿势的位置都连接了动画层接口(AnimLayersInterface)中的一个动画层。ALI_ItemAnimLayers
:作为一个动画层接口,里面定义了一系列姿势输入与输出的动画层。可以这么思考,动画层相当于接口类中的虚函数,但必须要返回一个姿势,是否有输入姿势或其他变量则作为可选项。继承了这个接口的动画蓝图类可以使用动画层(类似调用)作为节点的输入。也可以实现接口,在运行时动态的链接(Link)到使用动画层的动画蓝图。在本例中ABP_ItemAnimLayersBase
实现了所有的动画层,输出姿势给ABP_Mannequin_Base
使用,后者使用动画层作为自己 AnimGraph 中一些节点的输入。ABP_ItemAnimLayersBase
:派生自 AnimInstance。作为 ALI_ItemAnimLayers 的一个实现类,对于接口中的每个动画层都提供了有效的输出。它作为一种基类存在,将具体的姿势暴露为一个个变量,而派生类只需要为这些变量赋值即可。例如,ABP_PistolAnimLayers
派生类只是给变量赋值,提供了角色装备手枪时的站立、运动、跳跃等动画。
Lyra 并不是一个专注于射击的游戏,它支持使用GameFeature
来增加更多的游戏玩法,官方示例中除了死斗、占点模式外,还有一个玩法完全不一样的炸弹人(类似QQ堂)模式,也允许开发者增加更多的玩法。因此,ABP_Mannequin_Base
动画蓝图中获取与计算的变量较为基础,一些与射击模式有关的变量获取与计算工作留给了ABP_ItemAnimLayersBase
来完成,后者与射击模式有关。如果游戏项目专门是射击玩法的话,可以考虑将ABP_ItemAnimLayersBase
中计算变量的逻辑迁移到ABP_Mannequin_Base
,使前者可以专注于生成姿势的逻辑。
打开动画蓝图时,会发现 EventGraph 没有任何逻辑在执行。原先会在BlueprintUpdateAnimation
中进行的逻辑,现在在BlueprintThreadSafeUpdateAnimation
完成了,后者在逻辑中会利用多线程来分摊数据的计算,减轻了游戏主线程的负担。在这个函数中需要注意以下三点:
- 获取外部函数和变量的值使用
PropertyAccess
节点来访问。据虚幻引擎官方介绍,该节点会在 Update 函数执行之前会对相关的变量进行缓存,读取缓存时的数据,这样一定程度上避免了数据竞争的问题,总之被官方定义为是线程安全的访问形式。 - 在该函数中执行的函数要勾选 Advanced / Thread Safe,线程安全的函数访问外部数据时只能使用
PropertyAccess
进行。 - 如果想定义可以在
PropertyAccess
节点中选择的函数,它必须是线程安全的、没有形参,返回值的名称必须是ReturnValue
。对返回值的名称要求暂不清楚这是刻意的行为还是软件错误,如果不是这个名称则无法调用。在本例中ABP_Mannequin_Base
定义了返回UCharacterMovementComponent
实例的函数,而ABP_ItemAnimLayersBase
也需要使用这个实例,但它只能使用PropertyAccess
的形式来调用ABP_Mannequin_Base
定义的函数。
本节接下来的内容是谈论对一些变量的理解,如果不需要可以跳到下一节。
在 Lyra 的实现中并不会有空闲、步行到奔跑的渐变过渡,或者说并没有一个混合空间(BlendSpace)来处理速度从 0 到 Max Walk Speed 的过渡,而是根据用户的输入直接在一帧内直接切换到另一种速度的姿态。这是基于游戏玩法设计的需求,角色的运动组件中加速度大小的增量被设置为了 2048 cm,这意味着步行的动画还没有播放几帧就已经完全在播放奔跑的了。而步行的姿态只会在一个名为GameplayTag_is_ADS
布尔变量为 true 时播放,玩家进行精准瞄准(例如按下鼠标右键)或者静步走时会获得对应类似的标签,角色类会检查是否获得了这个标签从而改变 GameplayTag_is_ADS
的值。
这里也没有四个方向运动的渐变过渡,而是计算局部速度方向角度值来设置一个 AnimEnum_CardinalDirection
类型的枚举变量 LocalVelocityDirectionNoOffset
,里面枚举值分别是前(Forward)、后(Backward)、左(Left)和右(Right)。在设置该枚举变量的函数SelectCardinalDirectionFromAngle
可以发现,如果玩家持续向前或向后运动的时候,将会更难切换到向左或向右的动画,这减少了频繁切换动画的情况发生。而在混合空间中很难(好像不可能)在运行时调整各个采样的坐标。而为了让姿势更加自然,输出的姿势引脚接入了 Orientation Warping
节点,它会根据局部速度方向角度值来旋转角色的朝向。在浏览SelectCardinalDirectionFromAngle
函数时,需要注意的是:1. 形参 Angle 是[-180,180]的值,正面角度是 0,反面是 -180 或 180,右面是 90,左面是 -90;2. 在判断正反面使用的是 Angle 的绝对值,但判断左右面的时候用的是 Angle 原值,在复现时这里有可能会误用绝对值版本。
在UpdateCharacterStateData
函数中可以看到,角色在竖直方向(z)的速度方向是正还是反来区分是跳跃还是下落的状态。紧接着在UpdateJumpFallData
函数中可以看到在跳跃状态时,会以竖直方向速度大小的相反数除以重力Z值(重力值是负值,表示竖直向下的方向),作为从跳跃开始循环到跳跃极点两个动画状态之间的转换条件。
在UpdateRootYawOffset
函数中可以看到RootYawOffsetMode
变量的更新方法,这个变量是AnimEnum_RootYawOffsetMode
枚举类型,一共有Accmulate
、Hold
和BlendOut
三个枚举值。在外部修改这个变量时也只会是Accumulate
和Hold
之间的一个,前者会累加角色旋转体(Rotator)的 Yaw 值到RootYawOffset
值上,后者保持RootYawOffset
当前值。无论是哪一种选项,最后都会修改成BlendOut
值,意味着下一帧如果没有修改该枚举变量的话,它会从当前的值线性插值到 0。在实际运用中,会在休闲状态(Idle)中锁在Accumulate
值,这样旋转摄影机可以看到角色的各个面,因为根骨骼正在与摄影机在 Yaw 轴上进行相反的旋转。在开始状态(Start)时锁到Hold
值,以当时的旋转值播放开始姿势。在运动状态(Cycle)中没有锁住该枚举变量的值,因此根骨骼会慢慢插值回到摄影机看向的方向。在停止状态(Stop)开始锁为Accumulate
值,这样停止动画期间就可以旋转视角而不会影响角色旋转了。
LinkedLayerChanged
和LastLinkedLayer
两个变量与运行时是否链接了新动画层有关,它们的更新时机是在 LocomotionSM 状态机的OnUpdate
绑定函数中(单击状态机后在细节面板中可以找到),在内部会发现一个StartLayerNode
的动画节点引用,在那里会检查是否与之前存储的动画层引用相同,进而触发修改逻辑。关键在于这个引用节点的创建方式,实际上它指向的是 LocomotionSM / Start 中的 FullBody_StartState,在它的细节面板中可以看到Tag
条目,为它指定一个名字就可以在动画蓝图中引用它了。
最后需要了解的是,动画蓝图中大量使用了Sequence Player
和Sequence Evaluator
节点,可简单地理解为前者可以播放一个动画序列,后者在前者基础上可以更改播放起始点。为了实现动态地的更换播放的资产和调整播放时间,因此它们在细节面板中Sequence
和Explicit Time
(仅限 Evaluator)都会更改成Dynamic
形式。
对于这两个播放节点会绑定On Become Relevant
和On Update
函数:前者代表这个序列开始贡献姿势的第一帧,也就是权重值不再是 0% 了;后者就是每帧都会调用的函数。在函数内部通常对Node
形参调用Convert to Sequence Player
或 Convert to Sequence Evaluator
,从而进一步设置播放的 Sequence 以及设置 Explicit Time(仅限 Evaluator)。在上文中讲述过开始、停止和枢轴动画中加入了 Distance 曲线。对于它们来说我们可以在Sequence Evaluator
的On Update
函数中调用Advance Time by Distance Matching
节点,将当前帧与上一帧角色移动的位置差作为参数就可以转换为播放的帧起始点。
对于停止动画会使用Predict Ground Movement Stop Location
来计算出停止的位置出,得到向量长度后可以使用Distance Match to Target
节点来调整Explicit Time
。在 Lyra 中计算距离值的逻辑封装为了Get Predicted Stop Distance
函数。
在运行时链接动画层
在 Lyra 解决方案中,角色类使用的动画蓝图类是ABP_Mannequin_Base
,但从上一节的描述中已经知道这个蓝图类中没有具体的动画资产,所以直接使用只会得到 T-Pose。为了链接到实现了动画层的动画蓝图,可以使用下面的函数来动态的链接和取消链接动画蓝图:
// 参数为实现了动画层的动画蓝图类
GetMesh()->LinkAnimClassLayers();
GetMesh()->UnlinkAnimClassLayers();
但直接调用这个函数并不会进行网络同步,需要定义一个NetMulticast
函数来确保所有客户端中都可以看到这个链接函数。以下是一个示例,如果第二个参数为 true 就调用 Link 函数,否则就调用 Unlink 函数:
UFUNCTION(NetMulticast, Reliable)
void Multicast_SetMeshCompAnimLayers(TSubclassOf<UAnimInstance> InClass, bool bLinkIt = true);
可装备道具在设计时可以加入一个 TSubclassOfMulticast_SetMeshCompAnimLayers
函数以便于动态地调整姿势。