Bind HUD status to GAS and player state

This commit is contained in:
2026-04-26 20:20:32 +08:00
parent 7f3d9cd989
commit c4f185736f
5 changed files with 521 additions and 6 deletions

View File

@@ -109,5 +109,11 @@ UCommonActivatableWidget* APHYHUD::PushDefaultHUDWidget(APlayerController* Ownin
} }
const FGameplayTag HUDLayerTag = (UISettings && UISettings->HUDLayerTag.IsValid()) ? UISettings->HUDLayerTag : PHYGameplayTags::UI_Layer_HUD; const FGameplayTag HUDLayerTag = (UISettings && UISettings->HUDLayerTag.IsValid()) ? UISettings->HUDLayerTag : PHYGameplayTags::UI_Layer_HUD;
return RootLayout->PushWidgetToLayerStack<UCommonActivatableWidget>(HUDLayerTag, HUDWidgetClass); UCommonActivatableWidget* PushedWidget = RootLayout->PushWidgetToLayerStack<UCommonActivatableWidget>(HUDLayerTag, HUDWidgetClass);
if (UPHYHUDStatusWidget* StatusWidget = Cast<UPHYHUDStatusWidget>(PushedWidget))
{
StatusWidget->InitializeFromPlayer(OwningPlayerController);
}
return PushedWidget;
} }

View File

@@ -10,6 +10,7 @@
#include "Class/PHYClassComponent.h" #include "Class/PHYClassComponent.h"
#include "Class/PHYClassSettings.h" #include "Class/PHYClassSettings.h"
#include "GGA_AbilitySystemComponent.h" #include "GGA_AbilitySystemComponent.h"
#include "Net/UnrealNetwork.h"
APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer) APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer) : Super(ObjectInitializer)
@@ -28,7 +29,69 @@ APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer)
ClassComponent->SetClassTag(GetDefault<UPHYClassSettings>()->DefaultPlayerClassTag); ClassComponent->SetClassTag(GetDefault<UPHYClassSettings>()->DefaultPlayerClassTag);
} }
void APHYPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, PlayerLevel);
DOREPLIFETIME(ThisClass, Experience);
DOREPLIFETIME(ThisClass, ExperienceForNextLevel);
}
UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const
{ {
return AbilitySystemComponent; return AbilitySystemComponent;
} }
void APHYPlayerState::SetPlayerLevel(const int32 NewLevel)
{
SetProgression(NewLevel, Experience, ExperienceForNextLevel);
}
void APHYPlayerState::SetExperience(const int32 NewExperience)
{
SetProgression(PlayerLevel, NewExperience, ExperienceForNextLevel);
}
void APHYPlayerState::AddExperience(const int32 DeltaExperience)
{
SetExperience(Experience + DeltaExperience);
}
void APHYPlayerState::SetExperienceForNextLevel(const int32 NewExperienceForNextLevel)
{
SetProgression(PlayerLevel, Experience, NewExperienceForNextLevel);
}
void APHYPlayerState::SetProgression(const int32 NewLevel, const int32 NewExperience, const int32 NewExperienceForNextLevel)
{
if (!HasAuthority())
{
return;
}
const int32 ClampedLevel = FMath::Max(1, NewLevel);
const int32 ClampedExperience = FMath::Max(0, NewExperience);
const int32 ClampedExperienceForNextLevel = FMath::Max(1, NewExperienceForNextLevel);
if (PlayerLevel == ClampedLevel && Experience == ClampedExperience && ExperienceForNextLevel == ClampedExperienceForNextLevel)
{
return;
}
PlayerLevel = ClampedLevel;
Experience = ClampedExperience;
ExperienceForNextLevel = ClampedExperienceForNextLevel;
BroadcastProgressionChanged();
}
void APHYPlayerState::OnRep_Progression()
{
BroadcastProgressionChanged();
}
void APHYPlayerState::BroadcastProgressionChanged()
{
OnProgressionChanged.Broadcast(PlayerLevel, Experience, ExperienceForNextLevel);
}

View File

@@ -4,6 +4,8 @@
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYHUDStatusWidget) #include UE_INLINE_GENERATED_CPP_BY_NAME(PHYHUDStatusWidget)
#include "AbilitySystemComponent.h"
#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h"
#include "Blueprint/WidgetTree.h" #include "Blueprint/WidgetTree.h"
#include "Components/Overlay.h" #include "Components/Overlay.h"
#include "Components/OverlaySlot.h" #include "Components/OverlaySlot.h"
@@ -13,6 +15,9 @@
#include "Components/VerticalBox.h" #include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h" #include "Components/VerticalBoxSlot.h"
#include "Fonts/SlateFontInfo.h" #include "Fonts/SlateFontInfo.h"
#include "GameFramework/PlayerController.h"
#include "GameplayEffectTypes.h"
#include "Player/PHYPlayerState.h"
#include "Styling/CoreStyle.h" #include "Styling/CoreStyle.h"
#include "Widgets/SWidget.h" #include "Widgets/SWidget.h"
@@ -23,10 +28,29 @@ namespace
return FText::FromString(FString::Printf(TEXT("%s %.0f / %.0f"), Label, FMath::Max(0.0f, Value), FMath::Max(0.0f, MaxValue))); return FText::FromString(FString::Printf(TEXT("%s %.0f / %.0f"), Label, FMath::Max(0.0f, Value), FMath::Max(0.0f, MaxValue)));
} }
FText MakeProgressionText(const int32 CurrentExperience, const int32 RequiredExperience)
{
return FText::FromString(FString::Printf(TEXT("XP %d / %d"), FMath::Max(0, CurrentExperience), FMath::Max(1, RequiredExperience)));
}
float MakeResourcePercent(const float Value, const float MaxValue) float MakeResourcePercent(const float Value, const float MaxValue)
{ {
return MaxValue > UE_SMALL_NUMBER ? FMath::Clamp(Value / MaxValue, 0.0f, 1.0f) : 0.0f; return MaxValue > UE_SMALL_NUMBER ? FMath::Clamp(Value / MaxValue, 0.0f, 1.0f) : 0.0f;
} }
float MakeProgressionPercent(const int32 CurrentExperience, const int32 RequiredExperience)
{
return RequiredExperience > 0 ? FMath::Clamp(static_cast<float>(CurrentExperience) / static_cast<float>(RequiredExperience), 0.0f, 1.0f) : 0.0f;
}
void RemoveAttributeChangedDelegate(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute, FDelegateHandle& DelegateHandle)
{
if (AbilitySystemComponent && DelegateHandle.IsValid())
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).Remove(DelegateHandle);
DelegateHandle.Reset();
}
}
} }
UPHYHUDStatusWidget::UPHYHUDStatusWidget(const FObjectInitializer& ObjectInitializer) UPHYHUDStatusWidget::UPHYHUDStatusWidget(const FObjectInitializer& ObjectInitializer)
@@ -47,6 +71,35 @@ void UPHYHUDStatusWidget::SynchronizeProperties()
UpdateResourceWidgets(); UpdateResourceWidgets();
} }
void UPHYHUDStatusWidget::InitializeFromPlayer(APlayerController* InPlayerController)
{
if (!InPlayerController)
{
BoundPlayerController.Reset();
UnbindFromPlayerState();
UnbindFromAbilitySystem();
return;
}
BoundPlayerController = InPlayerController;
RefreshFromBoundSources();
}
void UPHYHUDStatusWidget::RefreshFromBoundSources()
{
APlayerController* PlayerController = BoundPlayerController.Get();
if (!PlayerController)
{
UnbindFromPlayerState();
UnbindFromAbilitySystem();
return;
}
APHYPlayerState* PHYPlayerState = PlayerController->GetPlayerState<APHYPlayerState>();
BindToPlayerState(PHYPlayerState);
BindToAbilitySystem(PHYPlayerState ? PHYPlayerState->GetAbilitySystemComponent() : nullptr);
}
void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHealth) void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHealth)
{ {
Health = NewHealth; Health = NewHealth;
@@ -54,6 +107,13 @@ void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHea
UpdateResourceWidgets(); UpdateResourceWidgets();
} }
void UPHYHUDStatusWidget::SetMana(const float NewMana, const float NewMaxMana)
{
Mana = NewMana;
MaxMana = NewMaxMana;
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxStamina) void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxStamina)
{ {
Stamina = NewStamina; Stamina = NewStamina;
@@ -61,6 +121,54 @@ void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxS
UpdateResourceWidgets(); UpdateResourceWidgets();
} }
void UPHYHUDStatusWidget::SetProgression(const int32 NewLevel, const int32 NewExperience, const int32 NewExperienceForNextLevel)
{
Level = FMath::Max(1, NewLevel);
Experience = FMath::Max(0, NewExperience);
ExperienceForNextLevel = FMath::Max(1, NewExperienceForNextLevel);
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::NativeConstruct()
{
Super::NativeConstruct();
if (!BoundPlayerController.IsValid())
{
InitializeFromPlayer(GetOwningPlayer());
}
else
{
RefreshFromBoundSources();
}
}
void UPHYHUDStatusWidget::NativeDestruct()
{
UnbindFromPlayerState();
UnbindFromAbilitySystem();
BoundPlayerController.Reset();
Super::NativeDestruct();
}
void UPHYHUDStatusWidget::NativeTick(const FGeometry& MyGeometry, const float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
APlayerController* PlayerController = BoundPlayerController.Get();
if (!PlayerController)
{
return;
}
APHYPlayerState* CurrentPlayerState = PlayerController->GetPlayerState<APHYPlayerState>();
if (CurrentPlayerState != BoundPlayerState.Get() || !BoundAbilitySystemComponent.IsValid())
{
RefreshFromBoundSources();
}
}
void UPHYHUDStatusWidget::BuildNativeWidgetTree() void UPHYHUDStatusWidget::BuildNativeWidgetTree()
{ {
if (!WidgetTree) if (!WidgetTree)
@@ -68,7 +176,7 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree()
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient); WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
} }
if (WidgetTree->RootWidget && TitleText && HealthText && HealthBar && StaminaText && StaminaBar) if (WidgetTree->RootWidget && TitleText && LevelText && HealthText && HealthBar && ManaText && ManaBar && StaminaText && StaminaBar && ExperienceText && ExperienceBar)
{ {
return; return;
} }
@@ -93,7 +201,15 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree()
TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.85f, 0.95f, 1.0f, 1.0f))); TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.85f, 0.95f, 1.0f, 1.0f)));
if (UVerticalBoxSlot* TitleSlot = StatusBox->AddChildToVerticalBox(TitleText)) if (UVerticalBoxSlot* TitleSlot = StatusBox->AddChildToVerticalBox(TitleText))
{ {
TitleSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 8.0f)); TitleSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 4.0f));
}
LevelText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("LevelText"));
LevelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
LevelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
if (UVerticalBoxSlot* LevelSlot = StatusBox->AddChildToVerticalBox(LevelText))
{
LevelSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 8.0f));
} }
HealthText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("HealthText")); HealthText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("HealthText"));
@@ -108,6 +224,18 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree()
HealthBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f)); HealthBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f));
} }
ManaText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ManaText"));
ManaText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
ManaText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
StatusBox->AddChildToVerticalBox(ManaText);
ManaBar = WidgetTree->ConstructWidget<UProgressBar>(UProgressBar::StaticClass(), TEXT("ManaBar"));
ManaBar->SetFillColorAndOpacity(FLinearColor(0.26f, 0.34f, 0.86f, 1.0f));
if (UVerticalBoxSlot* ManaBarSlot = StatusBox->AddChildToVerticalBox(ManaBar))
{
ManaBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f));
}
StaminaText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("StaminaText")); StaminaText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("StaminaText"));
StaminaText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16)); StaminaText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
StaminaText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f))); StaminaText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
@@ -117,14 +245,146 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree()
StaminaBar->SetFillColorAndOpacity(FLinearColor(0.12f, 0.62f, 0.86f, 1.0f)); StaminaBar->SetFillColorAndOpacity(FLinearColor(0.12f, 0.62f, 0.86f, 1.0f));
if (UVerticalBoxSlot* StaminaBarSlot = StatusBox->AddChildToVerticalBox(StaminaBar)) if (UVerticalBoxSlot* StaminaBarSlot = StatusBox->AddChildToVerticalBox(StaminaBar))
{ {
StaminaBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 0.0f)); StaminaBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f));
}
ExperienceText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ExperienceText"));
ExperienceText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
ExperienceText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
StatusBox->AddChildToVerticalBox(ExperienceText);
ExperienceBar = WidgetTree->ConstructWidget<UProgressBar>(UProgressBar::StaticClass(), TEXT("ExperienceBar"));
ExperienceBar->SetFillColorAndOpacity(FLinearColor(0.93f, 0.62f, 0.18f, 1.0f));
if (UVerticalBoxSlot* ExperienceBarSlot = StatusBox->AddChildToVerticalBox(ExperienceBar))
{
ExperienceBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 0.0f));
} }
UpdateResourceWidgets(); UpdateResourceWidgets();
} }
void UPHYHUDStatusWidget::BindToPlayerState(APHYPlayerState* NewPlayerState)
{
if (BoundPlayerState.Get() == NewPlayerState)
{
RefreshProgressionFromPlayerState();
return;
}
UnbindFromPlayerState();
BoundPlayerState = NewPlayerState;
if (NewPlayerState)
{
NewPlayerState->OnProgressionChanged.AddUniqueDynamic(this, &UPHYHUDStatusWidget::HandleProgressionChanged);
RefreshProgressionFromPlayerState();
}
}
void UPHYHUDStatusWidget::UnbindFromPlayerState()
{
if (APHYPlayerState* PlayerState = BoundPlayerState.Get())
{
PlayerState->OnProgressionChanged.RemoveDynamic(this, &UPHYHUDStatusWidget::HandleProgressionChanged);
}
BoundPlayerState.Reset();
}
void UPHYHUDStatusWidget::BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent)
{
if (BoundAbilitySystemComponent.Get() == NewAbilitySystemComponent && HealthChangedHandle.IsValid())
{
RefreshResourcesFromAbilitySystem();
return;
}
UnbindFromAbilitySystem();
BoundAbilitySystemComponent = NewAbilitySystemComponent;
if (!NewAbilitySystemComponent)
{
UpdateResourceWidgets();
return;
}
HealthChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetHealthAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
MaxHealthChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetMaxHealthAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
ManaChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetManaAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
MaxManaChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetMaxManaAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
StaminaChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetStaminaAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
MaxStaminaChangedHandle = NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UPHYCombatAttributeSet::GetMaxStaminaAttribute()).AddUObject(this, &UPHYHUDStatusWidget::HandleAttributeChanged);
RefreshResourcesFromAbilitySystem();
}
void UPHYHUDStatusWidget::UnbindFromAbilitySystem()
{
if (UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get())
{
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetHealthAttribute(), HealthChangedHandle);
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxHealthAttribute(), MaxHealthChangedHandle);
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetManaAttribute(), ManaChangedHandle);
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxManaAttribute(), MaxManaChangedHandle);
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetStaminaAttribute(), StaminaChangedHandle);
RemoveAttributeChangedDelegate(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxStaminaAttribute(), MaxStaminaChangedHandle);
}
HealthChangedHandle.Reset();
MaxHealthChangedHandle.Reset();
ManaChangedHandle.Reset();
MaxManaChangedHandle.Reset();
StaminaChangedHandle.Reset();
MaxStaminaChangedHandle.Reset();
BoundAbilitySystemComponent.Reset();
}
void UPHYHUDStatusWidget::RefreshResourcesFromAbilitySystem()
{
UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get();
if (!AbilitySystemComponent)
{
UpdateResourceWidgets();
return;
}
Health = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetHealthAttribute());
MaxHealth = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetMaxHealthAttribute());
Mana = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetManaAttribute());
MaxMana = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetMaxManaAttribute());
Stamina = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetStaminaAttribute());
MaxStamina = AbilitySystemComponent->GetNumericAttribute(UPHYCombatAttributeSet::GetMaxStaminaAttribute());
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::RefreshProgressionFromPlayerState()
{
APHYPlayerState* PlayerState = BoundPlayerState.Get();
if (!PlayerState)
{
UpdateResourceWidgets();
return;
}
SetProgression(PlayerState->GetPlayerLevel(), PlayerState->GetExperience(), PlayerState->GetExperienceForNextLevel());
}
void UPHYHUDStatusWidget::HandleAttributeChanged(const FOnAttributeChangeData& /*ChangeData*/)
{
RefreshResourcesFromAbilitySystem();
}
void UPHYHUDStatusWidget::HandleProgressionChanged(const int32 NewLevel, const int32 NewExperience, const int32 NewExperienceForNextLevel)
{
SetProgression(NewLevel, NewExperience, NewExperienceForNextLevel);
}
void UPHYHUDStatusWidget::UpdateResourceWidgets() void UPHYHUDStatusWidget::UpdateResourceWidgets()
{ {
if (LevelText)
{
LevelText->SetText(FText::FromString(FString::Printf(TEXT("Lv %d"), FMath::Max(1, Level))));
}
if (HealthText) if (HealthText)
{ {
HealthText->SetText(MakeResourceText(TEXT("HP"), Health, MaxHealth)); HealthText->SetText(MakeResourceText(TEXT("HP"), Health, MaxHealth));
@@ -133,6 +393,14 @@ void UPHYHUDStatusWidget::UpdateResourceWidgets()
{ {
HealthBar->SetPercent(MakeResourcePercent(Health, MaxHealth)); HealthBar->SetPercent(MakeResourcePercent(Health, MaxHealth));
} }
if (ManaText)
{
ManaText->SetText(MakeResourceText(TEXT("MP"), Mana, MaxMana));
}
if (ManaBar)
{
ManaBar->SetPercent(MakeResourcePercent(Mana, MaxMana));
}
if (StaminaText) if (StaminaText)
{ {
StaminaText->SetText(MakeResourceText(TEXT("ST"), Stamina, MaxStamina)); StaminaText->SetText(MakeResourceText(TEXT("ST"), Stamina, MaxStamina));
@@ -141,4 +409,12 @@ void UPHYHUDStatusWidget::UpdateResourceWidgets()
{ {
StaminaBar->SetPercent(MakeResourcePercent(Stamina, MaxStamina)); StaminaBar->SetPercent(MakeResourcePercent(Stamina, MaxStamina));
} }
if (ExperienceText)
{
ExperienceText->SetText(MakeProgressionText(Experience, ExperienceForNextLevel));
}
if (ExperienceBar)
{
ExperienceBar->SetPercent(MakeProgressionPercent(Experience, ExperienceForNextLevel));
}
} }

View File

@@ -13,6 +13,8 @@ class UPHYCoreAttributeSet;
class UPHYElementAttributeSet; class UPHYElementAttributeSet;
class UPHYClassComponent; class UPHYClassComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FPHYPlayerProgressionChangedSignature, int32, NewLevel, int32, NewExperience, int32, NewExperienceForNextLevel);
/** /**
* @brief PHY 玩家状态。 * @brief PHY 玩家状态。
* *
@@ -27,6 +29,9 @@ public:
/** @brief 构造玩家状态与 ASC。 */ /** @brief 构造玩家状态与 ASC。 */
APHYPlayerState(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); APHYPlayerState(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 注册 PlayerState 自身复制字段。 */
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/** @brief 获取玩家持久 AbilitySystemComponent。 */ /** @brief 获取玩家持久 AbilitySystemComponent。 */
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
@@ -50,7 +55,50 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class")
UPHYClassComponent* GetClassComponent() const { return ClassComponent; } UPHYClassComponent* GetClassComponent() const { return ClassComponent; }
/** @brief 获取当前角色等级。 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Progression")
int32 GetPlayerLevel() const { return PlayerLevel; }
/** @brief 获取当前经验值。 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Progression")
int32 GetExperience() const { return Experience; }
/** @brief 获取升到下一级所需经验值。 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Progression")
int32 GetExperienceForNextLevel() const { return ExperienceForNextLevel; }
/** @brief 服务端设置角色等级。 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="PHY|Progression")
void SetPlayerLevel(int32 NewLevel);
/** @brief 服务端设置当前经验值。 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="PHY|Progression")
void SetExperience(int32 NewExperience);
/** @brief 服务端增加当前经验值。 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="PHY|Progression")
void AddExperience(int32 DeltaExperience);
/** @brief 服务端设置下一级所需经验值。 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="PHY|Progression")
void SetExperienceForNextLevel(int32 NewExperienceForNextLevel);
/** @brief 服务端一次性设置角色成长状态。 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="PHY|Progression")
void SetProgression(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel);
/** @brief 等级或经验变化时通知本地 UI。 */
UPROPERTY(BlueprintAssignable, Category="PHY|Progression")
FPHYPlayerProgressionChangedSignature OnProgressionChanged;
protected: protected:
/** @brief 复制后通知本地 UI 刷新成长信息。 */
UFUNCTION()
void OnRep_Progression();
/** @brief 广播当前成长状态。 */
void BroadcastProgressionChanged();
/** @brief 玩家持久 ASC。 */ /** @brief 玩家持久 ASC。 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|PlayerState") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|PlayerState")
TObjectPtr<UGGA_AbilitySystemComponent> AbilitySystemComponent; TObjectPtr<UGGA_AbilitySystemComponent> AbilitySystemComponent;
@@ -70,4 +118,16 @@ protected:
/** @brief 玩家持久职业组件,职业不保存在 PlayerCharacter 上。 */ /** @brief 玩家持久职业组件,职业不保存在 PlayerCharacter 上。 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Class") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Class")
TObjectPtr<UPHYClassComponent> ClassComponent; TObjectPtr<UPHYClassComponent> ClassComponent;
/** @brief 当前角色等级,由服务端复制给客户端 UI。 */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing=OnRep_Progression, Category="PHY|Progression")
int32 PlayerLevel = 1;
/** @brief 当前经验值,由服务端复制给客户端 UI。 */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing=OnRep_Progression, Category="PHY|Progression")
int32 Experience = 0;
/** @brief 当前等级升到下一级所需经验值,由服务端复制给客户端 UI。 */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, ReplicatedUsing=OnRep_Progression, Category="PHY|Progression")
int32 ExperienceForNextLevel = 100;
}; };

View File

@@ -6,13 +6,17 @@
#include "UI/GUIS_ActivatableWidget.h" #include "UI/GUIS_ActivatableWidget.h"
#include "PHYHUDStatusWidget.generated.h" #include "PHYHUDStatusWidget.generated.h"
class APlayerController;
class APHYPlayerState;
class UAbilitySystemComponent;
class UProgressBar; class UProgressBar;
class UTextBlock; class UTextBlock;
struct FOnAttributeChangeData;
/** /**
* @brief PHY 默认 HUD 状态控件。 * @brief PHY 默认 HUD 状态控件。
* *
* 作为基础 UI 的 C++ 可见占位,后续战斗、背包、队伍等系统可以替换或扩展为项目资产 * 作为基础 UI 的 C++ 可见占位,绑定 PlayerState 上的 GAS 资源属性和成长状态
*/ */
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYHUDStatusWidget : public UGUIS_ActivatableWidget class PHY_API UPHYHUDStatusWidget : public UGUIS_ActivatableWidget
@@ -29,19 +33,69 @@ public:
/** @brief 同步文本和进度条显示。 */ /** @brief 同步文本和进度条显示。 */
virtual void SynchronizeProperties() override; virtual void SynchronizeProperties() override;
/** @brief 绑定本地玩家控制器,随后从 PlayerState 和 GAS 拉取状态。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void InitializeFromPlayer(APlayerController* InPlayerController);
/** @brief 从当前绑定来源重新拉取 PlayerState、ASC、资源和成长信息。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void RefreshFromBoundSources();
/** @brief 设置生命值显示。 */ /** @brief 设置生命值显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI") UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetHealth(float NewHealth, float NewMaxHealth); void SetHealth(float NewHealth, float NewMaxHealth);
/** @brief 设置法力值显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetMana(float NewMana, float NewMaxMana);
/** @brief 设置耐力值显示。 */ /** @brief 设置耐力值显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI") UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetStamina(float NewStamina, float NewMaxStamina); void SetStamina(float NewStamina, float NewMaxStamina);
/** @brief 设置等级和经验显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetProgression(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel);
protected: protected:
/** @brief Widget 构造后尝试自动绑定 OwningPlayer。 */
virtual void NativeConstruct() override;
/** @brief Widget 销毁前解除 GAS 与 PlayerState 委托。 */
virtual void NativeDestruct() override;
/** @brief PlayerState 延迟到达时重试绑定。 */
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
/** @brief 构造原生 HUD 树。 */ /** @brief 构造原生 HUD 树。 */
void BuildNativeWidgetTree(); void BuildNativeWidgetTree();
/** @brief 刷新单条资源文本和进度条。 */ /** @brief 绑定新的 PlayerState 成长状态来源。 */
void BindToPlayerState(APHYPlayerState* NewPlayerState);
/** @brief 解除 PlayerState 成长状态来源。 */
void UnbindFromPlayerState();
/** @brief 绑定新的 AbilitySystemComponent 属性来源。 */
void BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent);
/** @brief 解除 AbilitySystemComponent 属性来源。 */
void UnbindFromAbilitySystem();
/** @brief 从 GAS 当前数值刷新生命、法力、耐力。 */
void RefreshResourcesFromAbilitySystem();
/** @brief 从 PlayerState 当前数值刷新等级、经验。 */
void RefreshProgressionFromPlayerState();
/** @brief GAS 属性变化后刷新资源条。 */
void HandleAttributeChanged(const FOnAttributeChangeData& ChangeData);
/** @brief PlayerState 成长信息变化后刷新显示。 */
UFUNCTION()
void HandleProgressionChanged(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel);
/** @brief 刷新所有文本和进度条。 */
void UpdateResourceWidgets(); void UpdateResourceWidgets();
/** @brief 当前生命值。 */ /** @brief 当前生命值。 */
@@ -52,6 +106,14 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float MaxHealth = 100.0f; float MaxHealth = 100.0f;
/** @brief 当前法力值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float Mana = 100.0f;
/** @brief 当前最大法力值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float MaxMana = 100.0f;
/** @brief 当前耐力值。 */ /** @brief 当前耐力值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float Stamina = 100.0f; float Stamina = 100.0f;
@@ -60,10 +122,42 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float MaxStamina = 100.0f; float MaxStamina = 100.0f;
/** @brief 当前角色等级。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
int32 Level = 1;
/** @brief 当前经验值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
int32 Experience = 0;
/** @brief 当前等级升到下一级所需经验值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
int32 ExperienceForNextLevel = 100;
/** @brief 当前绑定的本地玩家控制器弱引用。 */
TWeakObjectPtr<APlayerController> BoundPlayerController;
/** @brief 当前绑定的玩家状态弱引用。 */
TWeakObjectPtr<APHYPlayerState> BoundPlayerState;
/** @brief 当前绑定的 ASC 弱引用。 */
TWeakObjectPtr<UAbilitySystemComponent> BoundAbilitySystemComponent;
FDelegateHandle HealthChangedHandle;
FDelegateHandle MaxHealthChangedHandle;
FDelegateHandle ManaChangedHandle;
FDelegateHandle MaxManaChangedHandle;
FDelegateHandle StaminaChangedHandle;
FDelegateHandle MaxStaminaChangedHandle;
/** @brief 标题文本。 */ /** @brief 标题文本。 */
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText; TObjectPtr<UTextBlock> TitleText;
/** @brief 等级文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LevelText;
/** @brief 生命文本。 */ /** @brief 生命文本。 */
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> HealthText; TObjectPtr<UTextBlock> HealthText;
@@ -72,6 +166,14 @@ protected:
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UProgressBar> HealthBar; TObjectPtr<UProgressBar> HealthBar;
/** @brief 法力文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ManaText;
/** @brief 法力进度条。 */
UPROPERTY(Transient)
TObjectPtr<UProgressBar> ManaBar;
/** @brief 耐力文本。 */ /** @brief 耐力文本。 */
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UTextBlock> StaminaText; TObjectPtr<UTextBlock> StaminaText;
@@ -79,4 +181,12 @@ protected:
/** @brief 耐力进度条。 */ /** @brief 耐力进度条。 */
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UProgressBar> StaminaBar; TObjectPtr<UProgressBar> StaminaBar;
/** @brief 经验文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ExperienceText;
/** @brief 经验进度条。 */
UPROPERTY(Transient)
TObjectPtr<UProgressBar> ExperienceBar;
}; };