Add paged character attribute menu

This commit is contained in:
2026-04-26 20:53:55 +08:00
parent c4f185736f
commit 57363d9e7f
22 changed files with 1922 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ bUseGenericUISystem=True
bOpenMenusWithGameplayPause=False
RootLayoutClass="/Script/PHY.PHYGameUILayout"
DefaultHUDWidgetClass="/Script/PHY.PHYHUDStatusWidget"
AttributeMenuWidgetClass="/Script/PHY.PHYAttributeMenuWidget"
GameplayLayerTag=(TagName="UI.Layer.Game")
HUDLayerTag=(TagName="UI.Layer.HUD")
MenuLayerTag=(TagName="UI.Layer.Menu")

View File

@@ -11,6 +11,7 @@
#include "UI/GUIS_GameUILayout.h"
#include "UI/GUIS_GameUIPolicy.h"
#include "UI/GUIS_GameUISubsystem.h"
#include "UI/PHYAttributeMenuWidget.h"
#include "UI/PHYHUDStatusWidget.h"
#include "UI/PHYPlayerUIContext.h"
@@ -40,6 +41,38 @@ void APHYHUD::InitializeHUDForPlayer(APlayerController* OwningPlayerController)
}
}
UCommonActivatableWidget* APHYHUD::OpenAttributeMenu(APlayerController* OwningPlayerController)
{
APlayerController* TargetPlayerController = OwningPlayerController ? OwningPlayerController : GetOwningPlayerController();
if (!TargetPlayerController)
{
return nullptr;
}
if (!bHUDInitialized)
{
InitializeHUDForPlayer(TargetPlayerController);
}
const UPHYUISettings* UISettings = GetDefault<UPHYUISettings>();
TSubclassOf<UCommonActivatableWidget> AttributeMenuClass = UPHYAttributeMenuWidget::StaticClass();
if (UISettings && !UISettings->AttributeMenuWidgetClass.IsNull())
{
if (UClass* LoadedAttributeMenuClass = UISettings->AttributeMenuWidgetClass.LoadSynchronous())
{
AttributeMenuClass = LoadedAttributeMenuClass;
}
}
UCommonActivatableWidget* PushedWidget = PushMenuWidget(TargetPlayerController, AttributeMenuClass);
if (UPHYAttributeMenuWidget* AttributeMenuWidget = Cast<UPHYAttributeMenuWidget>(PushedWidget))
{
AttributeMenuWidget->InitializeFromPlayer(TargetPlayerController);
}
return PushedWidget;
}
bool APHYHUD::InitializeGenericUIForPlayer(APlayerController* OwningPlayerController)
{
if (!OwningPlayerController || !OwningPlayerController->IsLocalController())
@@ -117,3 +150,28 @@ UCommonActivatableWidget* APHYHUD::PushDefaultHUDWidget(APlayerController* Ownin
return PushedWidget;
}
UCommonActivatableWidget* APHYHUD::PushMenuWidget(APlayerController* OwningPlayerController, TSubclassOf<UCommonActivatableWidget> WidgetClass)
{
if (!OwningPlayerController || !WidgetClass)
{
return nullptr;
}
const UPHYUISettings* UISettings = GetDefault<UPHYUISettings>();
ULocalPlayer* LocalPlayer = OwningPlayerController->GetLocalPlayer();
if (!LocalPlayer)
{
return nullptr;
}
UGUIS_GameUIPolicy* UIPolicy = UGUIS_GameUIPolicy::GetGameUIPolicy(OwningPlayerController);
UGUIS_GameUILayout* RootLayout = UIPolicy ? UIPolicy->GetRootLayout(LocalPlayer) : nullptr;
if (!RootLayout)
{
return nullptr;
}
const FGameplayTag MenuLayerTag = (UISettings && UISettings->MenuLayerTag.IsValid()) ? UISettings->MenuLayerTag : PHYGameplayTags::UI_Layer_Menu;
return RootLayout->PushWidgetToLayerStack<UCommonActivatableWidget>(MenuLayerTag, WidgetClass);
}

View File

@@ -0,0 +1,106 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeGroupWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeGroupWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/CoreStyle.h"
#include "UI/PHYAttributeRowWidget.h"
#include "Widgets/SWidget.h"
UPHYAttributeGroupWidget::UPHYAttributeGroupWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYAttributeGroupWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributeGroupWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdateGroupWidgets();
}
void UPHYAttributeGroupWidget::SetGroupTitle(FText InGroupTitle)
{
GroupTitle = MoveTemp(InGroupTitle);
UpdateGroupWidgets();
}
void UPHYAttributeGroupWidget::ClearRows()
{
if (RowsBox)
{
RowsBox->ClearChildren();
}
}
UPHYAttributeRowWidget* UPHYAttributeGroupWidget::AddRow(FText Label, FText Value, const bool bCanUpgrade)
{
BuildNativeWidgetTree();
if (!RowsBox || !WidgetTree)
{
return nullptr;
}
UPHYAttributeRowWidget* RowWidget = WidgetTree->ConstructWidget<UPHYAttributeRowWidget>(UPHYAttributeRowWidget::StaticClass());
RowWidget->SetRow(MoveTemp(Label), MoveTemp(Value), bCanUpgrade);
if (UVerticalBoxSlot* RowSlot = RowsBox->AddChildToVerticalBox(RowWidget))
{
RowSlot->SetPadding(FMargin(0.0f, 1.0f));
}
return RowWidget;
}
void UPHYAttributeGroupWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && TitleText && RowsBox)
{
return;
}
UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass(), TEXT("RootBorder"));
RootBorder->SetBrushColor(FLinearColor(0.98f, 0.94f, 0.84f, 0.58f));
RootBorder->SetPadding(FMargin(14.0f, 10.0f));
WidgetTree->RootWidget = RootBorder;
UVerticalBox* RootBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("RootBox"));
RootBorder->SetContent(RootBox);
TitleText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("TitleText"));
TitleText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17));
TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.56f, 0.18f, 0.10f, 1.0f)));
if (UVerticalBoxSlot* TitleSlot = RootBox->AddChildToVerticalBox(TitleText))
{
TitleSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 6.0f));
}
RowsBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("RowsBox"));
RootBox->AddChildToVerticalBox(RowsBox);
UpdateGroupWidgets();
}
void UPHYAttributeGroupWidget::UpdateGroupWidgets()
{
if (TitleText)
{
TitleText->SetText(GroupTitle);
}
}

View File

@@ -0,0 +1,460 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeMenuWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeMenuWidget)
#include "AbilitySystemComponent.h"
#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h"
#include "AbilitySystem/Attributes/PHYCoreAttributeSet.h"
#include "AbilitySystem/Attributes/PHYElementAttributeSet.h"
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Button.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/SafeZone.h"
#include "Components/SizeBox.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/WidgetSwitcher.h"
#include "Fonts/SlateFontInfo.h"
#include "GameFramework/PlayerController.h"
#include "Player/PHYPlayerState.h"
#include "Styling/CoreStyle.h"
#include "UI/PHYAttributeOverviewPageWidget.h"
#include "UI/PHYAttributeSummaryWidget.h"
#include "UI/PHYCombatDerivedAttributePageWidget.h"
#include "UI/PHYElementDerivedAttributePageWidget.h"
#include "Widgets/SWidget.h"
namespace
{
TArray<FGameplayAttribute> MakeTrackedAttributes()
{
return {
UPHYCoreAttributeSet::GetStrengthAttribute(),
UPHYCoreAttributeSet::GetDexterityAttribute(),
UPHYCoreAttributeSet::GetVitalityAttribute(),
UPHYCoreAttributeSet::GetIntelligenceAttribute(),
UPHYCoreAttributeSet::GetSpiritAttribute(),
UPHYCoreAttributeSet::GetPerceptionAttribute(),
UPHYCombatAttributeSet::GetHealthAttribute(),
UPHYCombatAttributeSet::GetMaxHealthAttribute(),
UPHYCombatAttributeSet::GetManaAttribute(),
UPHYCombatAttributeSet::GetMaxManaAttribute(),
UPHYCombatAttributeSet::GetStaminaAttribute(),
UPHYCombatAttributeSet::GetMaxStaminaAttribute(),
UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute(),
UPHYCombatAttributeSet::GetSpellPowerAttribute(),
UPHYCombatAttributeSet::GetArmorAttribute(),
UPHYCombatAttributeSet::GetMagicResistanceAttribute(),
UPHYCombatAttributeSet::GetAccuracyAttribute(),
UPHYCombatAttributeSet::GetEvasionAttribute(),
UPHYCombatAttributeSet::GetCriticalChanceAttribute(),
UPHYCombatAttributeSet::GetCriticalDamageAttribute(),
UPHYCombatAttributeSet::GetAttackSpeedAttribute(),
UPHYCombatAttributeSet::GetCooldownReductionAttribute(),
UPHYCombatAttributeSet::GetBlockPowerAttribute(),
UPHYCombatAttributeSet::GetGuardBreakPowerAttribute(),
UPHYCombatAttributeSet::GetPoiseAttribute(),
UPHYCombatAttributeSet::GetPoiseDamageAttribute(),
UPHYElementAttributeSet::GetFireDamageBonusAttribute(),
UPHYElementAttributeSet::GetWaterDamageBonusAttribute(),
UPHYElementAttributeSet::GetIceDamageBonusAttribute(),
UPHYElementAttributeSet::GetLightningDamageBonusAttribute(),
UPHYElementAttributeSet::GetEarthDamageBonusAttribute(),
UPHYElementAttributeSet::GetWindDamageBonusAttribute(),
UPHYElementAttributeSet::GetLightDamageBonusAttribute(),
UPHYElementAttributeSet::GetDarkDamageBonusAttribute(),
UPHYElementAttributeSet::GetFireResistanceAttribute(),
UPHYElementAttributeSet::GetWaterResistanceAttribute(),
UPHYElementAttributeSet::GetIceResistanceAttribute(),
UPHYElementAttributeSet::GetLightningResistanceAttribute(),
UPHYElementAttributeSet::GetEarthResistanceAttribute(),
UPHYElementAttributeSet::GetWindResistanceAttribute(),
UPHYElementAttributeSet::GetLightResistanceAttribute(),
UPHYElementAttributeSet::GetDarkResistanceAttribute(),
UPHYElementAttributeSet::GetElementPenetrationAttribute()
};
}
}
UPHYAttributeMenuWidget::UPHYAttributeMenuWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
InputConfig = EGUIS_ActivatableWidgetInputMode::Menu;
SetIsBackHandler(true);
}
TSharedRef<SWidget> UPHYAttributeMenuWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributeMenuWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
RefreshAttributePages();
UpdateTabVisuals();
}
void UPHYAttributeMenuWidget::InitializeFromPlayer(APlayerController* InPlayerController)
{
if (!InPlayerController)
{
BoundPlayerController.Reset();
UnbindFromPlayerState();
UnbindFromAbilitySystem();
RefreshAttributePages();
return;
}
BoundPlayerController = InPlayerController;
RefreshFromBoundSources();
}
void UPHYAttributeMenuWidget::RefreshFromBoundSources()
{
APlayerController* PlayerController = BoundPlayerController.Get();
if (!PlayerController)
{
UnbindFromPlayerState();
UnbindFromAbilitySystem();
RefreshAttributePages();
return;
}
APHYPlayerState* PHYPlayerState = PlayerController->GetPlayerState<APHYPlayerState>();
BindToPlayerState(PHYPlayerState);
BindToAbilitySystem(PHYPlayerState ? PHYPlayerState->GetAbilitySystemComponent() : nullptr);
RefreshAttributePages();
}
void UPHYAttributeMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (!BoundPlayerController.IsValid())
{
InitializeFromPlayer(GetOwningPlayer());
}
else
{
RefreshFromBoundSources();
}
}
void UPHYAttributeMenuWidget::NativeDestruct()
{
UnbindFromPlayerState();
UnbindFromAbilitySystem();
BoundPlayerController.Reset();
Super::NativeDestruct();
}
void UPHYAttributeMenuWidget::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 UPHYAttributeMenuWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && SummaryWidget && PageSwitcher && OverviewPage && CombatPage && ElementPage)
{
return;
}
USafeZone* RootSafeZone = WidgetTree->ConstructWidget<USafeZone>(USafeZone::StaticClass(), TEXT("RootSafeZone"));
WidgetTree->RootWidget = RootSafeZone;
UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass(), TEXT("RootBorder"));
RootBorder->SetBrushColor(FLinearColor(0.83f, 0.78f, 0.64f, 0.94f));
RootBorder->SetPadding(FMargin(28.0f));
RootSafeZone->SetContent(RootBorder);
UHorizontalBox* MainBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("MainBox"));
RootBorder->SetContent(MainBox);
SummaryWidget = WidgetTree->ConstructWidget<UPHYAttributeSummaryWidget>(UPHYAttributeSummaryWidget::StaticClass(), TEXT("SummaryWidget"));
USizeBox* SummarySizeBox = WidgetTree->ConstructWidget<USizeBox>(USizeBox::StaticClass(), TEXT("SummarySizeBox"));
SummarySizeBox->SetWidthOverride(360.0f);
SummarySizeBox->AddChild(SummaryWidget);
if (UHorizontalBoxSlot* SummarySlot = MainBox->AddChildToHorizontalBox(SummarySizeBox))
{
SummarySlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic));
SummarySlot->SetPadding(FMargin(0.0f, 0.0f, 18.0f, 0.0f));
}
UBorder* PageBorder = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass(), TEXT("PageBorder"));
PageBorder->SetBrushColor(FLinearColor(0.96f, 0.92f, 0.82f, 0.72f));
PageBorder->SetPadding(FMargin(22.0f));
if (UHorizontalBoxSlot* PageSlot = MainBox->AddChildToHorizontalBox(PageBorder))
{
PageSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
UVerticalBox* PageBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("PageBox"));
PageBorder->SetContent(PageBox);
UTextBlock* HeaderText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("HeaderText"));
HeaderText->SetText(FText::FromString(TEXT("角色属性")));
HeaderText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 28));
HeaderText->SetColorAndOpacity(FSlateColor(FLinearColor(0.35f, 0.13f, 0.06f, 1.0f)));
PageBox->AddChildToVerticalBox(HeaderText);
UHorizontalBox* TabBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("TabBox"));
if (UVerticalBoxSlot* TabSlot = PageBox->AddChildToVerticalBox(TabBox))
{
TabSlot->SetPadding(FMargin(0.0f, 12.0f, 0.0f, 16.0f));
}
OverviewTabButton = CreateTabButton(TEXT("OverviewTabButton"), FText::FromString(TEXT("总览")));
CombatTabButton = CreateTabButton(TEXT("CombatTabButton"), FText::FromString(TEXT("战斗衍生")));
ElementTabButton = CreateTabButton(TEXT("ElementTabButton"), FText::FromString(TEXT("元素衍生")));
TabBox->AddChildToHorizontalBox(OverviewTabButton);
if (UHorizontalBoxSlot* CombatTabSlot = TabBox->AddChildToHorizontalBox(CombatTabButton))
{
CombatTabSlot->SetPadding(FMargin(8.0f, 0.0f, 0.0f, 0.0f));
}
if (UHorizontalBoxSlot* ElementTabSlot = TabBox->AddChildToHorizontalBox(ElementTabButton))
{
ElementTabSlot->SetPadding(FMargin(8.0f, 0.0f, 0.0f, 0.0f));
}
PageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(UWidgetSwitcher::StaticClass(), TEXT("PageSwitcher"));
if (UVerticalBoxSlot* SwitcherSlot = PageBox->AddChildToVerticalBox(PageSwitcher))
{
SwitcherSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
OverviewPage = WidgetTree->ConstructWidget<UPHYAttributeOverviewPageWidget>(UPHYAttributeOverviewPageWidget::StaticClass(), TEXT("OverviewPage"));
CombatPage = WidgetTree->ConstructWidget<UPHYCombatDerivedAttributePageWidget>(UPHYCombatDerivedAttributePageWidget::StaticClass(), TEXT("CombatPage"));
ElementPage = WidgetTree->ConstructWidget<UPHYElementDerivedAttributePageWidget>(UPHYElementDerivedAttributePageWidget::StaticClass(), TEXT("ElementPage"));
PageSwitcher->AddChild(OverviewPage);
PageSwitcher->AddChild(CombatPage);
PageSwitcher->AddChild(ElementPage);
OverviewTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleOverviewTabClicked);
CombatTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleCombatTabClicked);
ElementTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleElementTabClicked);
SetActivePage(ActivePageIndex);
RefreshAttributePages();
}
UButton* UPHYAttributeMenuWidget::CreateTabButton(const FName ButtonName, const FText Label)
{
UButton* TabButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), ButtonName);
TabButton->SetBackgroundColor(FLinearColor(0.25f, 0.48f, 0.42f, 0.78f));
UTextBlock* TabText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
TabText->SetText(Label);
TabText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
TabText->SetColorAndOpacity(FSlateColor(FLinearColor(0.98f, 0.94f, 0.84f, 1.0f)));
TabButton->SetContent(TabText);
return TabButton;
}
void UPHYAttributeMenuWidget::SetActivePage(const int32 NewPageIndex)
{
ActivePageIndex = FMath::Clamp(NewPageIndex, 0, 2);
if (PageSwitcher)
{
PageSwitcher->SetActiveWidgetIndex(ActivePageIndex);
}
UpdateTabVisuals();
}
void UPHYAttributeMenuWidget::UpdateTabVisuals()
{
const FLinearColor ActiveColor(0.68f, 0.21f, 0.12f, 0.92f);
const FLinearColor InactiveColor(0.25f, 0.48f, 0.42f, 0.78f);
if (OverviewTabButton)
{
OverviewTabButton->SetBackgroundColor(ActivePageIndex == 0 ? ActiveColor : InactiveColor);
}
if (CombatTabButton)
{
CombatTabButton->SetBackgroundColor(ActivePageIndex == 1 ? ActiveColor : InactiveColor);
}
if (ElementTabButton)
{
ElementTabButton->SetBackgroundColor(ActivePageIndex == 2 ? ActiveColor : InactiveColor);
}
}
void UPHYAttributeMenuWidget::BindToPlayerState(APHYPlayerState* NewPlayerState)
{
if (BoundPlayerState.Get() == NewPlayerState)
{
return;
}
UnbindFromPlayerState();
BoundPlayerState = NewPlayerState;
if (NewPlayerState)
{
NewPlayerState->OnProgressionChanged.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleProgressionChanged);
}
}
void UPHYAttributeMenuWidget::UnbindFromPlayerState()
{
if (APHYPlayerState* PlayerState = BoundPlayerState.Get())
{
PlayerState->OnProgressionChanged.RemoveDynamic(this, &UPHYAttributeMenuWidget::HandleProgressionChanged);
}
BoundPlayerState.Reset();
}
void UPHYAttributeMenuWidget::BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent)
{
if (BoundAbilitySystemComponent.Get() == NewAbilitySystemComponent && AttributeChangedHandles.Num() > 0)
{
return;
}
UnbindFromAbilitySystem();
BoundAbilitySystemComponent = NewAbilitySystemComponent;
if (!NewAbilitySystemComponent)
{
return;
}
BoundAttributes = MakeTrackedAttributes();
AttributeChangedHandles.Reserve(BoundAttributes.Num());
for (const FGameplayAttribute& Attribute : BoundAttributes)
{
AttributeChangedHandles.Add(NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(this, &UPHYAttributeMenuWidget::HandleAttributeChanged));
}
}
void UPHYAttributeMenuWidget::UnbindFromAbilitySystem()
{
if (UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get())
{
for (int32 Index = 0; Index < BoundAttributes.Num() && Index < AttributeChangedHandles.Num(); ++Index)
{
if (AttributeChangedHandles[Index].IsValid())
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(BoundAttributes[Index]).Remove(AttributeChangedHandles[Index]);
}
}
}
AttributeChangedHandles.Reset();
BoundAttributes.Reset();
BoundAbilitySystemComponent.Reset();
}
void UPHYAttributeMenuWidget::RefreshAttributePages()
{
if (SummaryWidget)
{
const APHYPlayerState* PlayerState = BoundPlayerState.Get();
const FString PlayerName = PlayerState ? PlayerState->GetPlayerName() : TEXT("修行者");
const int32 Level = PlayerState ? PlayerState->GetPlayerLevel() : 1;
const int32 Experience = PlayerState ? PlayerState->GetExperience() : 0;
const int32 ExperienceForNextLevel = PlayerState ? PlayerState->GetExperienceForNextLevel() : 100;
SummaryWidget->SetSummary(FText::FromString(PlayerName), Level, Experience, ExperienceForNextLevel, CalculateDisplayCombatPower());
SummaryWidget->SetResources(
GetAttributeValue(UPHYCombatAttributeSet::GetHealthAttribute()),
GetAttributeValue(UPHYCombatAttributeSet::GetMaxHealthAttribute()),
GetAttributeValue(UPHYCombatAttributeSet::GetManaAttribute()),
GetAttributeValue(UPHYCombatAttributeSet::GetMaxManaAttribute()),
GetAttributeValue(UPHYCombatAttributeSet::GetStaminaAttribute()),
GetAttributeValue(UPHYCombatAttributeSet::GetMaxStaminaAttribute()));
}
UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get();
if (OverviewPage)
{
OverviewPage->RefreshAttributes(AbilitySystemComponent);
}
if (CombatPage)
{
CombatPage->RefreshAttributes(AbilitySystemComponent);
}
if (ElementPage)
{
ElementPage->RefreshAttributes(AbilitySystemComponent);
}
}
void UPHYAttributeMenuWidget::HandleAttributeChanged(const FOnAttributeChangeData& /*ChangeData*/)
{
RefreshAttributePages();
}
void UPHYAttributeMenuWidget::HandleProgressionChanged(const int32 /*NewLevel*/, const int32 /*NewExperience*/, const int32 /*NewExperienceForNextLevel*/)
{
RefreshAttributePages();
}
void UPHYAttributeMenuWidget::HandleOverviewTabClicked()
{
SetActivePage(0);
}
void UPHYAttributeMenuWidget::HandleCombatTabClicked()
{
SetActivePage(1);
}
void UPHYAttributeMenuWidget::HandleElementTabClicked()
{
SetActivePage(2);
}
float UPHYAttributeMenuWidget::GetAttributeValue(const FGameplayAttribute& Attribute) const
{
UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get();
return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f;
}
int32 UPHYAttributeMenuWidget::CalculateDisplayCombatPower() const
{
const float OffensivePower =
GetAttributeValue(UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetSpellPowerAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetGuardBreakPowerAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetPoiseDamageAttribute());
const float DefensivePower =
GetAttributeValue(UPHYCombatAttributeSet::GetArmorAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetMagicResistanceAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetBlockPowerAttribute()) +
GetAttributeValue(UPHYCombatAttributeSet::GetPoiseAttribute());
const float ResourcePower =
GetAttributeValue(UPHYCombatAttributeSet::GetMaxHealthAttribute()) * 0.20f +
GetAttributeValue(UPHYCombatAttributeSet::GetMaxManaAttribute()) * 0.10f +
GetAttributeValue(UPHYCombatAttributeSet::GetMaxStaminaAttribute()) * 0.10f;
return FMath::RoundToInt(OffensivePower + DefensivePower + ResourcePower);
}

View File

@@ -0,0 +1,55 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeOverviewPageWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeOverviewPageWidget)
#include "AbilitySystemComponent.h"
#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h"
#include "AbilitySystem/Attributes/PHYCoreAttributeSet.h"
#include "UI/PHYAttributeGroupWidget.h"
namespace
{
float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute)
{
return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f;
}
FText MakeNumberText(const float Value)
{
return FText::FromString(FString::Printf(TEXT("%.0f"), Value));
}
FText MakePairText(const float CurrentValue, const float MaxValue)
{
return FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), FMath::Max(0.0f, CurrentValue), FMath::Max(0.0f, MaxValue)));
}
void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value, const bool bCanUpgrade = false)
{
if (GroupWidget)
{
GroupWidget->AddRow(FText::FromString(Label), Value, bCanUpgrade);
}
}
}
void UPHYAttributeOverviewPageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent)
{
SetPageTitle(FText::FromString(TEXT("属性总览")));
ClearGroups();
UPHYAttributeGroupWidget* CoreGroup = AddGroup(FText::FromString(TEXT("核心属性")));
AddAttributeRow(CoreGroup, TEXT("力量"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetStrengthAttribute())), true);
AddAttributeRow(CoreGroup, TEXT("敏捷"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetDexterityAttribute())), true);
AddAttributeRow(CoreGroup, TEXT("体质"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetVitalityAttribute())), true);
AddAttributeRow(CoreGroup, TEXT("智力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetIntelligenceAttribute())), true);
AddAttributeRow(CoreGroup, TEXT("精神"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetSpiritAttribute())), true);
AddAttributeRow(CoreGroup, TEXT("感知"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetPerceptionAttribute())), true);
UPHYAttributeGroupWidget* ResourceGroup = AddGroup(FText::FromString(TEXT("战斗资源")));
AddAttributeRow(ResourceGroup, TEXT("生命值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetHealthAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxHealthAttribute())));
AddAttributeRow(ResourceGroup, TEXT("法力值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetManaAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxManaAttribute())));
AddAttributeRow(ResourceGroup, TEXT("耐力值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetStaminaAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxStaminaAttribute())));
}

View File

@@ -0,0 +1,111 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributePageWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributePageWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/ScrollBox.h"
#include "Components/ScrollBoxSlot.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/CoreStyle.h"
#include "UI/PHYAttributeGroupWidget.h"
#include "Widgets/SWidget.h"
UPHYAttributePageWidget::UPHYAttributePageWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYAttributePageWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributePageWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdatePageWidgets();
}
void UPHYAttributePageWidget::SetPageTitle(FText InPageTitle)
{
PageTitle = MoveTemp(InPageTitle);
UpdatePageWidgets();
}
void UPHYAttributePageWidget::ClearGroups()
{
if (GroupsBox)
{
GroupsBox->ClearChildren();
}
}
UPHYAttributeGroupWidget* UPHYAttributePageWidget::AddGroup(FText GroupTitle)
{
BuildNativeWidgetTree();
if (!GroupsBox || !WidgetTree)
{
return nullptr;
}
UPHYAttributeGroupWidget* GroupWidget = WidgetTree->ConstructWidget<UPHYAttributeGroupWidget>(UPHYAttributeGroupWidget::StaticClass());
GroupWidget->SetGroupTitle(MoveTemp(GroupTitle));
if (UVerticalBoxSlot* GroupSlot = GroupsBox->AddChildToVerticalBox(GroupWidget))
{
GroupSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 12.0f));
}
return GroupWidget;
}
void UPHYAttributePageWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && TitleText && GroupsScrollBox && GroupsBox)
{
return;
}
UVerticalBox* RootBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("RootBox"));
WidgetTree->RootWidget = RootBox;
TitleText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("TitleText"));
TitleText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 22));
TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.42f, 0.18f, 0.08f, 1.0f)));
if (UVerticalBoxSlot* TitleSlot = RootBox->AddChildToVerticalBox(TitleText))
{
TitleSlot->SetPadding(FMargin(2.0f, 0.0f, 0.0f, 12.0f));
}
GroupsScrollBox = WidgetTree->ConstructWidget<UScrollBox>(UScrollBox::StaticClass(), TEXT("GroupsScrollBox"));
if (UVerticalBoxSlot* ScrollSlot = RootBox->AddChildToVerticalBox(GroupsScrollBox))
{
ScrollSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
GroupsBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("GroupsBox"));
if (UScrollBoxSlot* GroupsSlot = Cast<UScrollBoxSlot>(GroupsScrollBox->AddChild(GroupsBox)))
{
GroupsSlot->SetPadding(FMargin(0.0f));
}
UpdatePageWidgets();
}
void UPHYAttributePageWidget::UpdatePageWidgets()
{
if (TitleText)
{
TitleText->SetText(PageTitle);
}
}

View File

@@ -0,0 +1,110 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeResourceBarWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeResourceBarWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/CoreStyle.h"
#include "Widgets/SWidget.h"
namespace
{
float MakePercent(const float CurrentValue, const float MaxValue)
{
return MaxValue > UE_SMALL_NUMBER ? FMath::Clamp(CurrentValue / MaxValue, 0.0f, 1.0f) : 0.0f;
}
}
UPHYAttributeResourceBarWidget::UPHYAttributeResourceBarWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYAttributeResourceBarWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributeResourceBarWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdateResourceWidgets();
}
void UPHYAttributeResourceBarWidget::SetResource(FText InLabel, const float InCurrentValue, const float InMaxValue, const FLinearColor InBarTint)
{
Label = MoveTemp(InLabel);
CurrentValue = InCurrentValue;
MaxValue = FMath::Max(1.0f, InMaxValue);
BarTint = InBarTint;
UpdateResourceWidgets();
}
void UPHYAttributeResourceBarWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && LabelText && ValueText && ResourceBar)
{
return;
}
UVerticalBox* RootBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("RootBox"));
WidgetTree->RootWidget = RootBox;
UHorizontalBox* HeaderBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("HeaderBox"));
if (UVerticalBoxSlot* HeaderSlot = RootBox->AddChildToVerticalBox(HeaderBox))
{
HeaderSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 2.0f));
}
LabelText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("LabelText"));
LabelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14));
LabelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.30f, 0.26f, 0.20f, 1.0f)));
if (UHorizontalBoxSlot* LabelSlot = HeaderBox->AddChildToHorizontalBox(LabelText))
{
LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
ValueText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ValueText"));
ValueText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14));
ValueText->SetColorAndOpacity(FSlateColor(FLinearColor(0.10f, 0.26f, 0.23f, 1.0f)));
HeaderBox->AddChildToHorizontalBox(ValueText);
ResourceBar = WidgetTree->ConstructWidget<UProgressBar>(UProgressBar::StaticClass(), TEXT("ResourceBar"));
if (UVerticalBoxSlot* BarSlot = RootBox->AddChildToVerticalBox(ResourceBar))
{
BarSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 7.0f));
}
UpdateResourceWidgets();
}
void UPHYAttributeResourceBarWidget::UpdateResourceWidgets()
{
if (LabelText)
{
LabelText->SetText(Label);
}
if (ValueText)
{
ValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), FMath::Max(0.0f, CurrentValue), FMath::Max(1.0f, MaxValue))));
}
if (ResourceBar)
{
ResourceBar->SetPercent(MakePercent(CurrentValue, MaxValue));
ResourceBar->SetFillColorAndOpacity(BarTint);
}
}

View File

@@ -0,0 +1,107 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeRowWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeRowWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/Button.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Components/TextBlock.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/CoreStyle.h"
#include "Widgets/SWidget.h"
UPHYAttributeRowWidget::UPHYAttributeRowWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYAttributeRowWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributeRowWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdateRowWidgets();
}
void UPHYAttributeRowWidget::SetRow(FText InLabel, FText InValue, const bool bInCanUpgrade)
{
Label = MoveTemp(InLabel);
Value = MoveTemp(InValue);
bCanUpgrade = bInCanUpgrade;
UpdateRowWidgets();
}
void UPHYAttributeRowWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && RowBox && LabelText && ValueText && UpgradeButton && UpgradeText)
{
return;
}
RowBox = WidgetTree->ConstructWidget<UHorizontalBox>(UHorizontalBox::StaticClass(), TEXT("RowBox"));
WidgetTree->RootWidget = RowBox;
LabelText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("LabelText"));
LabelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 15));
LabelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.28f, 0.25f, 0.20f, 1.0f)));
if (UHorizontalBoxSlot* LabelSlot = RowBox->AddChildToHorizontalBox(LabelText))
{
LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
LabelSlot->SetPadding(FMargin(0.0f, 4.0f, 10.0f, 4.0f));
LabelSlot->SetVerticalAlignment(VAlign_Center);
}
ValueText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("ValueText"));
ValueText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 15));
ValueText->SetColorAndOpacity(FSlateColor(FLinearColor(0.08f, 0.24f, 0.22f, 1.0f)));
if (UHorizontalBoxSlot* ValueSlot = RowBox->AddChildToHorizontalBox(ValueText))
{
ValueSlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic));
ValueSlot->SetPadding(FMargin(0.0f, 4.0f, 8.0f, 4.0f));
ValueSlot->SetVerticalAlignment(VAlign_Center);
}
UpgradeButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("UpgradeButton"));
UpgradeButton->SetBackgroundColor(FLinearColor(0.67f, 0.16f, 0.10f, 0.85f));
UpgradeText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("UpgradeText"));
UpgradeText->SetText(FText::FromString(TEXT("+")));
UpgradeText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14));
UpgradeText->SetColorAndOpacity(FSlateColor(FLinearColor(0.98f, 0.92f, 0.78f, 1.0f)));
UpgradeButton->SetContent(UpgradeText);
if (UHorizontalBoxSlot* UpgradeSlot = RowBox->AddChildToHorizontalBox(UpgradeButton))
{
UpgradeSlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic));
UpgradeSlot->SetPadding(FMargin(0.0f, 3.0f, 0.0f, 3.0f));
UpgradeSlot->SetVerticalAlignment(VAlign_Center);
}
UpdateRowWidgets();
}
void UPHYAttributeRowWidget::UpdateRowWidgets()
{
if (LabelText)
{
LabelText->SetText(Label);
}
if (ValueText)
{
ValueText->SetText(Value);
}
if (UpgradeButton)
{
UpgradeButton->SetVisibility(bCanUpgrade ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}
}

View File

@@ -0,0 +1,153 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYAttributeSummaryWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeSummaryWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/Border.h"
#include "Components/Spacer.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/CoreStyle.h"
#include "UI/PHYAttributeResourceBarWidget.h"
#include "Widgets/SWidget.h"
UPHYAttributeSummaryWidget::UPHYAttributeSummaryWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYAttributeSummaryWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYAttributeSummaryWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdateSummaryWidgets();
}
void UPHYAttributeSummaryWidget::SetSummary(FText InPlayerName, const int32 InLevel, const int32 InExperience, const int32 InExperienceForNextLevel, const int32 InCombatPower)
{
PlayerName = MoveTemp(InPlayerName);
Level = FMath::Max(1, InLevel);
Experience = FMath::Max(0, InExperience);
ExperienceForNextLevel = FMath::Max(1, InExperienceForNextLevel);
CombatPower = FMath::Max(0, InCombatPower);
UpdateSummaryWidgets();
}
void UPHYAttributeSummaryWidget::SetResources(const float InHealth, const float InMaxHealth, const float InMana, const float InMaxMana, const float InStamina, const float InMaxStamina)
{
Health = InHealth;
MaxHealth = FMath::Max(1.0f, InMaxHealth);
Mana = InMana;
MaxMana = FMath::Max(1.0f, InMaxMana);
Stamina = InStamina;
MaxStamina = FMath::Max(1.0f, InMaxStamina);
UpdateSummaryWidgets();
}
void UPHYAttributeSummaryWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && PlayerNameText && LevelText && CombatPowerText && ExperienceBar && HealthBar && ManaBar && StaminaBar)
{
return;
}
UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(UBorder::StaticClass(), TEXT("RootBorder"));
RootBorder->SetBrushColor(FLinearColor(0.92f, 0.86f, 0.70f, 0.55f));
RootBorder->SetPadding(FMargin(18.0f));
WidgetTree->RootWidget = RootBorder;
UVerticalBox* RootBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("RootBox"));
RootBorder->SetContent(RootBox);
PlayerNameText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("PlayerNameText"));
PlayerNameText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 24));
PlayerNameText->SetColorAndOpacity(FSlateColor(FLinearColor(0.28f, 0.16f, 0.08f, 1.0f)));
RootBox->AddChildToVerticalBox(PlayerNameText);
LevelText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("LevelText"));
LevelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17));
LevelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.14f, 0.34f, 0.30f, 1.0f)));
if (UVerticalBoxSlot* LevelSlot = RootBox->AddChildToVerticalBox(LevelText))
{
LevelSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f));
}
CombatPowerText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("CombatPowerText"));
CombatPowerText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17));
CombatPowerText->SetColorAndOpacity(FSlateColor(FLinearColor(0.60f, 0.34f, 0.08f, 1.0f)));
if (UVerticalBoxSlot* PowerSlot = RootBox->AddChildToVerticalBox(CombatPowerText))
{
PowerSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 14.0f));
}
UTextBlock* PortraitPlaceholder = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("PortraitPlaceholder"));
PortraitPlaceholder->SetText(FText::FromString(TEXT("云纹角色剪影")));
PortraitPlaceholder->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 20));
PortraitPlaceholder->SetColorAndOpacity(FSlateColor(FLinearColor(0.38f, 0.45f, 0.38f, 0.85f)));
if (UVerticalBoxSlot* PortraitSlot = RootBox->AddChildToVerticalBox(PortraitPlaceholder))
{
PortraitSlot->SetPadding(FMargin(0.0f, 22.0f, 0.0f, 24.0f));
PortraitSlot->SetHorizontalAlignment(HAlign_Center);
}
ResourceBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("ResourceBox"));
RootBox->AddChildToVerticalBox(ResourceBox);
ExperienceBar = WidgetTree->ConstructWidget<UPHYAttributeResourceBarWidget>(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("ExperienceBar"));
HealthBar = WidgetTree->ConstructWidget<UPHYAttributeResourceBarWidget>(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("HealthBar"));
ManaBar = WidgetTree->ConstructWidget<UPHYAttributeResourceBarWidget>(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("ManaBar"));
StaminaBar = WidgetTree->ConstructWidget<UPHYAttributeResourceBarWidget>(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("StaminaBar"));
ResourceBox->AddChildToVerticalBox(ExperienceBar);
ResourceBox->AddChildToVerticalBox(HealthBar);
ResourceBox->AddChildToVerticalBox(ManaBar);
ResourceBox->AddChildToVerticalBox(StaminaBar);
UpdateSummaryWidgets();
}
void UPHYAttributeSummaryWidget::UpdateSummaryWidgets()
{
if (PlayerNameText)
{
PlayerNameText->SetText(PlayerName.IsEmpty() ? FText::FromString(TEXT("修行者")) : PlayerName);
}
if (LevelText)
{
LevelText->SetText(FText::FromString(FString::Printf(TEXT("等级 %d"), Level)));
}
if (CombatPowerText)
{
CombatPowerText->SetText(FText::FromString(FString::Printf(TEXT("战力 %d"), CombatPower)));
}
if (ExperienceBar)
{
ExperienceBar->SetResource(FText::FromString(TEXT("经验")), static_cast<float>(Experience), static_cast<float>(ExperienceForNextLevel), FLinearColor(0.80f, 0.55f, 0.18f, 1.0f));
}
if (HealthBar)
{
HealthBar->SetResource(FText::FromString(TEXT("生命")), Health, MaxHealth, FLinearColor(0.72f, 0.16f, 0.12f, 1.0f));
}
if (ManaBar)
{
ManaBar->SetResource(FText::FromString(TEXT("法力")), Mana, MaxMana, FLinearColor(0.22f, 0.38f, 0.86f, 1.0f));
}
if (StaminaBar)
{
StaminaBar->SetResource(FText::FromString(TEXT("耐力")), Stamina, MaxStamina, FLinearColor(0.18f, 0.60f, 0.54f, 1.0f));
}
}

View File

@@ -0,0 +1,72 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYCombatDerivedAttributePageWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYCombatDerivedAttributePageWidget)
#include "AbilitySystemComponent.h"
#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h"
#include "UI/PHYAttributeGroupWidget.h"
namespace
{
float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute)
{
return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f;
}
FText MakeNumberText(const float Value)
{
return FText::FromString(FString::Printf(TEXT("%.0f"), Value));
}
FText MakePercentText(const float Value)
{
return FText::FromString(FString::Printf(TEXT("%.1f%%"), Value * 100.0f));
}
FText MakeMultiplierText(const float Value)
{
return FText::FromString(FString::Printf(TEXT("x%.2f"), Value));
}
void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value)
{
if (GroupWidget)
{
GroupWidget->AddRow(FText::FromString(Label), Value);
}
}
}
void UPHYCombatDerivedAttributePageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent)
{
SetPageTitle(FText::FromString(TEXT("战斗衍生属性")));
ClearGroups();
UPHYAttributeGroupWidget* ResourceGroup = AddGroup(FText::FromString(TEXT("资源上限")));
AddAttributeRow(ResourceGroup, TEXT("当前生命"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetHealthAttribute())));
AddAttributeRow(ResourceGroup, TEXT("生命上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxHealthAttribute())));
AddAttributeRow(ResourceGroup, TEXT("当前法力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetManaAttribute())));
AddAttributeRow(ResourceGroup, TEXT("法力上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxManaAttribute())));
AddAttributeRow(ResourceGroup, TEXT("当前耐力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetStaminaAttribute())));
AddAttributeRow(ResourceGroup, TEXT("耐力上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxStaminaAttribute())));
UPHYAttributeGroupWidget* AttackGroup = AddGroup(FText::FromString(TEXT("攻击能力")));
AddAttributeRow(AttackGroup, TEXT("物理攻击"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute())));
AddAttributeRow(AttackGroup, TEXT("法术强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetSpellPowerAttribute())));
AddAttributeRow(AttackGroup, TEXT("命中率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetAccuracyAttribute())));
AddAttributeRow(AttackGroup, TEXT("暴击率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCriticalChanceAttribute())));
AddAttributeRow(AttackGroup, TEXT("暴击伤害"), MakeMultiplierText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCriticalDamageAttribute())));
AddAttributeRow(AttackGroup, TEXT("攻击速度"), MakeMultiplierText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetAttackSpeedAttribute())));
AddAttributeRow(AttackGroup, TEXT("冷却缩减"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCooldownReductionAttribute())));
AddAttributeRow(AttackGroup, TEXT("破防强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetGuardBreakPowerAttribute())));
AddAttributeRow(AttackGroup, TEXT("韧性伤害"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPoiseDamageAttribute())));
UPHYAttributeGroupWidget* DefenseGroup = AddGroup(FText::FromString(TEXT("防御能力")));
AddAttributeRow(DefenseGroup, TEXT("护甲"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetArmorAttribute())));
AddAttributeRow(DefenseGroup, TEXT("魔法抗性"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMagicResistanceAttribute())));
AddAttributeRow(DefenseGroup, TEXT("闪避率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetEvasionAttribute())));
AddAttributeRow(DefenseGroup, TEXT("格挡强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetBlockPowerAttribute())));
AddAttributeRow(DefenseGroup, TEXT("韧性"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPoiseAttribute())));
}

View File

@@ -0,0 +1,57 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYElementDerivedAttributePageWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYElementDerivedAttributePageWidget)
#include "AbilitySystemComponent.h"
#include "AbilitySystem/Attributes/PHYElementAttributeSet.h"
#include "UI/PHYAttributeGroupWidget.h"
namespace
{
float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute)
{
return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f;
}
FText MakePercentText(const float Value)
{
return FText::FromString(FString::Printf(TEXT("%.1f%%"), Value * 100.0f));
}
void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value)
{
if (GroupWidget)
{
GroupWidget->AddRow(FText::FromString(Label), Value);
}
}
}
void UPHYElementDerivedAttributePageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent)
{
SetPageTitle(FText::FromString(TEXT("元素衍生属性")));
ClearGroups();
UPHYAttributeGroupWidget* BonusGroup = AddGroup(FText::FromString(TEXT("元素伤害加成")));
AddAttributeRow(BonusGroup, TEXT("火伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetFireDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("水伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWaterDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("冰伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetIceDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("雷伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightningDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("地伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetEarthDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("风伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWindDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("光伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("暗伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetDarkDamageBonusAttribute())));
AddAttributeRow(BonusGroup, TEXT("元素穿透"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetElementPenetrationAttribute())));
UPHYAttributeGroupWidget* ResistanceGroup = AddGroup(FText::FromString(TEXT("元素抗性")));
AddAttributeRow(ResistanceGroup, TEXT("火抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetFireResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("水抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWaterResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("冰抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetIceResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("雷抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightningResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("地抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetEarthResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("风抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWindResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("光抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightResistanceAttribute())));
AddAttributeRow(ResistanceGroup, TEXT("暗抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetDarkResistanceAttribute())));
}

View File

@@ -36,6 +36,10 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|UI")
bool IsHUDInitialized() const { return bHUDInitialized; }
/** @brief 打开角色属性菜单,默认推入 Generic UI Menu 层。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
UCommonActivatableWidget* OpenAttributeMenu(APlayerController* OwningPlayerController);
protected:
/** @brief 注册 GenericUISystem 本地玩家根布局和项目 UI 上下文。 */
virtual bool InitializeGenericUIForPlayer(APlayerController* OwningPlayerController);
@@ -43,6 +47,9 @@ protected:
/** @brief 推入默认 HUD 控件。 */
virtual UCommonActivatableWidget* PushDefaultHUDWidget(APlayerController* OwningPlayerController);
/** @brief 推入指定 CommonUI 控件到 Menu 层。 */
virtual UCommonActivatableWidget* PushMenuWidget(APlayerController* OwningPlayerController, TSubclassOf<UCommonActivatableWidget> WidgetClass);
/** @brief 当前本地玩家 UI 上下文。 */
UPROPERTY(Transient)
TObjectPtr<UPHYPlayerUIContext> PlayerUIContext;

View File

@@ -167,6 +167,10 @@ public:
UPROPERTY(Config)
TSoftClassPtr<UCommonActivatableWidget> DefaultHUDWidgetClass;
/** @brief 默认角色属性菜单控件类,用于推入 Menu 层。 */
UPROPERTY(Config)
TSoftClassPtr<UCommonActivatableWidget> AttributeMenuWidgetClass;
/** @brief 默认 gameplay 层 Tag。 */
UPROPERTY(Config)
FGameplayTag GameplayLayerTag;

View File

@@ -0,0 +1,63 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PHYAttributeGroupWidget.generated.h"
class UPHYAttributeRowWidget;
class UTextBlock;
class UVerticalBox;
/**
* @brief 属性分组子控件。
*
* 每个分组负责一个标题和若干属性行,例如攻击能力、防御能力、元素抗性。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeGroupWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** @brief 构造属性分组控件。 */
UPHYAttributeGroupWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生分组 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步分组显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置分组标题。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetGroupTitle(FText InGroupTitle);
/** @brief 清理当前分组内所有属性行。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void ClearRows();
/** @brief 添加一行属性。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
UPHYAttributeRowWidget* AddRow(FText Label, FText Value, bool bCanUpgrade = false);
protected:
/** @brief 构造原生分组树。 */
void BuildNativeWidgetTree();
/** @brief 刷新分组标题。 */
void UpdateGroupWidgets();
/** @brief 分组标题。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText GroupTitle;
/** @brief 标题文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText;
/** @brief 属性行容器。 */
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> RowsBox;
};

View File

@@ -0,0 +1,164 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "GameplayEffectTypes.h"
#include "UI/GUIS_ActivatableWidget.h"
#include "PHYAttributeMenuWidget.generated.h"
class APlayerController;
class APHYPlayerState;
class UAbilitySystemComponent;
class UButton;
class UHorizontalBox;
class UPHYAttributeOverviewPageWidget;
class UPHYCombatDerivedAttributePageWidget;
class UPHYElementDerivedAttributePageWidget;
class UPHYAttributeSummaryWidget;
class UTextBlock;
class UWidgetSwitcher;
struct FOnAttributeChangeData;
/**
* @brief PHY 角色属性主界面。
*
* 主界面只负责绑定 PlayerState/ASC、分页和数据分发具体显示拆到概览、分页、分组、属性行和资源条子控件。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeMenuWidget : public UGUIS_ActivatableWidget
{
GENERATED_BODY()
public:
/** @brief 构造角色属性主界面。 */
UPHYAttributeMenuWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生属性界面 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步属性界面显示。 */
virtual void SynchronizeProperties() override;
/** @brief 绑定本地玩家控制器,随后从 PlayerState 和 GAS 拉取属性。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void InitializeFromPlayer(APlayerController* InPlayerController);
/** @brief 重新从当前绑定来源刷新所有分页。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void RefreshFromBoundSources();
protected:
/** @brief Widget 构造后尝试自动绑定 OwningPlayer。 */
virtual void NativeConstruct() override;
/** @brief Widget 销毁前解除所有委托。 */
virtual void NativeDestruct() override;
/** @brief PlayerState 延迟到达时重试绑定。 */
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
/** @brief 构造原生属性界面树。 */
void BuildNativeWidgetTree();
/** @brief 创建一个分页按钮。 */
UButton* CreateTabButton(FName ButtonName, FText Label);
/** @brief 切换到指定分页。 */
void SetActivePage(int32 NewPageIndex);
/** @brief 刷新分页按钮颜色。 */
void UpdateTabVisuals();
/** @brief 绑定新的 PlayerState。 */
void BindToPlayerState(APHYPlayerState* NewPlayerState);
/** @brief 解除 PlayerState 委托。 */
void UnbindFromPlayerState();
/** @brief 绑定新的 AbilitySystemComponent。 */
void BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent);
/** @brief 解除 GAS 属性变化委托。 */
void UnbindFromAbilitySystem();
/** @brief 重建概览和所有属性分页内容。 */
void RefreshAttributePages();
/** @brief 属性变化后刷新分页。 */
void HandleAttributeChanged(const FOnAttributeChangeData& ChangeData);
/** @brief PlayerState 成长信息变化后刷新分页。 */
UFUNCTION()
void HandleProgressionChanged(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel);
/** @brief 点击总览分页。 */
UFUNCTION()
void HandleOverviewTabClicked();
/** @brief 点击战斗分页。 */
UFUNCTION()
void HandleCombatTabClicked();
/** @brief 点击元素分页。 */
UFUNCTION()
void HandleElementTabClicked();
/** @brief 读取一个 GAS 属性数值。 */
float GetAttributeValue(const FGameplayAttribute& Attribute) const;
/** @brief 计算仅用于 UI 概览的战力评分。 */
int32 CalculateDisplayCombatPower() const;
/** @brief 当前绑定的本地玩家控制器弱引用。 */
TWeakObjectPtr<APlayerController> BoundPlayerController;
/** @brief 当前绑定的玩家状态弱引用。 */
TWeakObjectPtr<APHYPlayerState> BoundPlayerState;
/** @brief 当前绑定的 ASC 弱引用。 */
TWeakObjectPtr<UAbilitySystemComponent> BoundAbilitySystemComponent;
/** @brief GAS 属性变化委托句柄。 */
TArray<FDelegateHandle> AttributeChangedHandles;
/** @brief GAS 属性变化委托对应属性。 */
TArray<FGameplayAttribute> BoundAttributes;
/** @brief 当前分页索引。 */
UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI|Attributes")
int32 ActivePageIndex = 0;
/** @brief 左侧概览子控件。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeSummaryWidget> SummaryWidget;
/** @brief 分页切换器。 */
UPROPERTY(Transient)
TObjectPtr<UWidgetSwitcher> PageSwitcher;
/** @brief 总览分页。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeOverviewPageWidget> OverviewPage;
/** @brief 战斗衍生属性分页。 */
UPROPERTY(Transient)
TObjectPtr<UPHYCombatDerivedAttributePageWidget> CombatPage;
/** @brief 元素衍生属性分页。 */
UPROPERTY(Transient)
TObjectPtr<UPHYElementDerivedAttributePageWidget> ElementPage;
/** @brief 总览按钮。 */
UPROPERTY(Transient)
TObjectPtr<UButton> OverviewTabButton;
/** @brief 战斗按钮。 */
UPROPERTY(Transient)
TObjectPtr<UButton> CombatTabButton;
/** @brief 元素按钮。 */
UPROPERTY(Transient)
TObjectPtr<UButton> ElementTabButton;
};

View File

@@ -0,0 +1,25 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/PHYAttributePageWidget.h"
#include "PHYAttributeOverviewPageWidget.generated.h"
class UAbilitySystemComponent;
/**
* @brief 属性总览分页子控件。
*
* 展示核心属性和三项主要战斗资源,核心属性保留加点按钮占位。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeOverviewPageWidget : public UPHYAttributePageWidget
{
GENERATED_BODY()
public:
/** @brief 从 ASC 刷新总览分页。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent);
};

View File

@@ -0,0 +1,68 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PHYAttributePageWidget.generated.h"
class UPHYAttributeGroupWidget;
class UScrollBox;
class UTextBlock;
class UVerticalBox;
/**
* @brief 属性分页子控件。
*
* 分页只负责标题和分组容器,具体属性项由分组和属性行子控件承载。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributePageWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** @brief 构造属性分页控件。 */
UPHYAttributePageWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生分页 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步分页显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置分页标题。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetPageTitle(FText InPageTitle);
/** @brief 清理当前分页内所有分组。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void ClearGroups();
/** @brief 添加一个属性分组。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
UPHYAttributeGroupWidget* AddGroup(FText GroupTitle);
protected:
/** @brief 构造原生分页树。 */
void BuildNativeWidgetTree();
/** @brief 刷新分页标题。 */
void UpdatePageWidgets();
/** @brief 分页标题。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText PageTitle;
/** @brief 标题文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText;
/** @brief 分组滚动区域。 */
UPROPERTY(Transient)
TObjectPtr<UScrollBox> GroupsScrollBox;
/** @brief 分组容器。 */
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> GroupsBox;
};

View File

@@ -0,0 +1,70 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PHYAttributeResourceBarWidget.generated.h"
class UProgressBar;
class UTextBlock;
/**
* @brief 资源条子控件。
*
* 用于生命、法力、耐力和经验等当前值/上限值显示。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeResourceBarWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** @brief 构造资源条控件。 */
UPHYAttributeResourceBarWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生资源条 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步资源条显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置资源条内容。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetResource(FText InLabel, float InCurrentValue, float InMaxValue, FLinearColor InBarTint);
protected:
/** @brief 构造原生资源条树。 */
void BuildNativeWidgetTree();
/** @brief 刷新资源条显示。 */
void UpdateResourceWidgets();
/** @brief 资源名称。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText Label;
/** @brief 当前值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
float CurrentValue = 0.0f;
/** @brief 上限值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
float MaxValue = 1.0f;
/** @brief 填充颜色。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FLinearColor BarTint = FLinearColor(0.25f, 0.68f, 0.63f, 1.0f);
/** @brief 名称文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LabelText;
/** @brief 数值文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ValueText;
/** @brief 进度条。 */
UPROPERTY(Transient)
TObjectPtr<UProgressBar> ResourceBar;
};

View File

@@ -0,0 +1,75 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PHYAttributeRowWidget.generated.h"
class UButton;
class UHorizontalBox;
class UTextBlock;
/**
* @brief 属性行子控件。
*
* 用于显示单个属性名称、当前数值和可选加点按钮,供属性页分组复用。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeRowWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** @brief 构造属性行控件。 */
UPHYAttributeRowWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生属性行 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步属性行显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置属性行内容。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetRow(FText InLabel, FText InValue, bool bInCanUpgrade = false);
protected:
/** @brief 构造原生属性行树。 */
void BuildNativeWidgetTree();
/** @brief 刷新属性行显示。 */
void UpdateRowWidgets();
/** @brief 属性名称。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText Label;
/** @brief 属性数值文本。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText Value;
/** @brief 是否显示加点按钮。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
bool bCanUpgrade = false;
/** @brief 行根容器。 */
UPROPERTY(Transient)
TObjectPtr<UHorizontalBox> RowBox;
/** @brief 属性名称文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LabelText;
/** @brief 属性数值文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ValueText;
/** @brief 加点按钮。 */
UPROPERTY(Transient)
TObjectPtr<UButton> UpgradeButton;
/** @brief 加点按钮文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UpgradeText;
};

View File

@@ -0,0 +1,106 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PHYAttributeSummaryWidget.generated.h"
class UPHYAttributeResourceBarWidget;
class UTextBlock;
class UVerticalBox;
/**
* @brief 属性界面左侧概览子控件。
*
* 展示角色名称、等级、战力、经验条和三项主要战斗资源。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYAttributeSummaryWidget : public UUserWidget
{
GENERATED_BODY()
public:
/** @brief 构造属性概览控件。 */
UPHYAttributeSummaryWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生概览 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步概览显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置角色概览信息。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetSummary(FText InPlayerName, int32 InLevel, int32 InExperience, int32 InExperienceForNextLevel, int32 InCombatPower);
/** @brief 设置三项主要资源。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void SetResources(float InHealth, float InMaxHealth, float InMana, float InMaxMana, float InStamina, float InMaxStamina);
protected:
/** @brief 构造原生概览树。 */
void BuildNativeWidgetTree();
/** @brief 刷新概览显示。 */
void UpdateSummaryWidgets();
/** @brief 角色名称。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
FText PlayerName;
/** @brief 当前等级。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
int32 Level = 1;
/** @brief 当前经验。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
int32 Experience = 0;
/** @brief 下一级所需经验。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
int32 ExperienceForNextLevel = 100;
/** @brief 概览战力评分。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes")
int32 CombatPower = 0;
float Health = 0.0f;
float MaxHealth = 1.0f;
float Mana = 0.0f;
float MaxMana = 1.0f;
float Stamina = 0.0f;
float MaxStamina = 1.0f;
/** @brief 角色名文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PlayerNameText;
/** @brief 等级文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> LevelText;
/** @brief 战力文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CombatPowerText;
/** @brief 资源条容器。 */
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> ResourceBox;
/** @brief 经验条。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeResourceBarWidget> ExperienceBar;
/** @brief 生命条。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeResourceBarWidget> HealthBar;
/** @brief 法力条。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeResourceBarWidget> ManaBar;
/** @brief 耐力条。 */
UPROPERTY(Transient)
TObjectPtr<UPHYAttributeResourceBarWidget> StaminaBar;
};

View File

@@ -0,0 +1,25 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/PHYAttributePageWidget.h"
#include "PHYCombatDerivedAttributePageWidget.generated.h"
class UAbilitySystemComponent;
/**
* @brief 战斗衍生属性分页子控件。
*
* 展示战斗资源、攻击能力和防御能力内的全部战斗衍生属性。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYCombatDerivedAttributePageWidget : public UPHYAttributePageWidget
{
GENERATED_BODY()
public:
/** @brief 从 ASC 刷新战斗衍生属性分页。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent);
};

View File

@@ -0,0 +1,25 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/PHYAttributePageWidget.h"
#include "PHYElementDerivedAttributePageWidget.generated.h"
class UAbilitySystemComponent;
/**
* @brief 元素衍生属性分页子控件。
*
* 展示全部元素伤害加成、元素抗性和通用元素穿透。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYElementDerivedAttributePageWidget : public UPHYAttributePageWidget
{
GENERATED_BODY()
public:
/** @brief 从 ASC 刷新元素衍生属性分页。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes")
void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent);
};