JustMakeGame 2. 移动状态&姿态&步态

先看一下这三个状态的枚举。主要是维护一些数据给动画蓝图用。

  • 移动状态 MovememtState,前期只需要关注Ground和InAir 即可。Ground就是地面状态,InAir包括了空格键跳跃和前面没路了继续走的下落状态。
  • 姿态 Stance。包括站立和蹲伏。
  • 步态Gait。走,跑,冲刺三个状态。
UENUM(BlueprintType)
enum class ECwlMovementState : uint8
{
    Grounded,
    InAir,
    Mantling,
    Ragdoll,
    Fly,
    Swimming,
    Climbing,
};

UENUM(BlueprintType)
enum class ECwlGait : uint8
{
    Walking,
    Running,
    Sprinting,
};

UENUM(BlueprintType)
enum class ECwlStance : uint8
{
    Standing,
    Crouch
};

基础的跳跃功能

完成一个空格跳跃的功能。
目前我们只关心MovememtState的Grounded和InAir
下面包括了按下和松开跳跃键的两个回调。核心就是调用Jump()接口。角色就跳了。StopJumping()停止跳跃。

// 按下跳跃键
void ACwlBasePlayer::InputBeginJump(const FInputActionValue& InputValue)
{
    // ...
    // 省略一些不允许跳跃的限制代码 return 掉,比如飞行状态,攀爬状态,正在做某些动作等

    if (GetMovementState() == ECwlMovementState::Grounded)
    {
        if (GetStance() == ECwlStance::Standing)
        {
            Jump();
        }
        else if (GetStance() == ECwlStance::Crouch)
        {
            UnCrouch();
        }
    }
}

// 松开跳跃键
void ACwlBasePlayer::InputEndJump(const FInputActionValue& InputValue)
{
    if (GetMovementState() == ECwlMovementState::InAir)
    {
        StopJumping();
    }
}

ps: 如果不能跳需要去角色蓝图,基础能力,可跳跃那里设置一下。会导致Character的Jump函数没有效果

跳跃后,我们需要有一个地方修改我们的MovememtState的地面和空中状态。所以需要重写角色身上的这个接口 OnMovementModeChanged
引擎维护的角色运动状态变化后,会从这里回调我们。可以在这里处理引擎的运动状态变化时,我们自己的一些状态赋值。

void ACwlCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
    Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);

    if (EMovementMode MovementMode = this->GetCharacterMovement()->MovementMode.GetValue();
        MovementMode == EMovementMode::MOVE_None || MovementMode == EMovementMode::MOVE_Walking || MovementMode == EMovementMode::MOVE_NavWalking)
    {
        SetMovementState(ECwlMovementState::Grounded);
    }
    else if (MovementMode == EMovementMode::MOVE_Falling)
    {
        SetMovementState(ECwlMovementState::InAir);
    }
    else if (MovementMode == EMovementMode::MOVE_Swimming)
    {
        SetMovementState(ECwlMovementState::Swimming);
    }
    else if (MovementMode == EMovementMode::MOVE_Custom)
    {
        SetMovementState(ECwlMovementState::Climbing);
    }
}

当然我们也可以主动调用角色运动组件SetMovementMode 来设置他

    UFUNCTION(BlueprintCallable, Category="Pawn|Components|CharacterMovement")
    virtual void SetMovementMode(EMovementMode NewMovementMode, uint8 NewCustomMode = 0);

比如我们入水的时候,手动设置游泳状态。按某个键切换飞行状态。你写了一个射线检测,检测到墙了,设置为Custom状态,然后在Custom状态中完成攀爬系统。后期详细展开。

到这一步,按跳跃键,角色会向上蹦一下,但是没有任何动作。

姿态:蹲伏和站立

同样从按键事件开始,比如ctrl键按一下蹲下,按一下起来。直接调用Character的Crouch(),UnCrouch() 接口即可。

void ACwlBasePlayer::InputChangeStanceAction(const FInputActionValue& InputValue)
{
    if (GetMovementState() == ECwlMovementState::Grounded)
    {
        switch (GetStance())
        {
        case ECwlStance::Standing:
            DesiredStance = ECwlStance::Crouch;
            Server_SetDesiredStance(DesiredStance);
            Crouch();
            break;
        case ECwlStance::Crouch:
            DesiredStance = ECwlStance::Standing;
            Server_SetDesiredStance(DesiredStance);
            UnCrouch();
            break;
        }
    }
}

Crouch(),UnCrouch() 调用后,会回调到OnStartCrouch,OnEndCrouch 用于我们修改变量,类似成功回调。这是Character的两个虚函数,重写他

void ACwlCharacter::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
    Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
    SetStance(ECwlStance::Crouch);
}

void ACwlCharacter::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
    Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
    SetStance(ECwlStance::Standing);
}

这两行是服务器环境下才需要运行的代码。主玩家修改步态,RPC服务端,再广播其他玩家。
这里应该有一个链接:虚幻服务器简单概念与使用【TODO】

    DesiredStance = ECwlStance::Crouch;
    Server_SetDesiredStance(DesiredStance);

虚幻风格的RPC的定义

    UFUNCTION(Server, Reliable)
    void Server_SetDesiredStance(const ECwlStance InDesiredStance);
void ACwlCharacter::Server_SetDesiredStance_Implementation(const ECwlStance InDesiredStance)
{
    DesiredStance = InDesiredStance;
}

主玩家调用Server_SetDesiredStance后,Server_SetDesiredStance_Implementation 会在服务端调用
这个函数只复制了变量,但是虚幻的变量赋值是可以触发更新函数的

    UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_DesiredStance, Category = "Cwl|State Values")
    ECwlStance DesiredStance = ECwlStance::Standing;

又由于属性变化时调用OnRep_DesiredStance。其他玩家同步到了这个值。

步态:走,跑,冲刺

从输入按键开始
ALT切换走跑,走和跑是两套动作,速度不一样。有点像手柄摇杆只动一点是走,推到底是跑。按下去是冲刺。
不过我们这里是PC,测试阶段,还是ALT切换走跑,按住Sprint键冲刺。所以有4个回调。在这里我们只需要设置变量的状态变化即可。

void ACwlBasePlayer::InputBeginWalk(const FInputActionValue& InputValue)
{
    if (DesiredGait == ECwlGait::Running)
    {
        DesiredGait = ECwlGait::Walking;
        Server_SetDesiredGait(DesiredGait);
    }
    else if (DesiredGait == ECwlGait::Walking)
    {
        DesiredGait = ECwlGait::Running;
        Server_SetDesiredGait(DesiredGait);
    }
}

void ACwlBasePlayer::InputEndWalk(const FInputActionValue& InputValue)
{
}

void ACwlBasePlayer::InputBeginSprint(const FInputActionValue& InputValue)
{
    DesiredGait = ECwlGait::Sprinting;
    Server_SetDesiredGait(DesiredGait);
}

void ACwlBasePlayer::InputEndSprint(const FInputActionValue& InputValue)
{
    DesiredGait = ECwlGait::Running;
    Server_SetDesiredGait(DesiredGait);
}

状态的维护

在Character类身上有一个变量 StateValues 表示当前角色状态值,这个状态就包括上面的移动状态,姿态,步态
其中还有两个变量,叫期待步态,期待姿态。
意图是玩家的操作应该修改的是期待值,而能否修改当前值需要经过一系列判断。比如瞄准,蹲伏状态下不让冲刺。
直接设置期待值,可以让逻辑更清晰一点,调用的地方只管设置。具体能不能切换的逻辑丢到tick里面

    /*
     * state values
     */

    UPROPERTY(BlueprintReadOnly, Category = "Cwl|State Values")
    FCwlCharacterStateValues StateValues;

    /*
     * Desired state => gait / stance / rotation mode
     */

    UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_DesiredGait, Category = "Cwl|State Values")
    ECwlGait DesiredGait = ECwlGait::Running;

    UPROPERTY(BlueprintReadOnly, ReplicatedUsing=OnRep_DesiredStance, Category = "Cwl|State Values")
    ECwlStance DesiredStance = ECwlStance::Standing;

StateValues 内容如下,先只看 Stance,MovementState,Gait

USTRUCT(BlueprintType)
struct FCwlCharacterStateValues
{
    GENERATED_BODY()

    FCwlCharacterStateValues()
    {
        Stance = ECwlStance::Standing;
        MovementState = ECwlMovementState::Grounded;
        Gait = ECwlGait::Walking;

        // 。。。
    }

    UPROPERTY(EditAnywhere, Category = "Cwl|State Values")
    ECwlStance Stance;

    UPROPERTY(EditAnywhere, Category = "Cwl|State Values")
    ECwlMovementState MovementState;

    UPROPERTY(EditAnywhere, Category = "Cwl|State Values")
    ECwlGait Gait;

    // 。。。
};

然后就是BeginPlay的时候初始化一下

void ACwlCharacter::BeginPlay()
{
    Super::BeginPlay();
    // ...
    SetGait(DesiredGait, true);
    SetStance(DesiredStance, true);
    // ...
}

在各自需要的时候更新,
对于Stance,蹲下的时候直接设置DesiredStance,蹲伏回调的时候设置Stance。(看起来这个期待值变量意义不是那么大,但是这代表了一种扩展的可能)
步态Gait相对比较复杂。在玩家按键切换走跑状态时候,设置了期待步态,然后再Tick里面更新实际的步态。

void ACwlCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 。。。

    if (GetMovementState() == ECwlMovementState::Grounded)
    {
        UpdateCharacterMovement();

        // 。。。
    }

    // 。。。

}

UpdateCharacterMovement 中计算步态,然后设置值,最后按照当前走,跑,冲刺,设置角色的移动速度

void ACwlCharacter::UpdateCharacterMovement()
{
    ECwlGait AllowedGait = GetAllowedGait();
    ECwlGait ActualGait = GetActualGait(AllowedGait);
    if (ActualGait != GetGait())
    {
        SetGait(ActualGait);
    }

    MyCharacterMovementComponent->SetAllowedGait(AllowedGait);
    // UpdateDynamicMovementSettings(AllowedGait);
}

GetAllowedGait 就是上述所说,瞄准姿态不允许冲刺,如果这时候期待步态是冲刺,就把他改回奔跑。类似的逻辑可以写这里面

ECwlGait ACwlCharacter::GetAllowedGait()
{
    if (GetStance() == ECwlStance::Standing && GetRotationMode() == ECwlRotationMode::LookingDirection ||
        GetStance() == ECwlStance::Standing && GetRotationMode() == ECwlRotationMode::VelocityDirection)
    {
        if (DesiredGait == ECwlGait::Walking)
        {
            return ECwlGait::Walking;
        }
        if (DesiredGait == ECwlGait::Running)
        {
            return ECwlGait::Running;
        }
        if (DesiredGait == ECwlGait::Sprinting)
        {
            if (CanSprint())
            {
                return ECwlGait::Sprinting;
            }
            return ECwlGait::Running;
        }
    }
    else if (GetStance() == ECwlStance::Standing && GetRotationMode() == ECwlRotationMode::Aiming || GetStance() == ECwlStance::Crouch)
    {
        if (DesiredGait == ECwlGait::Walking)
        {
            return ECwlGait::Walking;
        }
        if (DesiredGait == ECwlGait::Running || GetGait() == ECwlGait::Sprinting)
        {
            return ECwlGait::Running;
        }
    }

    return ECwlGait::Walking;
}

GetActualGait 则是认为,走,跑,冲刺的过度还需要受实际速度的限制
必须走到跑,再到冲刺。不能走直接到冲刺。
EssentialInfomation中的数值的当前角色速度,CurrentMovementSettings是配置表的速度

ECwlGait ACwlCharacter::GetActualGait(ECwlGait AllowedGait)
{
    const float LocalWalkSpeed = MyCharacterMovementComponent->CurrentMovementSettings.WalkSpeed;
    const float LocalRunSpeed = MyCharacterMovementComponent->CurrentMovementSettings.RunSpeed;

    if (EssentialInfomation.Speed > LocalRunSpeed + 10.f)
    {
        if (AllowedGait == ECwlGait::Walking || AllowedGait == ECwlGait::Running)
        {
            return ECwlGait::Running;
        }
        if (AllowedGait == ECwlGait::Sprinting)
        {
            return ECwlGait::Sprinting;
        }
    }
    if (EssentialInfomation.Speed > LocalWalkSpeed + 10.f)
    {
        return ECwlGait::Running;
    }
    return ECwlGait::Walking;
}

这行代码被我注释掉了,其实在单机模式下是没有问题的,如果是使用UE服务端状态下,会触发一个UE网络同步的问题。这里引用另一篇文章【https://www.yuque.com/chenweilin-tryw7/zvnzey/ioc6ztvznchbekaq
这个函数的作用就是走,跑,冲刺的移动速度显然是不一样的,所以我们需要读表然后往角色运动组件设置一下。

// UpdateDynamicMovementSettings(AllowedGait);
void ACwlCharacter::UpdateDynamicMovementSettings(ECwlGait AllowedGait)
{
    const FCwlMovementSettings MovementSettings = GetTargetMovementSettings();
    switch (AllowedGait)
    {
    case ECwlGait::Walking:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.WalkSpeed;
        break;
    case ECwlGait::Running:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.RunSpeed;
        break;
    case ECwlGait::Sprinting:
        GetCharacterMovement()->MaxWalkSpeed = MovementSettings.SprintSpeed;
        break;
    default:
        break;
    }

    FVector MovementCurve = MovementSettings.MovementCurve->GetVectorValue(MyCharacterMovementComponent->GetMappingSpeed());
    GetCharacterMovement()->MaxAcceleration = MovementCurve.X;
    GetCharacterMovement()->BrakingDecelerationWalking = MovementCurve.Y;
    GetCharacterMovement()->BrakingFriction = MovementCurve.Z;
}

我们先认为是单机模式下,调用这个函数没有问题。引出下一篇

上一篇
下一篇