UE5 - 制作《塞尔达传说》中林克的技能 - 8 - 冲刺动作(Sprinting Action)

让我们继续《塞尔达传说》中林克技能的制作!!!
本章节的核心目标:冲刺动作
先让我们看一下完成后的效果:

8_冲刺效果

PS:由于我们现在暂未作疲劳的状态,因此当角色耗光体力时先切回到正常移动状态。(你可以发现,当体力耗尽后,若是仍按下冲刺按键,其实并没有进行冲刺,而且体力开始恢复)。

我们预计制作的动作列表如下:

动作按键是否完成
移动W、S、A、D完成
疲劳未开始
冲刺Left Shift进行中
滑行未设定未开始
下落未开始

角色的一些参数(后面可能会进行更改)

正常移动下冲刺下
最大运动速度(MaxWalkSpeed)6001200
空气控制(AirControl)0.350.35

本章节具体如下:

  1. 运动管理器(Locomotion Manager)
  2. 体力与冲刺
  3. 具体代码

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
具体包含了:

变量:

  1. 当前体力;
  2. 体力上限;
  3. 体力消耗的频率;
  4. 体力一次消耗的总值;
  5. 体力恢复的频率;
  6. 体力一次恢复的总值;

函数:

  1. 管理体力消耗
  2. 计时器句柄 - 用于管理体力消耗函数
  3. 触发函数 - 开始消耗体力
  4. 管理体力恢复
  5. 计时器句柄 - 用于管理体力恢复函数
  6. 触发函数 - 开始恢复体力
	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.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:
    		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章顺利完成!!
快运行看看效果吧!!!
十分感谢大家的阅读、点赞、收藏!!!!
如果有不足之处,有错误地方,欢迎大家在评论区讨论、批评、指正!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月忆铭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值