JustMakeGame 11. 原地旋转&原地转身&动态过度

原地旋转

效果是,角色在静止的时候,并且是瞄准状态下。视角移动,角色会触发一个踏步动作然后旋转。
在Tick中如果是瞄准状态下 CanRotateInPlace() == true
触发RotateInPlaceCheck

void UCwlBaseAnimIns::UpdateGroundValues()
{
    bShouldMove = CheckCanMove();

    if (IsFromStopToMove(bLastShouldMove, bShouldMove))
    {
        bRotateL = bRotateR = false;
    }

    bLastShouldMove = bShouldMove;
    if (bShouldMove)
    {
        // ...
    }
    else
    {
        if (CanRotateInPlace())
        {
            RotateInPlaceCheck();
        }
        else
        {
            bRotateL = bRotateR = false;
        }

        // ...
    }
}

bool UCwlBaseAnimIns::IsFromStopToMove(bool bInLastShouldMove, bool bInShouldMove) const
{
    return !bInLastShouldMove && bInShouldMove;
}

bool UCwlBaseAnimIns::CanRotateInPlace()
{
    return RotationMode == ECwlRotationMode::Aiming;
}
void UCwlBaseAnimIns::RotateInPlaceCheck()
{
    // 根据 AnimingAngle 计算左转还是右转
    // X 是 偏转值的Yaw 意思是左右
    bRotateL = AnimingAngle.X < RotateMinThreshold;
    bRotateR = AnimingAngle.X > RotateMaxThreshold;

    // 鼠标转速变成角色转速 AnimYawRate -> RotateRate
    if (bRotateL || bRotateR)
    {
        // 常量 
        RotateRate = UKismetMathLibrary::MapRangeClamped(AnimYawRate, AnimYawMinRange, AnimYawMaxRange, MinPlayRate, MaxPlayRate);
    }
}

里面大多都是常量

    const float RotateMinThreshold = -50;
    const float RotateMaxThreshold = 50;
    const float AnimYawMinRange = 90;
    const float AnimYawMaxRange = 270;
    const float MinPlayRate = 1.15;
    const float MaxPlayRate = 3;

其中 AnimingAngle 计算的是每次控制旋转和角色旋转的插是多少。AimingRotation 是 Control Rotation

    // 在当前角色的基础之上又偏了多少
    FRotator DeltaRotation = AimingRotation - Character->GetActorRotation();
    DeltaRotation.Normalize();
    AnimingAngle.X = DeltaRotation.Yaw;  // 左右
    AnimingAngle.Y = DeltaRotation.Pitch;  // 俯仰

动画状态机

image.png

绿色的条件是 bRotateL, 紫色的条件是 bRotate R
【Rotate L 90】和【Rotate R 90】在动画播放完毕
image.png
和有移动输入的时候需要过度到 Not Moving。以便于能继续过度到移动。图中白色和黄色的状态。

image.png

在旋转状态中,除了播放踏步动画以外。还会修改 Rotation Amount的值【注意这里modify是scale】。

image.png

看写这个动画Rotate_L_90的曲线 Rotation Amount。显然在脚步移动的时候,旋转比较快。非常真实。

这个数值会在角色没有移动时,AddActorWorldRotation 更新角色旋转

void ACwlCharacter::UpdateGroundedRotation()
{
    // 这里做的事就是 character movement 的使用控制器旋转做的事情
    float DeltaTime = GetWorld()->GetDeltaSeconds();

    // ...

    if (GetMovementAction() != ECwlMovementAction::None)
    {
        return ;    
    }

    if (CanUpdateMovingRotation())
    {
        // ...
    }
    else
    {
        if (GetViewMode() == ECwlViewMode::ThirdPerson && GetRotationMode() == ECwlRotationMode::Aiming || GetViewMode() == ECwlViewMode::FirstPerson)
        {
            LimitRotation(-100, 100, 20);
        }

        const float RotAmountCurve = GetAnimCurveValue(TEXT("RotationAmount"));
        if (FMath::Abs(RotAmountCurve) > 0.001)
        {
            float Yaw = RotAmountCurve * DeltaTime * 30;
            AddActorWorldRotation(UKismetMathLibrary::MakeRotator(0, 0, Yaw));
        }

        TargetRotator = GetActorRotation();
    }
}

原地转身

效果是非瞄准状态下,且位于LookingDirection状态下。视角看向角色后方,角色会自动触发一个转身动作

void UCwlBaseAnimIns::UpdateGroundValues()
{
    // ...

    if (bShouldMove)
    {
        // ...
    }
    else
    {
        // ...

        if (CanTurnInPlace())
        {
            TurnInPlaceCheck();
        }
        else
        {
            ELapsedDelayTime = 0;
        }

逻辑很简单 CanTurnInPlace() => TurnInPlaceCheck()

CanTurnInPlace的条件是 LookingDirection 且有 Enable_Transition 这条曲线。另外网络环境下 follower关闭这个功能。

bool UCwlBaseAnimIns::CanTurnInPlace()
{
    if (bIsFollower)
    {
        return false;
    }

    return RotationMode == ECwlRotationMode::LookingDirection && GetCurveValue(TEXT("Enable_Transition")) > 0.99;
}

这条曲线位于 Not Moving状态机中。将Rotation Scale缩放应用于曲线Rotation Amount 最后应用到角色旋转上。

image.png

这里还有一个插槽,用于播放蒙太奇。
总结一下就是需要转身,播放蒙太奇,处理旋转。

void UCwlBaseAnimIns::TurnInPlaceCheck()
{
    // 这里有BUG,DS下不同客户端表现不一样,还没修
    if (FMath::Abs(AnimingAngle.X) > TurnCheckMinAngle && AnimYawRate < AimYawRateLimit)
    {
        ELapsedDelayTime = ELapsedDelayTime + GetWorld()->GetDeltaSeconds();
        if (ELapsedDelayTime > UKismetMathLibrary::MapRangeClamped(
            FMath::Abs(AnimingAngle.X), TurnCheckMinAngle, 180, MinAngleDelay, MaxAngleDelay))
        {
            TurnInPlace(UKismetMathLibrary::MakeRotator(0, 0, AimingRotation.Yaw), 1, 0, false);
        }
    }
    else
    {
        ELapsedDelayTime = 0;
    }
}
void UCwlBaseAnimIns::TurnInPlace(const FRotator &TargetRotation, float PlayRateScale, float StartTime, bool bOverrideCurrent)
{
    float TurnAngle = UKismetMathLibrary::NormalizedDeltaRotator(TargetRotation, Character->GetActorRotation()).Yaw;

    if (FMath::Abs(TurnAngle) < Turn180Threshold)
    {
        if (TurnAngle < 0)
        {
            switch (Stance)
            {
            case ECwlStance::Standing:
                TargetTurnAsset = AlsAnimDataAsset->NTurnIpL90;
                break;
            case ECwlStance::Crouch:
                TargetTurnAsset = AlsAnimDataAsset->ClfTurnIpL90;
                break;
            }
        }
        else
        {
            switch (Stance)
            {
            case ECwlStance::Standing:
                TargetTurnAsset = AlsAnimDataAsset->NTurnIpR90;
                break;
            case ECwlStance::Crouch:
                TargetTurnAsset = AlsAnimDataAsset->ClfTurnIpR90;
                break;
            }
        }
    }
    else
    {
        if (TurnAngle < 0)
        {
            switch (Stance)
            {
            case ECwlStance::Standing:
                TargetTurnAsset = AlsAnimDataAsset->NTurnIpL180;
                break;
            case ECwlStance::Crouch:
                TargetTurnAsset = AlsAnimDataAsset->ClfTurnIpL180;
                break;
            }
        }
        else
        {
            switch (Stance)
            {
            case ECwlStance::Standing:
                TargetTurnAsset = AlsAnimDataAsset->NTurnIpL180;
                break;
            case ECwlStance::Crouch:
                TargetTurnAsset = AlsAnimDataAsset->ClfTurnIpL180;
                break;
            }
        }
    }

    if (bOverrideCurrent || !IsPlayingSlotAnimation(TargetTurnAsset.Animation, TargetTurnAsset.SlotName))
    {
        PlaySlotAnimationAsDynamicMontage(TargetTurnAsset.Animation, TargetTurnAsset.SlotName, 0.2, 0.2,
            TargetTurnAsset.PlayRate * PlayRateScale, 1.f, 0.f, StartTime);

        if (TargetTurnAsset.bScaleTurnAngle)
        {
            RotationScale = (TurnAngle / TargetTurnAsset.AnimatedAngle) * TargetTurnAsset.PlayRate * PlayRateScale;
        }
        else
        {
            RotationScale = TargetTurnAsset.PlayRate * PlayRateScale;
        }
    }
}

动态过度

地面变化的时候,通过播放一个蒙太奇,作为过度。脚步IK再适应地面。

static const FName GName_VB_Foot_Target_L(TEXT("VB foot_target_l"));
static const FName GName_VB_Foot_Target_R(TEXT("VB foot_target_r"));

void UCwlBaseAnimIns::UpdateGroundValues()
{
    // ...

    if (bShouldMove)
    {
        // ...
    }
    else
    {
        if (CanDynamicTransition())
        {
            DynamicTransitionCheck();
        }
        else
        {

        }
    }
}

bool UCwlBaseAnimIns::CanDynamicTransition()
{
    return GetCurveValue(TEXT("Enable_Transition")) > 0.99;
}

void UCwlBaseAnimIns::DynamicTransitionCheck()
{
    // 地面变化的时候,通过播放一个蒙太奇,作为过度。脚步IK再适应地面。
    if (UKismetAnimationLibrary::K2_DistanceBetweenTwoSocketsAndMapRange(
        GetOwningComponent(), IkFootLBoneName, RTS_Component, GName_VB_Foot_Target_L, RTS_Component,
        false, 0, 0, 0, 0) > 8.f)
    {
        PlayDynamicTransition(0.1, AlsAnimDataAsset->DynamicMontageParamsL);
    }

    if (UKismetAnimationLibrary::K2_DistanceBetweenTwoSocketsAndMapRange(
        GetOwningComponent(), IkFootRBoneName, RTS_Component, GName_VB_Foot_Target_R, RTS_Component,
        false, 0, 0, 0, 0) > 8.f)
    {
        PlayDynamicTransition(0.1, AlsAnimDataAsset->DynamicMontageParamsR);
    }
}
上一篇
下一篇