Unreal RootMotion使用
RootMotion介绍Unreal对RootMotion的介绍
看了unreal的介绍应该可以明白RootMotion是什么,以及如何在项目中使用的了。这里就不再多说,下面主要说说自己在项目中对RootMotion使用上,解决的一些问题和经验(如有错误或者不恰当的地方欢迎指正)。
项目技术背景
简单说明一下方便解释后面问题与方案由来。
[*]项目中的程序控制播放的动画最终会以Montage的方式播放。
[*]项目中没有使用DedicatedServer。因此对于RootMotion过程中的同步上需要自己处理。
为什么使用RootMotion
对于这个问题很好回答,为了提升表现,希望在游戏过程中美术设计的动画和位移可以完美配合。因此为了表现的更好,我们在战斗中的技能都是使用RootMotion驱动位移。
在这么做之前我也找了一些文档,觉得这么做还是有风险的。网上看到很多的做法都是单机游戏才会这么做,很多人会选择将RootMotion数据导出曲线的方式,然后由曲线驱动位移。这里只讨论技术,就不去评论哪个方案更好了。
程序决定RootMotion的使用
原因是没有使用DedicatedServer,因此同步上需要处理,基于对同步上比较友好的方法,程序会在运行时会修改动画的RootMotion的EnableRootMotion属性,来达到跟服务器同步。
注:这里好的方案应该是做一个提交检查,我们需要使用RootMotion的动画会分好文件夹,做到提交前检查的好处是为了让团队内所有人对齐信息,知道哪些是需要开启RootMotion并由其驱动位移的。另外,程序修改动画资源的设置还是难免会产生沟通成本(虽然规范是制定了...)。伪代码思路(有好的思路的大佬可以评论下~):
UAnimMontage* Montage;
bool bRootMotion;
if (FAnimMontageInstance* MontageInst = GetActiveInstanceForMontage(Montage))
{
// 程序控制RootMotion的锁 详见USkeletalMeshComponent::IsPlayingNetworkedRootMotionMontage()
if (bRootMotion && MontageInst->IsRootMotionDisabled())
{
MontageInst->PopDisableRootMotion();
}
else if (!bRootMotion && !MontageInst->IsRootMotionDisabled())
{
MontageInst->PushDisableRootMotion();
}
AnimNode->MontageInstance = MontageInst;
PlayMontageSection(AnimNode, Montage_GetCurrentSection(Montage), 0.f, Mode);
}
RootMotion缩放、补偿、叠加等
原则上使用RootMotion预期能够反馈到最好的美术设计表现状态。实际在游戏过程,策划会有很多战斗打击感设计,为了更好的打击感,策划期望的功能大概需要对RootMotion驱动的位移进行缩放、补偿、叠加等。这些可以使用分段动画去做,但无疑增加了动画师和策划的工作(如果能管理好应该也是可行的,策划如果可以预先想好技能的整个效果并设计好分段给动画师)。
计算当前的RootMotion数据流程为(伪代码):大概分下面三个步骤,注释写的也比较清楚。
[*]Clean up invalid RootMotion Sources.
[*]Prepare Root Motion (generate/accumulate from root motion sources to be used later).
[*]Apply Root Motion to Velocity.
void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)
{
...
// 1 - Clean up invalid RootMotion Sources.
// This includes RootMotion sources that ended naturally.
// They might want to perform a clamp on velocity or an override,
// so we want this to happen before ApplyAccumulatedForces and HandlePendingLaunch as to not clobber these.
const bool bHasRootMotionSources = HasRootMotionSources();
if (bHasRootMotionSources && !CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion)
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementRootMotionSourceCalculate);
const FVector VelocityBeforeCleanup = Velocity;
CurrentRootMotion.CleanUpInvalidRootMotion(DeltaSeconds, *CharacterOwner, *this);
}
...
// 2 - Prepare Root Motion (generate/accumulate from root motion sources to be used later)
if (bHasRootMotionSources && !CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion)
{
// Animation root motion - If using animation RootMotion, tick animations before running physics.
if( CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh() )
{
TickCharacterPose(DeltaSeconds);
}
}
...
CurrentRootMotion.PrepareRootMotion(DeltaSeconds, *CharacterOwner, *this, true);
...
// 3 - Apply Root Motion to Velocity
if( CurrentRootMotion.HasOverrideVelocity() || HasAnimRootMotion() )
{
// Animation root motion overrides Velocity and currently doesn't allow any other root motion sources
if( HasAnimRootMotion() )
{
// Convert to world space (animation root motion is always local)
USkeletalMeshComponent * SkelMeshComp = CharacterOwner->GetMesh();
if( SkelMeshComp )
{
// Convert Local Space Root Motion to world space. Do it right before used by physics to make sure we use up to date transforms, as translation is relative to rotation.
RootMotionParams.Set( ConvertLocalRootMotionToWorld(RootMotionParams.GetRootMotionTransform()) );
}
// Then turn root motion to velocity to be used by various physics modes.
if( DeltaSeconds > 0.f )
{
AnimRootMotionVelocity = CalcAnimRootMotionVelocity(RootMotionParams.GetRootMotionTransform().GetTranslation(), DeltaSeconds, Velocity);
Velocity = ConstrainAnimRootMotionVelocity(AnimRootMotionVelocity, Velocity);
}
}
}
...
}
里面的细节可以参照源码自己调试,流程清楚了其实修改起来就比较方便了。
RootMotion同步
带有RootMotion的动画会导出相应的曲线数据到服务器,然后服务器根据数据驱动动画位移。
同步RootMotion的基本思路为:服务器同步消息中有动画名、动画时间点、位置、转向等信息,利用这些信息计算出当前时间点客户端应该在的位置,然后进行插值同步,插值的方法可以自己选择。我这边是把差异的信息在之后的位移中分帧处理。对于RootMotion的缩放、补偿、叠加等同步上,与服务器采用同样的算法达到模拟一致。
一个有趣的问题
为了优化项目
[*]动画组将RootMotionMode由Root Motion from Montages Only更改为Root Motion from Everything。
[*]引擎组开启了URO(Update Rate Optimization)
两个修改产生了一个结果:开启URO的角色RootMotion计算不准确。原因是计算RootMotion时跳过了一些帧(由于URO引起的)。具体可以参考函数
FAnimUpdateRateManager::TickUpdateRateParameters(USkinnedMeshComponent* SkinnedComponent, float DeltaTime, bool bNeedsValidRootMotion):
个人总结:联机项目中,若使用DedicateServer,建议使用Root Motion from Montages Only(官方建议),若使用自己的服务器。使用前后端公共代码加上曲线驱动来的实在,可以自己把握,比较实在稳妥。当然,如果能对unreal理解较深,完全可以在客户端使用RootMotion驱动(决定的是我们项目的大佬)。单机项目完全可以自由使用,效果还是很不错的。
RootMotion应用至Z轴
仔细阅读官方文档会发现,只有在MovementMode为MOVE_Flying的时候才能应用完整的RootMotion。文档上是这么介绍的。
在根运动(Root Motion)期间,角色的物理状态将被加以考虑。例如,如果角色物理状态是行走或掉落,则忽略根运动(Root Motion)的Z轴,并应用重力。角色将掉落,下坡或上楼梯。如果角色物理状态是飞行,则应用完整的根运动(Root Motion),并忽略重力。如果游戏中的几个特殊动作这么使用,可能也不会用到下面介绍的一些处理方案,不过我们游戏对于RootMotion应用比较广,技能都应用了RootMotion。在设计技能的时候做了针对位移的效果处理的事件通知(我们自己设计了技能编辑器,之后我会把技能编辑器的设计思路整理下,这里可以理解为在技能过程中的事件通知)。
因此,我们在处理高低差的时候做了一些处理(针对玩法功能)仅供参考。
[*]RootMotion过程中持续某帧/某段动作直到落地,可以应用到下劈/下锤攻击(类似永劫无间中的下劈攻击)。
[*]RootMotion过程叠加向下的“重力”,为了实现快速下落,在RootMotion在Z轴方向上没有数值时表现的更好,表现更不会受地形影响而看起来比较奇怪。
[*]RootMotion过程中放慢一些过程(对应上面对RootMotion的缩放),增加战斗中的打击感/连击率。
其他
[*]除了以上介绍的这些,unreal本身也开发了像MotionWarping这样的插件供大家使用,功能完全可以实现对RootMotion的各种修改。MotionWarping本身是UE5开发的插件,不过迁移到UE4中也是可以用的。假如不想迁移或者不使用DS,MotionWarping本身的设计思路也可以学习。
页:
[1]