UE5 - 制作《塞尔达传说》中林克的技能 - 9 - 耐力条制作(涉及蓝图)

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

9_耐力条显示


本章节具体内容如下:

  1. 导入UMG包和创建Widget脚本(命名为ZSLayout)
  2. 创建耐力条
  3. 实际功能实现
  4. 具体代码
  5. 本章绘制的蓝图

1. 导入UMG包和创建Widget

1.1 UMG和Widget介绍

  UMG:UMG(Unreal Motion Graphics)是UE内置的用户界面(UI)框架,转为创建交互式游戏界面、HUD(平视显示器)和实时应用界面而设计。
  Widget:Widget则是通过UMG系统创建的UI组件,支持可视化设计(蓝图)和代码定制(C++)。其功能包括显示信息(如血条、得分、耐力条),接收玩家输入(如按钮点击)以及动态更新内容。

1.2 UMG包的导入

  导入方式与第5章,增强输入(Enhanced Input)一致,打开咱们的**.Build**脚本,如下图所示。
在这里插入图片描述
  导入完成后,关闭VS,到工程文件下重新生成一次。如下图所示。
在这里插入图片描述

1.3 创建Widget脚本

  通过VS打开咱们的UE。工具=》新建C++类=》Widget=》UserWidget
在这里插入图片描述
  重命名为ZSLayout,并更改一下存放的文件夹为UI(手动输入的)。
在这里插入图片描述


2.创建耐力条

2.1 根据ZSLayout创建蓝图类 - UI_ZSLayout

  第一步:右键之前创建的ZSLayout,然后选择创建基于ZSLayout的蓝图类
在这里插入图片描述
  第二步:将这个类命名为UI_ZSLayout,并放在自己创建的UI文件夹下。
在这里插入图片描述
  第三步:双击刚刚创建完的UI_ZSLayout就进入到这个神奇的UI布局界面了!
在这里插入图片描述

2.2 创建耐力条

2.2.1 创建画布面板

  创建画布面板(Canvas)的原因:画布面板是UI系统的入口容器,所有其他控件(按钮、文本、进度条等)都必须放置在其内部才能被渲染。
在这里插入图片描述

2.2.2 创建耐力条

  第一步:创建进度条。耐力条是通过组件进度条来制作的,具体流程如下,需要注意的就是要勾选其为变量,后续要在蓝图用到它
在这里插入图片描述
  第二步:可以更改一下进度条的背景,如下,效果会更好。
在这里插入图片描述
  第三步:调整进度条的锚点位置和拖拽进度条到如下图所示的位置。
在这里插入图片描述
  第四步:搜索渲染不透明度,我们将其设置为0。因为本次项目中耐力条是在消耗体力和恢复体力的时候才显示出来的,并不是一直存在的(这个我们通过代码来实现)。
在这里插入图片描述
  第五步:养成好习惯,编译保存一下
在这里插入图片描述

2.3 创建耐力条的动画

  如视频所示,我们的耐力条不是突然就蹦出来,也不是突然就消失的。
  这个的制作原理是:制作一个进度条的动画,当进度条需要显示的时候,让其透明度从0变到1【这个过程用时1秒,你也可以自己调整】,当体力恢复满后,进度条持续1秒才变为透明【这个过程是将原先的动画反播完成的】。我们在这个小节中,先完成耐力条的动画,在后面的小节会实现视频的效果

  动画的制作:
  第一步:新建一个动画(命名为Anim_Gauge,后面蓝图会用到它),并将进度条添加到动画中
在这里插入图片描述
  第二步:选择PB_ST的渲染不透明度
在这里插入图片描述
  第三步:分别在0s,1s,2s添加3个关键帧
0s的关键帧中:渲染不透明度为0
1s的关键帧中:渲染不透明度为1
2s的关键帧中:渲染不透明度为1
从第1s到第2s的关键帧,其实是起到一个延续的效果。
在这里插入图片描述


3. 实际功能的实现

功能1:角色创建出来后,实例化一个ZSLayout对象,其模板为UI_ZSLayout
功能2:正确的显示体力的变化。
功能3:在消耗体力和恢复体力的时候才显示耐力条。
功能4:(后续章节完成):当体力耗尽后,恢复体力的过程中体力条的颜色会变为红色。

3.1 功能1:实例化ZSLayout对象

 (1)ZSCharBase.h中声明变量,一个用于存放预设的ZSLayout对象(要在UE中进行手动赋值),一个用于存储实例化的对象,由于后续在蓝图中要用到当前体力和最大体力两个变量,因此给这两个变量添加BlueprintReadWrite

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stamina")
	float CurStamina = 0.0f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stamina")
	float MaxStamina = 100.0f;
	
	UPROPERTY(EditAnywhere, Category = "UI")
	// 指定需要动态加载的UI布局类(需继承自UUserWidget)
	TSubclassOf<UUserWidget> LayoutClassRef; 

	// 指向UZSLayout类型的UObject实例,用于管理UI布局的生命周期
	// 该指针由UE5垃圾回收系统自动管理,无需手动释放
	TObjectPtr<UZSLayout> LayoutRef;

 (2)ZSCharBase.cppBeginPlay中创建UI。

#include "ZSCharBase.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"

#include "ZSPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "UI/ZSLayout.h"


void AZSCharBase::BeginPlay()
{
	......
	// 创建UI
	if (LayoutClassRef)
	{
		LayoutRef = CreateWidget<UZSLayout>(GetWorld(), LayoutClassRef);
		if (LayoutRef)
		{
			LayoutRef->PlayerRef = this;
			LayoutRef->AddToViewport(); // 将UI添加到视图进行显示
		}
	}
}

 (3)UEBP_Player赋值LayoutClassRef变量,将第二章我们做的UI_ZSLayout赋值过去。
在这里插入图片描述

3.2功能2:动态显示体力值的变化

3.2.1 具体思路说明

  UI_ZSLayout的蓝图中要能获取到角色的当前体力和最大体力值将(当前体力/最大体力)这个比值,给到PB_ST的百分比属性,最后在每帧中进行调用即可实现动态调整进度条的显示
ZSLayout.h中:

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZSCharBase.h"
#include "ZSLayout.generated.h"


class AZSCharBase;

UCLASS()
class ZELDARSKILLS_API UZSLayout : public UUserWidget
{
	GENERATED_BODY()

public:
	// BlueprintReadWrite(被其修饰的变量,可以在蓝图中被读写) - ZSCharBase(自定义角色类)
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	AZSCharBase* PlayerRef;
};

3.2.2 功能实现-蓝图

 第一步:双击打开UI_ZSLayout,然后选择图表
在这里插入图片描述
 第二步:开始绘制蓝图(若是知道怎么绘制蓝图的,可以直接看该步骤的最后一张图)

  1. 创建PlayerRef节点右键空白处,选择GetPlayerRef
    在这里插入图片描述

  2. PlayerRef节点,拿到CurStamina和MaxStamina,详细步骤如下图1,创建出来后如下图2所示。
    在这里插入图片描述
    在这里插入图片描述

  3. 两个变量做除法,右键空白处,找到Safe Divide创建出来,然后将CurStamina和Max Stamina给过去
    在这里插入图片描述

  4. 拿到PB_ST,并设置其百分比值(Set Percent)
    在这里插入图片描述

  5. 为了安全起见,从PlayerRef节点,拉出一个Is Valid函数,其值有效时,才执行Set Percent
    在这里插入图片描述

  6. 每帧都执行,在Event Tick中调用
    在这里插入图片描述

3.3 功能3:耐力条的动态显示

  仅当消耗体力或是恢复体力才显示耐力条

  第一步:在ZSLayout.h中声明一个函数接口,在蓝图中进行具体的实现

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZSCharBase.h"
#include "ZSLayout.generated.h"

class AZSCharBase;

UCLASS()
class ZELDARSKILLS_API UZSLayout : public UUserWidget
{
	GENERATED_BODY()

public:
	.....

	UFUNCTION(BlueprintImplementableEvent)
	// BlueprintImplementableEvent(声明函数接口,被它修饰的函数具体逻辑在蓝图中实现) - 体力值动画
	void ShowGaugeAnim(bool bShow);
};

PS:更改代码后记得编译执行,重新打开UE。

  第二步:绘制蓝图

  1. 创建事件节点:ShowGaugeAnim,上面代码写的那个。
    在这里插入图片描述

  2. 我们将Show,提取成一个变量【选Promote to variable
    在这里插入图片描述

  3. 判断传入的Show值(我们称为bShow),与其本身存储的是否相同。若是不相等,则继续执行,并将传入的bShow赋值个Show。否则不再继续执行
    在这里插入图片描述

  4. 拿到咱们之前创建的动画(Anim Gauge),并选择Play Animation,然后根据Show的正负进行动画的播放。
    若Show == true:说明此时要显示耐力条,正向播放。
    若Show == false:说明此时要隐藏耐力条,反向播放。
    在这里插入图片描述

  5. 整理一下这些节点,并给个注释(全选从选项中创建注释
    在这里插入图片描述
    在这里插入图片描述

  第三步:在ZSCharBase.cpp中调用函数ShowGaugeAnim

void AZSCharBase::StartDrainStamina()
{
	// 执行前先清除一次体力相关的计时器 - 避免重复注册
	ClearDrainRecoverTimers();

	// 向内置的计时器管理,注册 消耗体力用的计时器
	GetWorldTimerManager().SetTimer(DrainStaminaTimerHandle,
		this, &AZSCharBase::DrainStaminaTimer, StaminaDepletionRate, true);

	// 显示体力条 UI
	if (LayoutRef)
	{
		LayoutRef->ShowGaugeAnim(true);
	}
}

void AZSCharBase::RecoverStaminaTimer()
{
	// 检查当前体力是否低于最大值
	if (CurStamina < MaxStamina)
	{
		// 恢复体力(最小不能小于0,最大不能超过MaxStamina)
		CurStamina = FMath::Clamp((CurStamina + StaminaRecoverAmount), 0.0f, MaxStamina);
	}
	else
	{
		// 体力恢复完成(体力值已满)
		// 清除 恢复体力计时器,并且进入到正常移动状态
		GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
		LocomotionManager(EMovementTypes::MT_Walking);

		// 隐藏体力条 UI
		if (LayoutRef)
		{
			LayoutRef->ShowGaugeAnim(false);
		}
	}
}

void AZSCharBase::DrainStaminaTimer()
{
	if (CurStamina <= 0.0f)
	{
		// 当前体力 <= 0 角色进入疲劳状态
		LocomotionManager(EMovementTypes::MT_Exhausted);
	}
	else
	{
		// 消耗体力(最小不能小于0,最大不能超过MaxStamina)
		CurStamina = FMath::Clamp((CurStamina - StaminaDepletionAmount), 0.0f, MaxStamina);
	}
}

完成咯!!
快去运行看看效果吧!


4. 具体代码

4.1 ZSLayout.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZSCharBase.h"
#include "ZSLayout.generated.h"

class AZSCharBase;

UCLASS()
class ZELDARSKILLS_API UZSLayout : public UUserWidget
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	// BlueprintReadWrite(被其修饰的变量,可以在蓝图中被读写) - ZSCharBase(自定义角色类)
	AZSCharBase* PlayerRef;

	UFUNCTION(BlueprintImplementableEvent)
	// BlueprintImplementableEvent(声明函数接口,被它修饰的函数具体逻辑在蓝图中实现) - 体力值动画
	void ShowGaugeAnim(bool bShow);
};

4.2 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;
class UZSLayout;


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, BlueprintReadWrite, Category = "Stamina")
	float CurStamina = 0.0f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, 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; // 体力恢复的总数


	UPROPERTY(EditAnywhere, Category = "UI")
	// 指定需要动态加载的UI布局类(需继承自UUserWidget)
	TSubclassOf<UUserWidget> LayoutClassRef; 

	// 当前帧移动速度分量
	float velocityX; // X轴,前后移动速度(W/S)
	float velocityY; // Y轴,左右移动速度(A/D)

	// 指向UZSLayout类型的UObject实例,用于管理UI布局的生命周期
	// 该指针由UE5垃圾回收系统自动管理,无需手动释放
	TObjectPtr<UZSLayout> LayoutRef;

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

	UFUNCTION(BlueprintCallable)
	// BlueprintCallable(将C++函数暴露给蓝图,使蓝图可以使用这个函数作为节点)  - 判断角色当前是否处于疲劳状态
	bool const IsCharacterExhausted(); 


	void SetSprinting(); // 冲刺的具体逻辑
	void SetWalking(); // 正常移动的具体逻辑
	void ResetToWalk(); // 重置内置的移动模式

	void DrainStaminaTimer(); //  管理体力消耗
	FTimerHandle DrainStaminaTimerHandle; // 计时器句柄 - 用于管理DrainStaminaTimer
	void StartDrainStamina(); // 触发函数 - 开始消耗体力

	void RecoverStaminaTimer(); //  管理体力恢复
	FTimerHandle RecoverStaminaTimerHandle; // 计时器句柄 - 用于管理RecoverStaminaTimer
	void StartRecoverStamina(); // 触发函数 - 开始恢复体力

	void ClearDrainRecoverTimers(); // 清除体力相关的计时器


};

4.3 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"
#include "UI/ZSLayout.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;

	// 创建UI
	if (LayoutClassRef)
	{
		LayoutRef = CreateWidget<UZSLayout>(GetWorld(), LayoutClassRef);
		if (LayoutRef)
		{
			LayoutRef->PlayerRef = this;
			LayoutRef->AddToViewport(); // 将UI添加到视图进行显示
		}
	}
}

// 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);
	}
}

bool const AZSCharBase::IsCharacterExhausted()
{
	// 若角色进入疲劳状态,体力条颜色显示 - 红色
	bool bEqual = CurrentMT == EMovementTypes::MT_Exhausted;
	return bEqual;
}

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)
	{
		// 当前体力 <= 0 角色进入疲劳状态
		LocomotionManager(EMovementTypes::MT_Exhausted);
	}
	else
	{
		// 消耗体力(最小不能小于0,最大不能超过MaxStamina)
		CurStamina = FMath::Clamp((CurStamina - StaminaDepletionAmount), 0.0f, MaxStamina);
	}
}

void AZSCharBase::StartDrainStamina()
{
	// 执行前先清除一次体力相关的计时器 - 避免重复注册
	ClearDrainRecoverTimers();

	// 向内置的计时器管理,注册 消耗体力用的计时器
	GetWorldTimerManager().SetTimer(DrainStaminaTimerHandle,
		this, &AZSCharBase::DrainStaminaTimer, StaminaDepletionRate, true);

	// 显示体力条 UI
	if (LayoutRef)
	{
		LayoutRef->ShowGaugeAnim(true);
	}
}

void AZSCharBase::RecoverStaminaTimer()
{
	// 检查当前体力是否低于最大值
	if (CurStamina < MaxStamina)
	{
		// 恢复体力(最小不能小于0,最大不能超过MaxStamina)
		CurStamina = FMath::Clamp((CurStamina + StaminaRecoverAmount), 0.0f, MaxStamina);
	}
	else
	{
		// 体力恢复完成(体力值已满)
		// 清除 恢复体力计时器,并且进入到正常移动状态
		GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
		LocomotionManager(EMovementTypes::MT_Walking);

		// 隐藏体力条 UI
		if (LayoutRef)
		{
			LayoutRef->ShowGaugeAnim(false);
		}
	}
}

void AZSCharBase::StartRecoverStamina()
{
	ClearDrainRecoverTimers();
	// 向内置的计时器管理,注册 消耗体力用的计时器
	GetWorldTimerManager().SetTimer(RecoverStaminaTimerHandle,
		this, &AZSCharBase::RecoverStaminaTimer, StaminaRecoverRate, true);
}

void AZSCharBase::ClearDrainRecoverTimers()
{
	GetWorldTimerManager().ClearTimer(DrainStaminaTimerHandle);
	GetWorldTimerManager().ClearTimer(RecoverStaminaTimerHandle);
}

	



5.本章的蓝图

在这里插入图片描述

第9部分完成啦!!
十分感谢大家的阅读、点赞、收藏!!
如果有不足之处,有错误地方,欢迎大家在评论区讨论、批评、指正!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

月忆铭

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

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

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

打赏作者

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

抵扣说明:

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

余额充值