LyraLog6 动画系统1

添加调试面板

用大家自己的方式把调试面板加出来,方便后续的调试。我这里用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的逻辑

  1. Start的速度方向和当前速度方向不一样
  2. 蹲伏状态变化
  3. 开镜状态变化
  4. 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就走距离匹配,否则就正常推荐动画时间

上一篇
下一篇