diff --git a/Source/PHY/Private/Game/PHYHUD.cpp b/Source/PHY/Private/Game/PHYHUD.cpp index d2baacf..2df664e 100644 --- a/Source/PHY/Private/Game/PHYHUD.cpp +++ b/Source/PHY/Private/Game/PHYHUD.cpp @@ -109,5 +109,11 @@ UCommonActivatableWidget* APHYHUD::PushDefaultHUDWidget(APlayerController* Ownin } const FGameplayTag HUDLayerTag = (UISettings && UISettings->HUDLayerTag.IsValid()) ? UISettings->HUDLayerTag : PHYGameplayTags::UI_Layer_HUD; - return RootLayout->PushWidgetToLayerStack(HUDLayerTag, HUDWidgetClass); + UCommonActivatableWidget* PushedWidget = RootLayout->PushWidgetToLayerStack(HUDLayerTag, HUDWidgetClass); + if (UPHYHUDStatusWidget* StatusWidget = Cast(PushedWidget)) + { + StatusWidget->InitializeFromPlayer(OwningPlayerController); + } + + return PushedWidget; } diff --git a/Source/PHY/Private/Player/PHYPlayerState.cpp b/Source/PHY/Private/Player/PHYPlayerState.cpp index 0d703c5..da50c73 100644 --- a/Source/PHY/Private/Player/PHYPlayerState.cpp +++ b/Source/PHY/Private/Player/PHYPlayerState.cpp @@ -10,6 +10,7 @@ #include "Class/PHYClassComponent.h" #include "Class/PHYClassSettings.h" #include "GGA_AbilitySystemComponent.h" +#include "Net/UnrealNetwork.h" APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -28,7 +29,69 @@ APHYPlayerState::APHYPlayerState(const FObjectInitializer& ObjectInitializer) ClassComponent->SetClassTag(GetDefault()->DefaultPlayerClassTag); } +void APHYPlayerState::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(ThisClass, PlayerLevel); + DOREPLIFETIME(ThisClass, Experience); + DOREPLIFETIME(ThisClass, ExperienceForNextLevel); +} + UAbilitySystemComponent* APHYPlayerState::GetAbilitySystemComponent() const { 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); +} diff --git a/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp b/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp index 346e829..473727e 100644 --- a/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp +++ b/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp @@ -4,6 +4,8 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(PHYHUDStatusWidget) +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h" #include "Blueprint/WidgetTree.h" #include "Components/Overlay.h" #include "Components/OverlaySlot.h" @@ -13,6 +15,9 @@ #include "Components/VerticalBox.h" #include "Components/VerticalBoxSlot.h" #include "Fonts/SlateFontInfo.h" +#include "GameFramework/PlayerController.h" +#include "GameplayEffectTypes.h" +#include "Player/PHYPlayerState.h" #include "Styling/CoreStyle.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))); } + 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) { 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(CurrentExperience) / static_cast(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) @@ -47,6 +71,35 @@ void UPHYHUDStatusWidget::SynchronizeProperties() 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(); + BindToPlayerState(PHYPlayerState); + BindToAbilitySystem(PHYPlayerState ? PHYPlayerState->GetAbilitySystemComponent() : nullptr); +} + void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHealth) { Health = NewHealth; @@ -54,6 +107,13 @@ void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHea UpdateResourceWidgets(); } +void UPHYHUDStatusWidget::SetMana(const float NewMana, const float NewMaxMana) +{ + Mana = NewMana; + MaxMana = NewMaxMana; + UpdateResourceWidgets(); +} + void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxStamina) { Stamina = NewStamina; @@ -61,6 +121,54 @@ void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxS 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(); + if (CurrentPlayerState != BoundPlayerState.Get() || !BoundAbilitySystemComponent.IsValid()) + { + RefreshFromBoundSources(); + } +} + void UPHYHUDStatusWidget::BuildNativeWidgetTree() { if (!WidgetTree) @@ -68,7 +176,7 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree() WidgetTree = NewObject(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; } @@ -93,7 +201,15 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree() TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.85f, 0.95f, 1.0f, 1.0f))); 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::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::StaticClass(), TEXT("HealthText")); @@ -108,6 +224,18 @@ void UPHYHUDStatusWidget::BuildNativeWidgetTree() HealthBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f)); } + ManaText = WidgetTree->ConstructWidget(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::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::StaticClass(), TEXT("StaminaText")); StaminaText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16)); 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)); 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::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::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(); } +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() { + if (LevelText) + { + LevelText->SetText(FText::FromString(FString::Printf(TEXT("Lv %d"), FMath::Max(1, Level)))); + } if (HealthText) { HealthText->SetText(MakeResourceText(TEXT("HP"), Health, MaxHealth)); @@ -133,6 +393,14 @@ void UPHYHUDStatusWidget::UpdateResourceWidgets() { HealthBar->SetPercent(MakeResourcePercent(Health, MaxHealth)); } + if (ManaText) + { + ManaText->SetText(MakeResourceText(TEXT("MP"), Mana, MaxMana)); + } + if (ManaBar) + { + ManaBar->SetPercent(MakeResourcePercent(Mana, MaxMana)); + } if (StaminaText) { StaminaText->SetText(MakeResourceText(TEXT("ST"), Stamina, MaxStamina)); @@ -141,4 +409,12 @@ void UPHYHUDStatusWidget::UpdateResourceWidgets() { StaminaBar->SetPercent(MakeResourcePercent(Stamina, MaxStamina)); } + if (ExperienceText) + { + ExperienceText->SetText(MakeProgressionText(Experience, ExperienceForNextLevel)); + } + if (ExperienceBar) + { + ExperienceBar->SetPercent(MakeProgressionPercent(Experience, ExperienceForNextLevel)); + } } diff --git a/Source/PHY/Public/Player/PHYPlayerState.h b/Source/PHY/Public/Player/PHYPlayerState.h index 09f0f13..a27df9c 100644 --- a/Source/PHY/Public/Player/PHYPlayerState.h +++ b/Source/PHY/Public/Player/PHYPlayerState.h @@ -13,6 +13,8 @@ class UPHYCoreAttributeSet; class UPHYElementAttributeSet; class UPHYClassComponent; +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FPHYPlayerProgressionChangedSignature, int32, NewLevel, int32, NewExperience, int32, NewExperienceForNextLevel); + /** * @brief PHY 玩家状态。 * @@ -27,6 +29,9 @@ public: /** @brief 构造玩家状态与 ASC。 */ APHYPlayerState(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + /** @brief 注册 PlayerState 自身复制字段。 */ + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + /** @brief 获取玩家持久 AbilitySystemComponent。 */ virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; @@ -50,7 +55,50 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|Class") 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: + /** @brief 复制后通知本地 UI 刷新成长信息。 */ + UFUNCTION() + void OnRep_Progression(); + + /** @brief 广播当前成长状态。 */ + void BroadcastProgressionChanged(); + /** @brief 玩家持久 ASC。 */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|PlayerState") TObjectPtr AbilitySystemComponent; @@ -70,4 +118,16 @@ protected: /** @brief 玩家持久职业组件,职业不保存在 PlayerCharacter 上。 */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="PHY|Class") TObjectPtr 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; }; diff --git a/Source/PHY/Public/UI/PHYHUDStatusWidget.h b/Source/PHY/Public/UI/PHYHUDStatusWidget.h index f1ca125..661cada 100644 --- a/Source/PHY/Public/UI/PHYHUDStatusWidget.h +++ b/Source/PHY/Public/UI/PHYHUDStatusWidget.h @@ -6,13 +6,17 @@ #include "UI/GUIS_ActivatableWidget.h" #include "PHYHUDStatusWidget.generated.h" +class APlayerController; +class APHYPlayerState; +class UAbilitySystemComponent; class UProgressBar; class UTextBlock; +struct FOnAttributeChangeData; /** * @brief PHY 默认 HUD 状态控件。 * - * 作为基础 UI 的 C++ 可见占位,后续战斗、背包、队伍等系统可以替换或扩展为项目资产。 + * 作为基础 UI 的 C++ 可见占位,绑定 PlayerState 上的 GAS 资源属性和成长状态。 */ UCLASS(BlueprintType, Blueprintable) class PHY_API UPHYHUDStatusWidget : public UGUIS_ActivatableWidget @@ -29,19 +33,69 @@ public: /** @brief 同步文本和进度条显示。 */ 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 设置生命值显示。 */ UFUNCTION(BlueprintCallable, Category="PHY|UI") void SetHealth(float NewHealth, float NewMaxHealth); + /** @brief 设置法力值显示。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI") + void SetMana(float NewMana, float NewMaxMana); + /** @brief 设置耐力值显示。 */ UFUNCTION(BlueprintCallable, Category="PHY|UI") void SetStamina(float NewStamina, float NewMaxStamina); + /** @brief 设置等级和经验显示。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI") + void SetProgression(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel); + 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 树。 */ 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(); /** @brief 当前生命值。 */ @@ -52,6 +106,14 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") 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 当前耐力值。 */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") float Stamina = 100.0f; @@ -60,10 +122,42 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI") 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 BoundPlayerController; + + /** @brief 当前绑定的玩家状态弱引用。 */ + TWeakObjectPtr BoundPlayerState; + + /** @brief 当前绑定的 ASC 弱引用。 */ + TWeakObjectPtr BoundAbilitySystemComponent; + + FDelegateHandle HealthChangedHandle; + FDelegateHandle MaxHealthChangedHandle; + FDelegateHandle ManaChangedHandle; + FDelegateHandle MaxManaChangedHandle; + FDelegateHandle StaminaChangedHandle; + FDelegateHandle MaxStaminaChangedHandle; + /** @brief 标题文本。 */ UPROPERTY(Transient) TObjectPtr TitleText; + /** @brief 等级文本。 */ + UPROPERTY(Transient) + TObjectPtr LevelText; + /** @brief 生命文本。 */ UPROPERTY(Transient) TObjectPtr HealthText; @@ -72,6 +166,14 @@ protected: UPROPERTY(Transient) TObjectPtr HealthBar; + /** @brief 法力文本。 */ + UPROPERTY(Transient) + TObjectPtr ManaText; + + /** @brief 法力进度条。 */ + UPROPERTY(Transient) + TObjectPtr ManaBar; + /** @brief 耐力文本。 */ UPROPERTY(Transient) TObjectPtr StaminaText; @@ -79,4 +181,12 @@ protected: /** @brief 耐力进度条。 */ UPROPERTY(Transient) TObjectPtr StaminaBar; + + /** @brief 经验文本。 */ + UPROPERTY(Transient) + TObjectPtr ExperienceText; + + /** @brief 经验进度条。 */ + UPROPERTY(Transient) + TObjectPtr ExperienceBar; };