feat: add Battle HUD(health) display

This commit is contained in:
2025-12-05 22:53:19 +08:00
parent f49735bcfe
commit 7bc8c57760
12 changed files with 140 additions and 13 deletions

5
Config/DefaultEditor.ini Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -18,12 +18,15 @@ void UHealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out
/** /**
* @brief UHealthComponent的构造函数。 * @brief UHealthComponent的构造函数。
* @note 必须在这里初始化最大生命值,不然就有问题
*/ */
UHealthComponent::UHealthComponent() { UHealthComponent::UHealthComponent() {
// 关闭每帧Tick以提高性能 // 关闭每帧Tick以提高性能
PrimaryComponentTick.bCanEverTick = false; PrimaryComponentTick.bCanEverTick = false;
// 默认启用网络复制 // 默认启用网络复制
SetIsReplicatedByDefault(true); SetIsReplicatedByDefault(true);
// 初始化当前生命值为最大生命值
CurrentHealth = MaxHealth;
} }
/** /**
@@ -31,8 +34,7 @@ UHealthComponent::UHealthComponent() {
*/ */
void UHealthComponent::BeginPlay() { void UHealthComponent::BeginPlay() {
Super::BeginPlay(); Super::BeginPlay();
// 初始化当前生命值为最大生命值 // OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
CurrentHealth = MaxHealth;
} }
/** /**
@@ -40,6 +42,7 @@ void UHealthComponent::BeginPlay() {
*/ */
void UHealthComponent::OnRep_CurrentHealth() { void UHealthComponent::OnRep_CurrentHealth() {
// 在客户端上当CurrentHealth被复制时调用 // 在客户端上当CurrentHealth被复制时调用
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
} }
@@ -60,6 +63,10 @@ void UHealthComponent::HandleDamage(float DamageAmount) {
// 仅在服务器上处理伤害逻辑 // 仅在服务器上处理伤害逻辑
if (GetOwner()->HasAuthority()) { if (GetOwner()->HasAuthority()) {
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth); CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth);
// 在服务器上直接广播事件
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
if (CurrentHealth <= 0.0f) { if (CurrentHealth <= 0.0f) {
OnDeath.Broadcast(); OnDeath.Broadcast();
} }

View File

@@ -12,6 +12,13 @@
*/ */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDeath);
/**
* @brief 当生命值变化时调用的多播委托。
* @param CurrentHealth 当前生命值
* @param MaxHealth 最大生命值
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, CurrentHealth, float, MaxHealth);
/** /**
* @class UHealthComponent * @class UHealthComponent
* @brief 生命组件 * @brief 生命组件
@@ -31,6 +38,12 @@ public:
UPROPERTY(BlueprintAssignable, Category = "Health") UPROPERTY(BlueprintAssignable, Category = "Health")
FOnDeath OnDeath; FOnDeath OnDeath;
/**
* @brief 生命值变化时广播的多播委托。可以在蓝图中绑定事件。
*/
UPROPERTY(BlueprintAssignable, Category = "Health")
FOnHealthChanged OnHealthChanged;
protected: protected:
/** /**
* @brief 角色的最大生命值。 * @brief 角色的最大生命值。
@@ -66,5 +79,11 @@ public:
*/ */
UFUNCTION(BlueprintCallable, Category="Health") UFUNCTION(BlueprintCallable, Category="Health")
void HandleDamage(float DamageAmount); void HandleDamage(float DamageAmount);
UFUNCTION(BlueprintPure, Category="Health")
float GetCurrentHealth() const { return CurrentHealth; }
UFUNCTION(BlueprintPure, Category="Health")
float GetMaxHealth() const { return MaxHealth; }
}; };

View File

@@ -15,7 +15,7 @@ class UHealthComponent;
/** /**
* @class ASurviverPlayer * @class ASurviverPlayer
* @brief 玩家角色基类,具有网络同步的移动组件,实现了可受伤害接口。 * @brief 玩家角色基类,具有网络同步的移动组件,实现了可受伤害接口。
* @ingroup Battle * @ingroup GameCore
*/ */
UCLASS() UCLASS()
class FIRSTPERSONDEMO_API ASurviverPlayer : public ACharacter, public IDamageableInterface { class FIRSTPERSONDEMO_API ASurviverPlayer : public ACharacter, public IDamageableInterface {

View File

@@ -3,8 +3,6 @@
#include "SurviverPlayerController.h" #include "SurviverPlayerController.h"
// Copyright Epic Games, Inc. All Rights Reserved.
#include "EnhancedInputSubsystems.h" #include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h" #include "Engine/LocalPlayer.h"
#include "InputMappingContext.h" #include "InputMappingContext.h"
@@ -12,6 +10,7 @@
#include "Blueprint/UserWidget.h" #include "Blueprint/UserWidget.h"
#include "FirstPersonDemo.h" #include "FirstPersonDemo.h"
#include "Widgets/Input/SVirtualJoystick.h" #include "Widgets/Input/SVirtualJoystick.h"
#include "FirstPersonDemo/Surviver_FPS/UI/BattleHUD.h"
ASurviverPlayerController::ASurviverPlayerController() { ASurviverPlayerController::ASurviverPlayerController() {
// set the player camera manager class // set the player camera manager class
@@ -34,6 +33,17 @@ void ASurviverPlayerController::BeginPlay() {
UE_LOG(LogFirstPersonDemo, Error, TEXT("Could not spawn mobile controls widget.")); UE_LOG(LogFirstPersonDemo, Error, TEXT("Could not spawn mobile controls widget."));
} }
} }
// 只在本地玩家控制器上创建 HUD
if (IsLocalPlayerController() && BattleHUDClass) {
BattleHUD = CreateWidget<UBattleHUD>(this, BattleHUDClass);
if (BattleHUD) {
BattleHUD->AddToViewport();
}
else {
UE_LOG(LogFirstPersonDemo, Error, TEXT("Could not create BattleHUD widget."));
}
}
} }
void ASurviverPlayerController::SetupInputComponent() { void ASurviverPlayerController::SetupInputComponent() {

View File

@@ -7,8 +7,11 @@
#include "SurviverPlayerController.generated.h" #include "SurviverPlayerController.generated.h"
class UInputMappingContext; class UInputMappingContext;
class UBattleHUD;
/** /**
* * @brief 玩家控制器类,包括玩家移动、玩家战斗中 HOD 的控制
* @ingroup GameCore
*/ */
UCLASS() UCLASS()
class FIRSTPERSONDEMO_API ASurviverPlayerController : public APlayerController { class FIRSTPERSONDEMO_API ASurviverPlayerController : public APlayerController {
@@ -18,6 +21,13 @@ public:
/** Constructor */ /** Constructor */
ASurviverPlayerController(); ASurviverPlayerController();
/** 玩家的 Battle HUD 类 */
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<class UBattleHUD> BattleHUDClass;
UPROPERTY()
UBattleHUD* BattleHUD;
protected: protected:
/** Input Mapping Contexts */ /** Input Mapping Contexts */
@@ -40,7 +50,10 @@ protected:
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls") UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false; bool bForceTouchControls = false;
/** Gameplay initialization */ /**
* @brief 初始化 Gameplay
* 初始化移动控制、初始化Battle HUD仅限本地玩家控制器
*/
virtual void BeginPlay() override; virtual void BeginPlay() override;
/** Input mapping context setup */ /** Input mapping context setup */

View File

@@ -0,0 +1,34 @@
// Copyright Majowaveon Games. All Rights Reserved.
#include "BattleHUD.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Kismet/GameplayStatics.h"
#include "FirstPersonDemo/Surviver_FPS/Battle/HealthComponent.h"
void UBattleHUD::UpdateHealth(float CurrentHealth, float MaxHealth) {
if (HealthBar) {
HealthBar->SetPercent(CurrentHealth / MaxHealth);
}
if (HealthText) {
HealthText->SetText(FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), CurrentHealth, MaxHealth)));
}
}
void UBattleHUD::NativeConstruct() {
Super::NativeConstruct();
// 获取玩家Pawn
APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
if (PlayerPawn) {
// 获取生命值组件
UHealthComponent* HealthComponent = PlayerPawn->FindComponentByClass<UHealthComponent>();
if (HealthComponent) {
// 绑定生命值变化事件
HealthComponent->OnHealthChanged.AddDynamic(this, &UBattleHUD::UpdateHealth);
// 初始化血量显示
UpdateHealth(HealthComponent->GetCurrentHealth(), HealthComponent->GetMaxHealth());
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright Majowaveon Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BattleHUD.generated.h"
class UProgressBar;
class UTextBlock;
/**
* @brief 战斗界面的 HUD显示当前血量
* @ingroup UI
*/
UCLASS()
class FIRSTPERSONDEMO_API UBattleHUD : public UUserWidget {
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "HUD")
void UpdateHealth(float CurrentHealth, float MaxHealth);
protected:
/** @brief 绑定生命显示逻辑
* 获取 @ref UHealthComponent绑定生命值变化 @ref OnHealthChanged 事件到 @ref UpdateHealth 函数
*/
virtual void NativeConstruct() override;
// 通过 meta = (BindWidget) 将此变量绑定到蓝图中的同名控件
UPROPERTY(meta = (BindWidget))
UProgressBar* HealthBar;
UPROPERTY(meta = (BindWidget))
UTextBlock* HealthText;
};