让我们继续《塞尔达传说》中林克技能的制作!!!
本章节的核心目标:冲刺动作
先让我们看一下完成后的效果:
8_冲刺效果
PS:由于我们现在暂未作疲劳的状态,因此当角色耗光体力时先切回到正常移动状态。(你可以发现,当体力耗尽后,若是仍按下冲刺按键,其实并没有进行冲刺,而且体力开始恢复)。
我们预计制作的动作列表如下:
动作 | 按键 | 是否完成 |
---|---|---|
移动 | W、S、A、D | 完成 |
疲劳 | 无 | 未开始 |
冲刺 | Left Shift | 进行中 |
滑行 | 未设定 | 未开始 |
下落 | 无 | 未开始 |
角色的一些参数(后面可能会进行更改)
正常移动下 | 冲刺下 | |
---|---|---|
最大运动速度(MaxWalkSpeed) | 600 | 1200 |
空气控制(AirControl) | 0.35 | 0.35 |
本章节具体如下:
- 运动管理器(Locomotion Manager)
- 体力与冲刺
- 具体代码
1. 运动管理器(Locomotion Manager)
从本章节开始,我们要来制作咱们林克在游戏中能够进行运动的方式啦。那么首先,我们肯定要有一个管理这些运动的管理器,现在我们就来编写他。(好口水的开场白)
-
(1)创建一个枚举类(EMovementTypes),来枚举角色所有运动类型。在ZSCharBase.h中完成
UENUM(BlueprintType) enum class EMovementTypes : uint8 { MT_EMAX UMETA(DisplayName = "EMAX"), MT_Walking UMETA(DisplayName = "Walking"), MT_Exhausted UMETA(DisplayName = "Exhausted"), MT_Sprinting UMETA(DisplayName = "Sprinting"), MT_Gliding UMETA(DisplayName = "Gliding"), MT_Falling UMETA(DisplayName = "Falling") };
-
(2)声明一个EMovementTypes 类变量CurrentMT,来表示当前角色处于那种运动状态。在ZSCharBase.h中完成
UPROPERTY(EditAnywhere, Category = "States") EMovementTypes CurrentMT{ EMovementTypes::MT_EMAX };
-
(3)创建一个函数Locomotion Manager来管理这些运动的切换。在ZSCharBase.h中完成
-
(4)Locomotion Manager具体的框架,在ZSCharBase.cpp中完成
其思路为,当角色当前的运动类型与传入的运动类型不同时,切换具体的运动执行逻辑。void AZSCharBase::LocomotionManager(EMovementTypes NewMovement) { // 若动作未变化,则无需进行切换,直接返回 if (CurrentMT == NewMovement) return; CurrentMT = NewMovement; // 若角色处于滑行状态,显示滑行的模型(想想林克的降落伞) if (CurrentMT == EMovementTypes::MT_Gliding) { // todo:后面来做,显示降落伞 } // 根据运动类型,切换运动的执行逻辑 switch (CurrentMT) { case EMovementTypes::MT_EMAX: break; case EMovementTypes::MT_Walking: break; case EMovementTypes::MT_Exhausted: break; case EMovementTypes::MT_Sprinting: break; case EMovementTypes::MT_Gliding: break; case EMovementTypes::MT_Falling: break; default: break; } }
2. 体力与冲刺
2.1 体力
在本次的项目中,冲刺是需要消耗体力值的,涉及到体力的消耗和恢复。因此我们先编写体力恢复和消耗相关的函数。
体力的恢复和消耗,是通过注册计时器,然后根据各自的数值进行计算。
先声明相关的变量和函数:
ZSCharBase.h
具体包含了:
变量:
- 当前体力;
- 体力上限;
- 体力消耗的频率;
- 体力一次消耗的总值;
- 体力恢复的频率;
- 体力一次恢复的总值;
函数:
- 管理体力消耗
- 计时器句柄 - 用于管理体力消耗函数
- 触发函数 - 开始消耗体力
- 管理体力恢复
- 计时器句柄 - 用于管理体力恢复函数
- 触发函数 - 开始恢复体力
UPROPERTY(EditAnywhere, Category = "Stamina")
float CurStamina = 0.0f;
UPROPERTY(EditAnywhere, Category = "Stamina")
float MaxStamina = 100.0f;
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaDepletionRate = 0.05f; // 体力消耗的频率
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaDepletionAmount = 0.5f; // 体力消耗的总数
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaRecoverRate = 0.05f; // 体力恢复的频率
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaRecoverAmount = 0.5f; // 体力恢复的总数
// 这些函数内部使用即可,不需要暴露给UE,因此不用UFUNCTION()进行修饰
void DrainStaminaTimer(); // 管理体力消耗
FTimerHandle DrainStaminaTimerHandle; // 计时器句柄 - 用于管理DrainStaminaTimer
void StartDrainStamina(); // 触发函数 - 开始消耗体力
void RecoverStaminaTimer(); // 管理体力恢复
FTimerHandle RecoverStaminaTimerHandle; // 计时器句柄 - 用于管理RecoverStaminaTimer
void StartRecoverStamina(); // 触发函数 - 开始恢复体力
void ClearDrainRecoverTimers(); // 清除体力相关的计时器
功能的具体实现(函数我都标明了注释!!为自己良好的编码习惯点个赞):
ZSCharBase.cpp
void AZSCharBase::DrainStaminaTimer()
{
if (CurStamina <= 0.0f)
{
// todo:当前体力 <= 0 角色进入疲劳状态(由于我们这里还没做疲劳状态,因此我们先切回正常移动)
LocomotionManager(EMovementTypes::MT_Walking);
}
else
{
// 消耗体力(最小不能小于0,最大不能超过MaxStamina)
CurStamina = FMath::Clamp((CurStamina - StaminaDepletionAmount), 0.0f, MaxStamina);
}
}
void AZSCharBase::StartDrainStamina()
{
// 执行前先清除一次体力相关的计时器 - 避免重复注册
ClearDrainRecoverTimers();
// 向内置的计时器管理,注册 消耗体力用的计时器
GetWorldTimerManager().SetTimer(DrainStaminaTimerHandle,
this, &AZSCharBase::DrainStaminaTimer, StaminaDepletionRate, true);
// todo:留个尾巴,显示体力条 UI
}
void AZSCharBase::RecoverStaminaTimer()
{
// 检查当前体力是否低于最大值
if (CurStamina < MaxStamina)
{
// 恢复体力(最小不能小于0,最大不能超过MaxStamina)
CurStamina = FMath::Clamp((CurStamina + StaminaRecoverAmount), 0.0f, MaxStamina);
}
else
{
// 体力恢复完成(体力值已满)
// 清除 恢复体力计时器,并且进入到正常移动状态
GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
LocomotionManager(EMovementTypes::MT_Walking);
// TODO:留个尾巴,后续在这里要 隐藏恢复体力条 UI
}
}
void AZSCharBase::StartRecoverStamina()
{
ClearDrainRecoverTimers();
// 向内置的计时器管理,注册 消耗体力用的计时器
GetWorldTimerManager().SetTimer(RecoverStaminaTimerHandle,
this, &AZSCharBase::RecoverStaminaTimer, StaminaRecoverRate, true);
}
void AZSCharBase::ClearDrainRecoverTimers()
{
GetWorldTimerManager().ClearTimer(DrainStaminaTimerHandle);
GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
}
为了方便测试,我们每帧打印一下CurStamina
ZSCharBase.cpp
void AZSCharBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 测试 - 打印 - 当前体力值
FString staminaStr = FString::SanitizeFloat(CurStamina);
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow, staminaStr);
}
2.2 冲刺
-
(1)创建动作映射:
-
(1.1)创建数据资产(输入操作): 并命名为IA_SprintAction
-
(1.2)IA_SprintAction的参数配置: 值类型:数字(布尔)。其实角色按下移动按键能否进行冲刺,就是看是否按下冲刺按键(true/false)。
-
(1.3)IMC_ZS_Settings: 我们将键盘左边的Shift作为冲刺按键(Left Shift)。
-
(1.4)Bp_Player: 与之前的动作相关操作相同,先在ZSCharBase.h中创建UInputAction类型的变量。然后 【编译=》执行=》在UE中对该变量进行赋值】,具体操作如下:
第一步(ZSCharBase.h):// 声明 冲刺动作 UPROPERTY(EditAnywhere, Category = "Inputs") UInputAction* SprintAction;
第二步(赋值):
-
-
(2)冲刺的具体逻辑:
- (2.1)按下冲刺按键,更改角色的最大运动速度,开始消耗体力。
- (2.2)冲刺阶段,角色会保持体力消耗和最大运动速度。
- (2.3)冲刺结束后,角色恢复正常运动速度,并开始恢复体力。
为了达成以上的目的。-
通过函数SetSprinting(): 实现最大运动速度,空气控制的变更,和调用消耗体力的触发函数。
-
通过函数SetWalking(): 实现恢复最大运动速度和空气控制,以及调用恢复体力的触发函数。
-
额外的函数RestToWalk(): 用于重置一下UE角色的内置运动模式,因为我们这里本质上只是改变了角色的运动速度,并没有变更UE内置的运动模式,因此为了防止不必要的错误,每次变更速度后重置一下。
。ZSCharBase.h 创建函数
void SetSprinting(); // 冲刺的具体逻辑 void SetWalking(); // 正常移动的具体逻辑 void ResetToWalk(); // 重置内置的移动模式
ZSCharBase.cpp实现具体的逻辑
#include "GameFramework/CharacterMovementComponent.h" ... void AZSCharBase::SetSprinting() { GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::White, "Sprinting"); GetCharacterMovement()->MaxWalkSpeed = 1200.0f; GetCharacterMovement()->AirControl = 0.35f; // 重新设置角色运动模式(内置的),其实冲刺本质上来说,值改变了角色的移动速度和控制灵敏度 // 以防万一,我们重置一下 ResetToWalk(); // 消耗体力 StartDrainStamina(); } void AZSCharBase::ResetToWalk() { GetCharacterMovement()->SetMovementMode(MOVE_Walking); } void AZSCharBase::SetWalking() { GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::White, "Walking"); // 重置正常移动下的:移动速度、控制灵活度 GetCharacterMovement()->MaxWalkSpeed = 600.0f; GetCharacterMovement()->AirControl = 0.35f; // 恢复体力 StartRecoverStamina(); }
-
(3)Sprint按键对应的事件(状态的切换逻辑):
冲刺可以被视为三个阶段(即:开始按下冲刺按钮,持续按下冲刺按钮,松开冲刺按钮)。因此,我们要在ZSCharBase.h下声明三个函数,用于响应相应的事件,代码如下:public: #pragma region Sprinting Action UFUNCTION() // 冲刺按钮 - 持续按着 - 触发的函数 void Sprint_Triggered(const FInputActionValue& val); UFUNCTION() // 冲刺按钮 - 松开 - 触发的函数 void Sprint_Released(const FInputActionValue& val); UFUNCTION() // 冲刺按钮 - 按下 - 触发的函数 void Sprint_Started(const FInputActionValue& val); #pragma endregion
将对应的事件进行绑定
ZSCharBase.cpp// 冲刺 EIComp->BindAction(SprintAction, ETriggerEvent::Triggered, this, &AZSCharBase::Sprint_Triggered); EIComp->BindAction(SprintAction, ETriggerEvent::Completed, this, &AZSCharBase::Sprint_Released); EIComp->BindAction(SprintAction, ETriggerEvent::Started, this, &AZSCharBase::Sprint_Started);
-
阶段一(开始按下冲刺按钮),对应 Sprint_Started函数:
- 检测判断:当玩家处于正常移动状态或是初始的默认状态下,才能切换到冲刺状态下:
void AZSCharBase::Sprint_Started(const FInputActionValue& val) { // 按键按下(仅触发一次) // 仅当,角色处于行走或是初始默认状态时,按下冲刺按钮,才会执行 if (CurrentMT == EMovementTypes::MT_EMAX || CurrentMT == EMovementTypes::MT_Walking) { LocomotionManager(EMovementTypes::MT_Sprinting); } }
-
阶段二(持续按着冲刺按钮),对应Sprint_Triggered函数:
- 检测判断:当玩家处于冲刺状态下,速度(X和Y方向)却为0时,要切换到正常移动状态
void AZSCharBase::Sprint_Triggered(const FInputActionValue& val) { // 持续按下按钮(每帧触发) // 当角色处于冲刺状态下,且速度为0时切换回正常移动状态 if (CurrentMT == EMovementTypes::MT_Sprinting && velocityX == 0 && velocityY == 0) { LocomotionManager(EMovementTypes::MT_Walking); } }
-
阶段三(松开冲刺按钮),对应Sprint_Released函数:
- 检测判断:当玩家处于冲刺状态下,切换到正常移动状态。
void AZSCharBase::Sprint_Released(const FInputActionValue& val) { // 松开按键(仅触发一次) // 角色处于冲刺状态,松开按钮意味着,回到正常移动状态 if (CurrentMT == EMovementTypes::MT_Sprinting) { LocomotionManager(EMovementTypes::MT_Walking); } }
-
-
(4)LocomotionManager 根据状态执行具体逻辑
ZSCharBase.cppvoid AZSCharBase::LocomotionManager(EMovementTypes NewMovement) { // 若动作未变化,则无需进行切换,直接返回 if (CurrentMT == NewMovement) return; CurrentMT = NewMovement; // 若角色处于滑行状态,显示滑行的模型(想想林克的降落伞) if (CurrentMT == EMovementTypes::MT_Gliding) { // todo:后面来做,显示降落伞 } // 根据运动类型,切换运动的执行逻辑 switch (CurrentMT) { case EMovementTypes::MT_EMAX: break; case EMovementTypes::MT_Walking: SetWalking(); break; case EMovementTypes::MT_Exhausted: break; case EMovementTypes::MT_Sprinting: SetSprinting(); break; case EMovementTypes::MT_Gliding: break; case EMovementTypes::MT_Falling: break; default: break; } }
3. 具体代码
3.1 ZSCharBase.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "ZSCharBase.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
UENUM(BlueprintType)
enum class EMovementTypes : uint8
{
MT_EMAX UMETA(DisplayName = "EMAX"),
MT_Walking UMETA(DisplayName = "Walking"),
MT_Exhausted UMETA(DisplayName = "Exhausted"),
MT_Sprinting UMETA(DisplayName = "Sprinting"),
MT_Gliding UMETA(DisplayName = "Gliding"),
MT_Falling UMETA(DisplayName = "Falling")
};
UCLASS()
class ZELDARSKILLS_API AZSCharBase : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AZSCharBase();
// 声明 Camera Boom(摄像机弹簧臂)
UPROPERTY(EditAnywhere, Category = "Components")
TObjectPtr<USpringArmComponent> CameraBoom;
// 声明 Follow Camera(跟随摄像机)
UPROPERTY(EditAnywhere, Category = "Components")
TObjectPtr<UCameraComponent> FollowCamera;
// 声明 InputMappingContext(输入映射上下文)
UPROPERTY(EditAnywhere, Category = "Inputs")
UInputMappingContext* IMC_ZS;
// 声明 移动动作
UPROPERTY(EditAnywhere, Category = "Inputs")
UInputAction* MoveAction;
// 声明 摄像机视角动作
UPROPERTY(EditAnywhere, Category = "Inputs")
UInputAction* LookAction;
// 声明 冲刺动作
UPROPERTY(EditAnywhere, Category = "Inputs")
UInputAction* SprintAction;
UPROPERTY(EditAnywhere, Category = "States")
EMovementTypes CurrentMT{ EMovementTypes::MT_EMAX };
UPROPERTY(EditAnywhere, Category = "Stamina")
float CurStamina = 0.0f;
UPROPERTY(EditAnywhere, Category = "Stamina")
float MaxStamina = 100.0f;
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaDepletionRate = 0.05f; // 体力消耗的频率
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaDepletionAmount = 0.5f; // 体力消耗的总数
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaRecoverRate = 0.05f; // 体力恢复的频率
UPROPERTY(EditAnywhere, Category = "Stamina")
float StaminaRecoverAmount = 0.5f; // 体力恢复的总数
// 当前帧移动速度分量
float velocityX; // X轴,前后移动速度(W/S)
float velocityY; // Y轴,左右移动速度(A/D)
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UFUNCTION()
void LocomotionManager(EMovementTypes NewMovement);
#pragma region Move_Action Function
UFUNCTION()
// 正常移动 - 持续按着 - 触发的函数
void Move_Triggered(const FInputActionValue& val);
UFUNCTION()
// 正常移动 - 松开 - 触发的函数
void Move_Released(const FInputActionValue& val); // 处理移动动作的释放事件
#pragma endregion
#pragma region Camera_Rotation Function
UFUNCTION()
void Look_Triggered(const FInputActionValue& val); // 处理鼠标晃动,摄像机跟随旋转事件
#pragma endregion
#pragma region Sprinting Action
UFUNCTION()
// 冲刺按钮 - 持续按着 - 触发的函数
void Sprint_Triggered(const FInputActionValue& val);
UFUNCTION()
// 冲刺按钮 - 松开 - 触发的函数
void Sprint_Released(const FInputActionValue& val);
UFUNCTION()
// 冲刺按钮 - 按下 - 触发的函数
void Sprint_Started(const FInputActionValue& val);
#pragma endregion
void SetSprinting(); // 冲刺的具体逻辑
void SetWalking(); // 正常移动的具体逻辑
void ResetToWalk(); // 重置内置的移动模式
void DrainStaminaTimer(); // 管理体力消耗
FTimerHandle DrainStaminaTimerHandle; // 计时器句柄 - 用于管理DrainStaminaTimer
void StartDrainStamina(); // 触发函数 - 开始消耗体力
void RecoverStaminaTimer(); // 管理体力恢复
FTimerHandle RecoverStaminaTimerHandle; // 计时器句柄 - 用于管理RecoverStaminaTimer
void StartRecoverStamina(); // 触发函数 - 开始恢复体力
void ClearDrainRecoverTimers(); // 清除体力相关的计时器
};
3.2 ZSCharBase.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ZSCharBase.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "ZSPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
// Sets default values
AZSCharBase::AZSCharBase()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
#pragma region Camera
// 摄像机弹簧臂(Camera Boom),需要设置的参数
// (1)创建 Camera Boom实体
// (2)挂接的对象
// (3)弹簧臂与角色的距离
// (4)玩家移动鼠标时,吊杆跟随旋转
CameraBoom = CreateDefaultSubobject<USpringArmComponent>("Camera Boom");
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
// 摄像机实体,需要设置的参数
// (1)创建摄像机实体
// (2)设置摄像机的挂接对象
// (3)玩家移动鼠标时,静止摄像机跟随旋转
FollowCamera = CreateDefaultSubobject<UCameraComponent>("Follow Camera");
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
#pragma endregion
}
// Called when the game starts or when spawned
void AZSCharBase::BeginPlay()
{
Super::BeginPlay();
// 获取自定义的角色控制器
AZSPlayerController* PC = Cast<AZSPlayerController>(Controller);
if (PC == nullptr) return; // 没获取到直接退出后续的操作
// 获取增强输入系统
UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
if (Subsystem == nullptr) return;
// 注册输入映射上下文IMC_ZS,优先级设为0(最低优先级,适用于基础输入场景)
Subsystem->AddMappingContext(IMC_ZS, 0);
// 初始化体力
CurStamina = MaxStamina;
}
// Called every frame
void AZSCharBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 测试 - 打印 - 当前体力值
FString staminaStr = FString::SanitizeFloat(CurStamina);
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow, staminaStr);
}
// Called to bind functionality to input
void AZSCharBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 输入组件初始化:
UEnhancedInputComponent* EIComp = Cast<UEnhancedInputComponent>(PlayerInputComponent);
if (EIComp == nullptr) return;
// 移动动作事件绑定
EIComp->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AZSCharBase::Move_Triggered);
EIComp->BindAction(MoveAction, ETriggerEvent::Completed, this, &AZSCharBase::Move_Released);
// 摄像机
EIComp->BindAction(LookAction, ETriggerEvent::Triggered, this, &AZSCharBase::Look_Triggered);
// 冲刺
EIComp->BindAction(SprintAction, ETriggerEvent::Triggered, this, &AZSCharBase::Sprint_Triggered);
EIComp->BindAction(SprintAction, ETriggerEvent::Completed, this, &AZSCharBase::Sprint_Released);
EIComp->BindAction(SprintAction, ETriggerEvent::Started, this, &AZSCharBase::Sprint_Started);
}
void AZSCharBase::LocomotionManager(EMovementTypes NewMovement)
{
// 若动作未变化,则无需进行切换,直接返回
if (CurrentMT == NewMovement) return;
CurrentMT = NewMovement;
// 若角色处于滑行状态,显示滑行的模型(想想林克的降落伞)
if (CurrentMT == EMovementTypes::MT_Gliding)
{
// todo:后面来做,显示降落伞
}
// 根据运动类型,切换运动的执行逻辑
switch (CurrentMT)
{
case EMovementTypes::MT_EMAX:
break;
case EMovementTypes::MT_Walking:
SetWalking();
break;
case EMovementTypes::MT_Exhausted:
break;
case EMovementTypes::MT_Sprinting:
SetSprinting();
break;
case EMovementTypes::MT_Gliding:
break;
case EMovementTypes::MT_Falling:
break;
default:
break;
}
}
void AZSCharBase::Move_Triggered(const FInputActionValue& val)
{
// 从数据容器中提取二维向量数据
FVector2D vectorData = val.Get<FVector2D>();
velocityX = vectorData.X; // 分解二维向量的水平分量(X轴),用于控制角色的纵向移动【处理键盘W/S】
velocityY = vectorData.Y; // 分解二维向量的水平分量(Y轴),用于控制角色的横向移动【处理键盘A/D】
if (Controller == nullptr) return;
// 从控制器获取当前水平的旋转角度(Yaw轴,绕Z轴旋转),忽略pitch和roll
FRotator YawRotator = FRotator(0, Controller->GetControlRotation().Yaw, 0);
// 将旋转角度转换为世界坐标系下的方向矩阵,并提取X轴单位向量
// 举个例子:角色面朝北时,X轴方向为(1,0,0),面朝南时(-1,0,0)
FVector worldDirectionX = FRotationMatrix(YawRotator).GetUnitAxis(EAxis::X);
AddMovementInput(worldDirectionX, velocityX); // 根据水平输入速度值(velocityX)沿世界X轴方向移动输入
// 同理,处理Y
FVector worldDirectionY = FRotationMatrix(YawRotator).GetUnitAxis(EAxis::Y);
AddMovementInput(worldDirectionY, velocityY);
}
void AZSCharBase::Move_Released(const FInputActionValue& val)
{
// 重置速度变量
velocityX = 0.0f;
velocityY = 0.0f;
}
void AZSCharBase::Look_Triggered(const FInputActionValue& val)
{
FVector2D controllerRotator = val.Get<FVector2D>();
// 控制摄像机旋转
AddControllerYawInput(controllerRotator.X);
AddControllerPitchInput(controllerRotator.Y);
}
void AZSCharBase::Sprint_Triggered(const FInputActionValue& val)
{
// 持续按下按钮(每帧触发)
// 当角色处于冲刺状态下,且速度为0时切换回正常移动状态
if (CurrentMT == EMovementTypes::MT_Sprinting && velocityX == 0 && velocityY == 0)
{
LocomotionManager(EMovementTypes::MT_Walking);
}
}
void AZSCharBase::Sprint_Released(const FInputActionValue& val)
{
// 松开按键(仅触发一次)
// 角色处于冲刺状态,松开按钮意味着,回到正常移动状态
if (CurrentMT == EMovementTypes::MT_Sprinting)
{
LocomotionManager(EMovementTypes::MT_Walking);
}
}
void AZSCharBase::Sprint_Started(const FInputActionValue& val)
{
// 按键按下(仅触发一次)
// 仅当,角色处于行走或是初始默认状态时,按下冲刺按钮,才会执行
if (CurrentMT == EMovementTypes::MT_EMAX || CurrentMT == EMovementTypes::MT_Walking)
{
LocomotionManager(EMovementTypes::MT_Sprinting);
}
}
void AZSCharBase::SetSprinting()
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::White, "Sprinting");
GetCharacterMovement()->MaxWalkSpeed = 1200.0f;
GetCharacterMovement()->AirControl = 0.35f;
// 重新设置角色运动模式(内置的),其实冲刺本质上来说,值改变了角色的移动速度和控制灵敏度
// 以防万一,我们重置一下
ResetToWalk();
// 消耗体力
StartDrainStamina();
}
void AZSCharBase::ResetToWalk()
{
GetCharacterMovement()->SetMovementMode(MOVE_Walking);
}
void AZSCharBase::SetWalking()
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::White, "Walking");
// 重置正常移动下的:移动速度、控制灵活度
GetCharacterMovement()->MaxWalkSpeed = 600.0f;
GetCharacterMovement()->AirControl = 0.35f;
// 恢复体力
StartRecoverStamina();
}
void AZSCharBase::DrainStaminaTimer()
{
if (CurStamina <= 0.0f)
{
// todo:当前体力 <= 0 角色进入疲劳状态(由于我们这里还没做疲劳状态,因此我们先切回正常移动)
LocomotionManager(EMovementTypes::MT_Walking);
}
else
{
// 消耗体力(最小不能小于0,最大不能超过MaxStamina)
CurStamina = FMath::Clamp((CurStamina - StaminaDepletionAmount), 0.0f, MaxStamina);
}
}
void AZSCharBase::StartDrainStamina()
{
// 执行前先清除一次体力相关的计时器 - 避免重复注册
ClearDrainRecoverTimers();
// 向内置的计时器管理,注册 消耗体力用的计时器
GetWorldTimerManager().SetTimer(DrainStaminaTimerHandle,
this, &AZSCharBase::DrainStaminaTimer, StaminaDepletionRate, true);
// todo:留个尾巴,显示体力条 UI
}
void AZSCharBase::RecoverStaminaTimer()
{
// 检查当前体力是否低于最大值
if (CurStamina < MaxStamina)
{
// 恢复体力(最小不能小于0,最大不能超过MaxStamina)
CurStamina = FMath::Clamp((CurStamina + StaminaRecoverAmount), 0.0f, MaxStamina);
}
else
{
// 体力恢复完成(体力值已满)
// 清除 恢复体力计时器,并且进入到正常移动状态
GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
LocomotionManager(EMovementTypes::MT_Walking);
// TODO:留个尾巴,后续在这里要 隐藏恢复体力条 UI
}
}
void AZSCharBase::StartRecoverStamina()
{
ClearDrainRecoverTimers();
// 向内置的计时器管理,注册 消耗体力用的计时器
GetWorldTimerManager().SetTimer(RecoverStaminaTimerHandle,
this, &AZSCharBase::RecoverStaminaTimer, StaminaRecoverRate, true);
}
void AZSCharBase::ClearDrainRecoverTimers()
{
GetWorldTimerManager().ClearTimer(DrainStaminaTimerHandle);
GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
}
至此第8章顺利完成!!
快运行看看效果吧!!!
十分感谢大家的阅读、点赞、收藏!!!!
如果有不足之处,有错误地方,欢迎大家在评论区讨论、批评、指正!!!!!