添加调试面板
用大家自己的方式把调试面板加出来,方便后续的调试。我这里用Lyra的方式
/EqZeroCore/Experiences/LAS_ShooterGame_StandardHUD.LAS_ShooterGame_StandardHUD
配置一个GameFeature Action Set
/EqZeroCore/UserInterface/W_EqZHUDLayout.W_EqZHUDLayout

注意class settings 的 input 从 Default改成 Game And Menu 否则输入被挡住了额

Lyra动画蓝图是如何运作的
查看 【Lyra 角色动画重建记录】文档
如果不懂Lyra这一套
动画蓝图
/Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_Base.ABP_Mannequin_Base
/Game/Characters/Heroes/Mannequin/Animations/LinkedLayers/ABP_ItemAnimLayersBase.ABP_ItemAnimLayersBase
动画层接口
/Game/Characters/Heroes/Mannequin/Animations/LinkedLayers/ALI_ItemAnimLayers.ALI_ItemAnimLayers
两个动画蓝图
我们简单称之为Base 和 Item,他们都实现了动画层接口
只需要抄这个动画层接口。
主角色mesh配置动画蓝图Base。在beginplay的时候链接动画层Item
基础动画循环
Idle,Start,Cycle, Stop, Pivot 动画循环
动画蓝图外面简单分为Base和Item

Base的这四个先完成
Base_Idle

嵌入知识,在update中
void UEqZeroAnimInstance::UpdateRotationData()
{
// Yaw 变化量 和 速度
FRotator NewWorldRotation = Character->GetActorRotation();
YawDeltaSinceLastUpdate = UKismetMathLibrary::NormalizedDeltaRotator(NewWorldRotation, WorldRotation).Yaw;
YawDeltaSpeed = YawDeltaSinceLastUpdate / GetWorld()->GetDeltaSeconds();
WorldRotation = NewWorldRotation;
AdditiveLeanAngle = YawDeltaSpeed * 0.0375; // TODO tmp value
if (IsFirstUpdate)
{
YawDeltaSinceLastUpdate = AdditiveLeanAngle = 0.f;
// YawDeltaSpeed 不需要忽略,我们需要至少两次update来计算delta
}
}
UE左手系,如果角色面向世界坐标X轴正方向,WorldRotaion.Yaw是0,顺时针是正。
所以YawDeltaSinceLastUpdate向右转是正值
你向右转45度,让一个值RootYawOffset是 -45度,角色就能转回来。(用RotateRootBone)
所以 RootYawOffset = RootYawOffset + (- YawDeltaSinceLastUpdate)
代码中的SetRootYawOffset(RootYawOffset - YawDeltaSinceLastUpdate); 相比之下只是多了一些clamp限制。
到此我们 RootYawOffset 就理解了。他如何恢复呢,嵌入再各个状态机逻辑里面。
UpdateIdleState

混出IDLE状态的时候,TurnYawCurveValue值为0。RootYawOffsetMode模式改为累计。并且调用ProcessTurnYawCurve更新数值。
TurnYawCurveValue是什么?要看ProcessTurnYawCurve函数
void UEqZeroAnimInstance::ProcessTurnYawCurve()
{
float PreviousTurnYawCurveValue = TurnYawCurveValue;
TurnYawCurveValue = GetCurveValue(TEXT("TurnYawCurve"));
if (FMath::IsNearlyZero(TurnYawCurveValue))
{
PreviousTurnYawCurveValue = 0.f;
TurnYawCurveValue = 0.f;
return;
}
TurnYawCurveValue = GetCurveValue(TEXT("RemainingTurnYaw")) / TurnYawCurveValue;
if (!FMath::IsNearlyZero(PreviousTurnYawCurveValue))
{
float DeltaTurnYawCurveValue = TurnYawCurveValue - PreviousTurnYawCurveValue;
SetRootYawOffset(RootYawOffset - DeltaTurnYawCurveValue);
}
}
所有Turn Left Right 动画中
TurnYawWeight 1持续很长一段时间,然后快速到0
RemainingTurnYaw 旋转的度数 慢慢过度到0

Turn Yaw Curve Value = RemainingTurnYaw / TurnYawWeight,前面是除0判断
它的变化趋势是怎么样的
分母一直是1。所以看分子。在旋转的过程中,数值从旋转的角度例如90,慢启动中途快一点再慢。
然后分母突然变成0,由于有除0判断。而且分子早就是0了,所以我觉得分母的变0。没有过度意义,只是为了让数值变为0。
然后 Turn Yaw Curve Value 的变化量,会导致 Root Yaw Offset 减小到0。
总的来说就是,原地转身动画播放的时候,推进Root Yaw Offset,让角色转过去。
Base_Start

!!!注意这个 动画层节点点一下,这里有个TAG

这里Apply Additive的是一个倾斜的混合空间
AdditiveLeanAngle 是啥
void UEqZeroAnimInstance::UpdateRotationData()
{
// Yaw 变化量 和 速度
FRotator NewWorldRotation = Character->GetActorRotation();
YawDeltaSinceLastUpdate = UKismetMathLibrary::NormalizedDeltaRotator(NewWorldRotation, WorldRotation).Yaw;
YawDeltaSpeed = YawDeltaSinceLastUpdate / GetWorld()->GetDeltaSeconds();
WorldRotation = NewWorldRotation;
if (IsCrouching || GameplayTag_IsADS)
{
AdditiveLeanAngle = YawDeltaSpeed * 0.025;
}
else
{
AdditiveLeanAngle = YawDeltaSpeed * 0.0375;
}
if (IsFirstUpdate)
{
YawDeltaSinceLastUpdate = AdditiveLeanAngle = 0.f;
// YawDeltaSpeed 不需要忽略,我们需要至少两次update来计算delta
}
}
AdditiveLeanAngle 是旋转速度成一个缩放值,如果是蹲下和开镜,数值会小一点。ADS是开镜技能的TAG
两个函数

存一下Start状态开始的方向。

更新的时候,如果没有Blend Out 把 RootYawOffsetMode 保持为Hold
Base_Cycle

Base_Stop


Base(Idle,Cycle,Start,Stop)过度
- Idle=>Start
Duration 0.15
Blend Profile FastFeet
条件:有加速度 || (开镜 && 有速度)
- Start=>Cycle
优先级1:RootYawOffset 绝对值 > 60
同步组:Locomotion
Duration:0.5
如果偏移量过大,就提前过渡到 “循环” 状态。例如,如果相机位于角色的右侧,而用户按下前进键,我们不希望长时间向左平移,而是希望向前慢跑。
此过渡使用 SyncGroupNameToRequireValidMarkersRule 设置,以确保我们等待直到有有效的同步标记,从而平稳地进入 “循环” 状态。
优先级2:Linked Layer Changed 动画层变化,例如手枪换步枪的时候马上到Cycle
Blend Logic: Inertialization (惯性化)
优先级2:Automatic Rule Base On Sequence Player In State
Duration 1.0
起步动画完成,马上过度到Cycle
优先级3:
4个条件满足一个就True,OR的逻辑
- Start的速度方向和当前速度方向不一样
- 蹲伏状态变化
- 开镜状态变化
- LocomotionSM 当前时间>0.15且 Displacement Speed < 10,不理解刚刚进入这个状态机且速度很小的时候,应该是有Locomotion其他状态的时候,这里应该不会触发。
Cycle=>Stop
条件:False == 有加速度 || (开镜 && 有速度) ps: 和上面Idle=>Start的条件相反
Duration:0.25
Blend Profile: Fast Feet
共享条件规则
Stop=>Idle
优先级1:Linked Layer Changed 动画层变化,例如手枪换步枪的时候马上到Idle
Blend Logic: Inertialization 惯性化
优先级2:蹲伏状态变化 || 开镜状态变化
Blend Logic: Inertialization 惯性化
优先级3:Automatic Rule Base On Sequence Player In State
Item_IdleState

我们先跳过turn in place 的逻辑

蹲伏状态变化的时候过去,再次变化或者动画完成的时候回来。过去的时候快一点,回来慢一点。都需要惯性化。
Idle=>StanceTransition
条件:蹲伏状态变化,Duration 0.15
StanceTransition=>Idle
优先级1条件:蹲伏状态变化,Duration 0.3
优先级2条件:自动过度规则,Duration 0.25
三个状态都有惯性化
StanceTransition里面使用 SetUpIdleTransition函数选择了进入蹲伏和退出蹲伏的动画,然后SetSequence就不截图了。
Item_StartState

选择开始动画
第一部分
注意同步组 Locomotion
SetUpStartAnim 和 UpdateStartAnim 选择开始状态的动画
SetUpStartAnim就是根据是否开镜,是否蹲伏,移动方向选择一个配置的动画而已。(缩减篇幅就不截图了)
UpdateStartAnim
其中Explict Time 是动画播放时间单位秒
Stride Warping Start Alpha 是 (Explict Time - 0.15) 在 MapRangeClamped(value, 0, 0.2, 0, 1)
直接写数字意味着这个地方是一个配置

Stride Warping Start Alpha:基于Start动画播放时间被映射到[0,1]的一个值,表现上会就是从0过渡到1
X = Lerp(0.2, 0.6, Alpha)
Y = 5

根据 DisplacementSinceLastUpdate 这一个更新的距离量
Locomotion动画曲线,打开一个Start动画,可以看到这个是从0.4过度到750的数值,看起来也是描述速度
Play Rate Clamp 是一个范围限制上面的 X Y
通过实际速度,动画曲线的速度,算出动画要跑到那一帧了。
混合举枪资深
HipFireUpperBodyOverrideWeight 比如刚刚开火,就需要混入上半身的动画
void UEqZeroItemAnimInstance::UpdateBlendWeightData(float DeltaSeconds)
{
// RaiseWeaponAfterFiringWhenCrouched 是默认False的配置
bool bCondition1 = !RaiseWeaponAfterFiringWhenCrouched && MainAnim->IsCrouching;
bool bCondition2 = !MainAnim->IsCrouching && MainAnim->GameplayTag_IsADS && MainAnim->IsOnGround;
// 蹲伏状态 || 非蹲伏状态下在地面瞄准
if (bCondition1 || bCondition2)
{
HipFireUpperBodyOverrideWeight = 0.0f;
AimOffsetBlendWeight = 1.0f;
return ;
}
bCondition1 = MainAnim->TimeSinceFiredWeapon < RaiseWeaponAfterFiringDuration;
bCondition2 = MainAnim->GameplayTag_IsADS && (MainAnim->IsCrouching || !MainAnim->IsOnGround);
// 0.5s 内刚刚开火 || 瞄准 (蹲伏或不在地面) || 动画资产强制应用瞄准姿势覆盖
if (bCondition1 || bCondition2 || GetCurveValue(TEXT("applyHipfireOverridePose")) > 0.f)
{
HipFireUpperBodyOverrideWeight = 1.0f;
AimOffsetBlendWeight = 1.0f;
}
else
{
// 否则平滑地插值回0
HipFireUpperBodyOverrideWeight = FMath::FInterpTo(HipFireUpperBodyOverrideWeight, 0.0f, DeltaSeconds, 1.0f);
// 静止或存在根偏航偏移时使用瞄准偏移, 正常运动时使用放松瞄准偏移
float AimTargetInterpTo = 1.f;
if (bool UseHipFireWeight = FMath::Abs(MainAnim->RootYawOffset) < 10.f && MainAnim->HasAcceleration)
{
AimTargetInterpTo = HipFireUpperBodyOverrideWeight;
}
AimOffsetBlendWeight = FMath::FInterpTo(AimOffsetBlendWeight, AimTargetInterpTo, DeltaSeconds, 10.0f);
}
}

UpdateHipFireRaiseWeaponPose:根据是否蹲伏选择两个动画,输出,然后混合上半身
下一段是朝向扭曲和步幅扭曲

void UEqZeroAnimInstance::UpdateVelocityData()
{
bool WasMovingLastUpdate = LocalVelocity2D.SizeSquared() > KINDA_SMALL_NUMBER;
WorldVelocity = Character->GetVelocity();
FVector WorldVelocity2D = WorldVelocity * FVector(1.0f, 1.0f, 0.0f);
// Local
LocalVelocity2D = WorldRotation.UnrotateVector(WorldVelocity2D);
LocalVelocityDirectionAngle = UKismetAnimationLibrary::CalculateDirection(WorldVelocity2D, WorldRotation);
LocalVelocityDirectionAngleWithOffset = LocalVelocityDirectionAngle - RootYawOffset;
// ...
}
LocalVelocityDirectionAngle 就是 Yaw平面的角度
【-45】【0】【45】
【-90】【O】【90】
【-135【180】【135】
RootYawOffset的数值意义是,转多少度能回到角色的前方向。
Orientation Warping 朝向扭曲,目的是只有四个方向的动画通过混合出45度的效果。
可以通过姿势+修改LocomotionAngle 45 和 -45 观察角色45度移动的效果。
那么如何理解这个两个变量呢?LocalVelocityDirectionAngle 和 LocalVelocityDirectionAngleWithOffset
LocalVelocityDirectionAngle:
计算方式:CalculateDirection(WorldVelocity2D, WorldRotation)
用途:这是最基础的移动方向。它告诉你,相对于角色逻辑上的正面(Actor Forward),你正往哪个方向移动。
例如:你按 W 前进,值为 0 度。你按 D 右移,值为 90 度。
局限性:在现代 3A 动画系统中,角色的胶囊体朝向(逻辑朝向)和原本的模型朝向(视觉朝向)可能不一致。比如角色此时身体(Mesh)朝左看,但你按了 W 想往前走。如果只用这个变量,动画系统会播放“向前走”的动画,但因为模型身子是歪的,看起来就像是往侧面滑步。
LocalVelocityDirectionAngleWithOffset
含义:相对于「模型(Mesh / Root)」的移动方向。
计算方式:LocalVelocityDirectionAngle - RootYawOffset
关键点:RootYawOffset 是 胶囊体朝向 与 模型根骨骼朝向 的夹角(通常用于实现 Turn In Place 原地转身机制)。
当角色身体还没转过来,但胶囊体已经转过去时,就会产生 RootYawOffset。
用途:这是修正后的移动方向,专门用于驱动动画(BlendSpace)。
它回答了:“相对于我现在脚实际指向的方向,我应该往哪边迈腿?”
例子:假设胶囊体朝向正北(0°),但角色有一个向右的 45° 偏移(RootYawOffset = 45°,身体朝向东北)。
如果你此时按下 W 往前走(WorldVelocity 朝北)。
LocalVelocityDirectionAngle = 0°(相对于胶囊体是向前的)。
LocalVelocityDirectionAngleWithOffset = 0° - 45° = -45°。
结果:动画蓝图会选择“左前方 45°”的走路动画。这在视觉上是正确的,因为你的身体朝向右歪了 45°,想要走出直线的路径,你的腿必须相对于身体往左前方迈。
总结:
无 Offset (LocalVelocityDirectionAngle):纯逻辑方向。用于一般性判断(如“玩家是否输入了向后的指令”)。
有 Offset (WithOffset):视觉/动画方向。这是给 BlendSpace 用的,确保播放的跑步动画方向与模型当前的朝向完美匹配,防止“滑冰”现象。
==
Stride Warping 通过 Stride Warping Start Alpha(前面提过和播放实际相关的一个计算量)
来影响步幅。测试发现Alpha 0 的时候是源不服,越大步幅越小。
第二个参数没能看出明显的效果
Item_Cycle

朝向扭曲和步幅扭曲除了参数,其他和前一个一样,还有是不是开火混合动画。
这里有个UpdateCycleAnim
第一段是根据是否开镜,蹲伏,移动方向

第二段是根据 DisplacementSpeed,这一次更新的速度,推进动画序列的播放时间。
然后根据是否撞墙,计算一个【0.5,1】的混合都,给后续的距离匹配。
撞墙检测:
void UEqZeroAnimInstance::UpdateWallDetectionHeuristic()
{
// 该逻辑通过检查速度和加速度之间是否存在大角度(即角色正朝着墙壁推进,但实际上却在向侧面滑动)
// 以及角色是否在尝试加速但速度相对较低,来判断角色是否正在撞到墙上。
IsRunningIntoWall = false;
if (LocalAcceleration2D.Size() > 0.1f && LocalVelocity2D.Size() < 200.0f)
{
float Dot = FVector::DotProduct(LocalAcceleration2D.GetSafeNormal(), LocalVelocity2D.GetSafeNormal());
// Check if angle is in [-0.6, 0.6] range (approx 53 to 127 degrees)
// implying pushing into wall but sliding
if (Dot >= -0.6f && Dot <= 0.6f)
{
IsRunningIntoWall = true;
}
}
}
根据加速度和速度方向,判断是否在撞墙,如果是,就会影响cycle的播放速度,来减速
注意Sequence Player,同步组Locomotion, Always Follower。
Item_Stop

比较短,但是注意这个选项
Stop同步组
Should Loop False。如果
- 勾选 → 动画到末尾后会回到开头继续评估。
- 不勾选 → 动画到末尾后停在最后一帧。
SetUpStopAnim
根据蹲伏,开镜,移动方向选择动画

然后就是距离匹配
Should Distance Match Stop 的条件是:
(有速度 && 没有加速度)
没有加速度失去了输入,但是 如果有速度就会滑。
UpdateStopAnim

计算一个预计停下的距离
如果>0就走距离匹配,否则就正常推荐动画时间