先看一下这三个状态的枚举。主要是维护一些数据给动画蓝图用。
- 移动状态 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;
}
我们先认为是单机模式下,调用这个函数没有问题。引出下一篇