Add Generic UI foundation

This commit is contained in:
2026-04-26 20:05:25 +08:00
parent 2ae207533c
commit ee12f1ad32
18 changed files with 701 additions and 3 deletions

View File

@@ -2,6 +2,9 @@
[/Script/CommonUI.CommonUISettings]
CommonButtonAcceptKeyHandling=TriggerClick
[/Script/GenericUISystem.GUIS_GenericUISystemSettings]
GameUIPolicyClass="/Script/PHY.PHYGameUIPolicy"
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=E7C26E1F4D195F0DBE49C2A3E5017988
CopyrightNotice=

View File

@@ -1,4 +1,11 @@
[/Script/PHY.PHYUISettings]
bUseCommonUI=True
bUseGenericUISystem=True
bOpenMenusWithGameplayPause=False
RootLayoutClass="/Script/PHY.PHYGameUILayout"
DefaultHUDWidgetClass="/Script/PHY.PHYHUDStatusWidget"
GameplayLayerTag=(TagName="UI.Layer.Game")
HUDLayerTag=(TagName="UI.Layer.HUD")
MenuLayerTag=(TagName="UI.Layer.Menu")
ModalLayerTag=(TagName="UI.Layer.Modal")
DefaultHUDLayerName=HUD

View File

@@ -77,6 +77,10 @@
{
"Name": "EnhancedInput",
"Enabled": true
},
{
"Name": "CommonUI",
"Enabled": true
}
]
}

View File

@@ -19,10 +19,16 @@ public class PHY : ModuleRules
"GameplayAbilities",
"GameplayTasks",
"ModularGameplay",
"CommonUI",
"CommonInput",
"UMG",
"Slate",
"SlateCore",
"GenericGameplayAbilities",
"GenericCombatSystem",
"GenericInputSystem",
"GenericGameSystem",
"GenericUISystem",
"GenericEffectsSystem",
"SLSCore",
"SmoothLocomotionSystem",

View File

@@ -4,6 +4,16 @@
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYHUD)
#include "CommonActivatableWidget.h"
#include "Engine/GameInstance.h"
#include "GameplayTags/PHYGameplayTags_UI.h"
#include "PHYConfigSettings.h"
#include "UI/GUIS_GameUILayout.h"
#include "UI/GUIS_GameUIPolicy.h"
#include "UI/GUIS_GameUISubsystem.h"
#include "UI/PHYHUDStatusWidget.h"
#include "UI/PHYPlayerUIContext.h"
APHYHUD::APHYHUD(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
@@ -23,6 +33,81 @@ void APHYHUD::InitializeHUDForPlayer(APlayerController* OwningPlayerController)
return;
}
// UI 专家后续在这里接入 GenericUISystem/CommonUI 的根层级。
bHUDInitialized = true;
if (InitializeGenericUIForPlayer(OwningPlayerController))
{
DefaultHUDWidget = PushDefaultHUDWidget(OwningPlayerController);
bHUDInitialized = true;
}
}
bool APHYHUD::InitializeGenericUIForPlayer(APlayerController* OwningPlayerController)
{
if (!OwningPlayerController || !OwningPlayerController->IsLocalController())
{
return false;
}
const UPHYUISettings* UISettings = GetDefault<UPHYUISettings>();
if (!UISettings || !UISettings->bUseCommonUI || !UISettings->bUseGenericUISystem)
{
return false;
}
ULocalPlayer* LocalPlayer = OwningPlayerController->GetLocalPlayer();
if (!LocalPlayer)
{
return false;
}
UGameInstance* GameInstance = OwningPlayerController->GetGameInstance();
UGUIS_GameUISubsystem* GameUISubsystem = GameInstance ? GameInstance->GetSubsystem<UGUIS_GameUISubsystem>() : nullptr;
if (!GameUISubsystem)
{
return false;
}
GameUISubsystem->AddPlayer(LocalPlayer);
if (!PlayerUIContext)
{
PlayerUIContext = NewObject<UPHYPlayerUIContext>(this);
}
PlayerUIContext->InitializeForPlayer(OwningPlayerController);
GameUISubsystem->RegisterUIContextForPlayer(LocalPlayer, PlayerUIContext, PlayerUIContextBindingHandle);
return true;
}
UCommonActivatableWidget* APHYHUD::PushDefaultHUDWidget(APlayerController* OwningPlayerController)
{
if (!OwningPlayerController || DefaultHUDWidget)
{
return DefaultHUDWidget;
}
const UPHYUISettings* UISettings = GetDefault<UPHYUISettings>();
TSubclassOf<UCommonActivatableWidget> HUDWidgetClass = UPHYHUDStatusWidget::StaticClass();
if (UISettings && !UISettings->DefaultHUDWidgetClass.IsNull())
{
if (UClass* LoadedHUDWidgetClass = UISettings->DefaultHUDWidgetClass.LoadSynchronous())
{
HUDWidgetClass = LoadedHUDWidgetClass;
}
}
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 HUDLayerTag = (UISettings && UISettings->HUDLayerTag.IsValid()) ? UISettings->HUDLayerTag : PHYGameplayTags::UI_Layer_HUD;
return RootLayout->PushWidgetToLayerStack<UCommonActivatableWidget>(HUDLayerTag, HUDWidgetClass);
}

View File

@@ -0,0 +1,12 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "GameplayTags/PHYGameplayTags_UI.h"
// UI 层级 Tag 是 GenericUISystem 注册和推送界面的稳定路由,不使用裸字符串。
namespace PHYGameplayTags
{
UE_DEFINE_GAMEPLAY_TAG(UI_Layer_Game, "UI.Layer.Game");
UE_DEFINE_GAMEPLAY_TAG(UI_Layer_HUD, "UI.Layer.HUD");
UE_DEFINE_GAMEPLAY_TAG(UI_Layer_Menu, "UI.Layer.Menu");
UE_DEFINE_GAMEPLAY_TAG(UI_Layer_Modal, "UI.Layer.Modal");
}

View File

@@ -0,0 +1,93 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYGameUILayout.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYGameUILayout)
#include "Blueprint/WidgetTree.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "GameplayTags/PHYGameplayTags_UI.h"
#include "Widgets/CommonActivatableWidgetContainer.h"
#include "Widgets/SWidget.h"
UPHYGameUILayout::UPHYGameUILayout(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedRef<SWidget> UPHYGameUILayout::RebuildWidget()
{
BuildNativeLayoutTree();
RegisterNativeLayers();
return Super::RebuildWidget();
}
void UPHYGameUILayout::BuildNativeLayoutTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && GameLayerStack && HUDLayerStack && MenuLayerStack && ModalLayerStack)
{
return;
}
UOverlay* RootOverlay = WidgetTree->ConstructWidget<UOverlay>(UOverlay::StaticClass(), TEXT("RootOverlay"));
WidgetTree->RootWidget = RootOverlay;
GameLayerStack = AddLayerStack(RootOverlay, TEXT("GameLayerStack"));
HUDLayerStack = AddLayerStack(RootOverlay, TEXT("HUDLayerStack"));
MenuLayerStack = AddLayerStack(RootOverlay, TEXT("MenuLayerStack"));
ModalLayerStack = AddLayerStack(RootOverlay, TEXT("ModalLayerStack"));
bNativeLayersRegistered = false;
}
UCommonActivatableWidgetStack* UPHYGameUILayout::AddLayerStack(UOverlay* RootOverlay, const FName StackName) const
{
if (!RootOverlay || !WidgetTree)
{
return nullptr;
}
UCommonActivatableWidgetStack* LayerStack = WidgetTree->ConstructWidget<UCommonActivatableWidgetStack>(UCommonActivatableWidgetStack::StaticClass(), StackName);
if (UOverlaySlot* LayerSlot = RootOverlay->AddChildToOverlay(LayerStack))
{
LayerSlot->SetHorizontalAlignment(HAlign_Fill);
LayerSlot->SetVerticalAlignment(VAlign_Fill);
LayerSlot->SetPadding(FMargin(0.0f));
}
return LayerStack;
}
void UPHYGameUILayout::RegisterNativeLayers()
{
if (bNativeLayersRegistered)
{
return;
}
if (GameLayerStack)
{
RegisterLayer(PHYGameplayTags::UI_Layer_Game, GameLayerStack);
}
if (HUDLayerStack)
{
RegisterLayer(PHYGameplayTags::UI_Layer_HUD, HUDLayerStack);
}
if (MenuLayerStack)
{
RegisterLayer(PHYGameplayTags::UI_Layer_Menu, MenuLayerStack);
}
if (ModalLayerStack)
{
RegisterLayer(PHYGameplayTags::UI_Layer_Modal, ModalLayerStack);
}
bNativeLayersRegistered = true;
}

View File

@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYGameUIPolicy.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYGameUIPolicy)
#include "PHYConfigSettings.h"
#include "UI/PHYGameUILayout.h"
#include "UObject/UnrealType.h"
UPHYGameUIPolicy::UPHYGameUIPolicy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
ApplyConfiguredLayoutClass();
}
void UPHYGameUIPolicy::ApplyConfiguredLayoutClass()
{
TSubclassOf<UGUIS_GameUILayout> SelectedLayoutClass = UPHYGameUILayout::StaticClass();
if (const UPHYUISettings* UISettings = GetDefault<UPHYUISettings>())
{
if (!UISettings->RootLayoutClass.IsNull())
{
if (UClass* LoadedLayoutClass = UISettings->RootLayoutClass.LoadSynchronous())
{
SelectedLayoutClass = LoadedLayoutClass;
}
}
}
// GenericUISystem 将 LayoutClass 设为私有 EditAnywhere 属性;这里通过反射写入 C++ 默认布局,避免创建仅用于赋默认值的蓝图资产。
if (const FSoftClassProperty* LayoutClassProperty = FindFProperty<FSoftClassProperty>(UGUIS_GameUIPolicy::StaticClass(), TEXT("LayoutClass")))
{
LayoutClassProperty->SetPropertyValue_InContainer(this, FSoftObjectPtr(SelectedLayoutClass.Get()));
}
}

View File

@@ -0,0 +1,144 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYHUDStatusWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYHUDStatusWidget)
#include "Blueprint/WidgetTree.h"
#include "Components/Overlay.h"
#include "Components/OverlaySlot.h"
#include "Components/ProgressBar.h"
#include "Components/SafeZone.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
{
FText MakeResourceText(const TCHAR* Label, const float Value, const float MaxValue)
{
return FText::FromString(FString::Printf(TEXT("%s %.0f / %.0f"), Label, FMath::Max(0.0f, Value), FMath::Max(0.0f, MaxValue)));
}
float MakeResourcePercent(const float Value, const float MaxValue)
{
return MaxValue > UE_SMALL_NUMBER ? FMath::Clamp(Value / MaxValue, 0.0f, 1.0f) : 0.0f;
}
}
UPHYHUDStatusWidget::UPHYHUDStatusWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
InputConfig = EGUIS_ActivatableWidgetInputMode::Default;
}
TSharedRef<SWidget> UPHYHUDStatusWidget::RebuildWidget()
{
BuildNativeWidgetTree();
return Super::RebuildWidget();
}
void UPHYHUDStatusWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::SetHealth(const float NewHealth, const float NewMaxHealth)
{
Health = NewHealth;
MaxHealth = NewMaxHealth;
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::SetStamina(const float NewStamina, const float NewMaxStamina)
{
Stamina = NewStamina;
MaxStamina = NewMaxStamina;
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::BuildNativeWidgetTree()
{
if (!WidgetTree)
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
if (WidgetTree->RootWidget && TitleText && HealthText && HealthBar && StaminaText && StaminaBar)
{
return;
}
USafeZone* RootSafeZone = WidgetTree->ConstructWidget<USafeZone>(USafeZone::StaticClass(), TEXT("RootSafeZone"));
UOverlay* RootOverlay = WidgetTree->ConstructWidget<UOverlay>(UOverlay::StaticClass(), TEXT("RootOverlay"));
UVerticalBox* StatusBox = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("StatusBox"));
WidgetTree->RootWidget = RootSafeZone;
RootSafeZone->SetContent(RootOverlay);
if (UOverlaySlot* StatusSlot = RootOverlay->AddChildToOverlay(StatusBox))
{
StatusSlot->SetHorizontalAlignment(HAlign_Left);
StatusSlot->SetVerticalAlignment(VAlign_Top);
StatusSlot->SetPadding(FMargin(32.0f, 24.0f, 0.0f, 0.0f));
}
TitleText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("TitleText"));
TitleText->SetText(FText::FromString(TEXT("PHY")));
TitleText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 22));
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));
}
HealthText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("HealthText"));
HealthText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
HealthText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
StatusBox->AddChildToVerticalBox(HealthText);
HealthBar = WidgetTree->ConstructWidget<UProgressBar>(UProgressBar::StaticClass(), TEXT("HealthBar"));
HealthBar->SetFillColorAndOpacity(FLinearColor(0.76f, 0.12f, 0.12f, 1.0f));
if (UVerticalBoxSlot* HealthBarSlot = StatusBox->AddChildToVerticalBox(HealthBar))
{
HealthBarSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 8.0f));
}
StaminaText = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), TEXT("StaminaText"));
StaminaText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16));
StaminaText->SetColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.95f, 0.95f, 1.0f)));
StatusBox->AddChildToVerticalBox(StaminaText);
StaminaBar = WidgetTree->ConstructWidget<UProgressBar>(UProgressBar::StaticClass(), TEXT("StaminaBar"));
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));
}
UpdateResourceWidgets();
}
void UPHYHUDStatusWidget::UpdateResourceWidgets()
{
if (HealthText)
{
HealthText->SetText(MakeResourceText(TEXT("HP"), Health, MaxHealth));
}
if (HealthBar)
{
HealthBar->SetPercent(MakeResourcePercent(Health, MaxHealth));
}
if (StaminaText)
{
StaminaText->SetText(MakeResourceText(TEXT("ST"), Stamina, MaxStamina));
}
if (StaminaBar)
{
StaminaBar->SetPercent(MakeResourcePercent(Stamina, MaxStamina));
}
}

View File

@@ -0,0 +1,18 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/PHYPlayerUIContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYPlayerUIContext)
#include "GameFramework/PlayerController.h"
void UPHYPlayerUIContext::InitializeForPlayer(APlayerController* InPlayerController)
{
PlayerController = InPlayerController;
RefreshPawn();
}
void UPHYPlayerUIContext::RefreshPawn()
{
Pawn = PlayerController.IsValid() ? PlayerController->GetPawn() : nullptr;
}

View File

@@ -4,8 +4,12 @@
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "UI/GUIS_GameUIStructLibrary.h"
#include "PHYHUD.generated.h"
class UCommonActivatableWidget;
class UPHYPlayerUIContext;
/**
* @brief PHY HUD 根入口。
*
@@ -33,6 +37,23 @@ public:
bool IsHUDInitialized() const { return bHUDInitialized; }
protected:
/** @brief 注册 GenericUISystem 本地玩家根布局和项目 UI 上下文。 */
virtual bool InitializeGenericUIForPlayer(APlayerController* OwningPlayerController);
/** @brief 推入默认 HUD 控件。 */
virtual UCommonActivatableWidget* PushDefaultHUDWidget(APlayerController* OwningPlayerController);
/** @brief 当前本地玩家 UI 上下文。 */
UPROPERTY(Transient)
TObjectPtr<UPHYPlayerUIContext> PlayerUIContext;
/** @brief 当前本地玩家 UI 上下文注册句柄。 */
UPROPERTY(Transient)
FGUIS_UIContextBindingHandle PlayerUIContextBindingHandle;
/** @brief 默认 HUD 控件实例。 */
UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI")
TObjectPtr<UCommonActivatableWidget> DefaultHUDWidget;
/** @brief HUD 是否已经完成基础初始化。 */
UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI")
bool bHUDInitialized = false;

View File

@@ -0,0 +1,23 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "NativeGameplayTags.h"
/**
* @brief PHY UI 层级 Gameplay Tag 定义。
*/
namespace PHYGameplayTags
{
/** @brief 非阻塞 gameplay 承载层,用于后续准星、交互提示等常驻玩法 UI。 */
UE_DECLARE_GAMEPLAY_TAG_EXTERN(UI_Layer_Game);
/** @brief HUD 承载层,用于血条、资源、快捷栏等常驻玩家界面。 */
UE_DECLARE_GAMEPLAY_TAG_EXTERN(UI_Layer_HUD);
/** @brief 菜单承载层,用于背包、角色、设置等可交互界面。 */
UE_DECLARE_GAMEPLAY_TAG_EXTERN(UI_Layer_Menu);
/** @brief 模态承载层,用于确认框、错误提示等最高优先级界面。 */
UE_DECLARE_GAMEPLAY_TAG_EXTERN(UI_Layer_Modal);
}

View File

@@ -3,11 +3,14 @@
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "UObject/Object.h"
#include "UObject/SoftObjectPtr.h"
#include "PHYConfigSettings.generated.h"
class UCommonActivatableWidget;
class UGIPS_InputProcessor;
class UGUIS_GameUILayout;
/**
* @brief PHY 项目核心配置。
@@ -148,10 +151,38 @@ public:
UPROPERTY(Config)
bool bUseCommonUI = true;
/** @brief 是否启用 GenericUISystem 作为项目 UI 根层级和推送系统。 */
UPROPERTY(Config)
bool bUseGenericUISystem = true;
/** @brief 打开菜单时是否暂停 Gameplay。 */
UPROPERTY(Config)
bool bOpenMenusWithGameplayPause = false;
/** @brief 原生根布局类,默认使用项目 C++ 布局并在其中注册 Generic UI 层。 */
UPROPERTY(Config)
TSoftClassPtr<UGUIS_GameUILayout> RootLayoutClass;
/** @brief 默认 HUD 控件类,本地玩家 HUD 初始化时推入 HUD 层。 */
UPROPERTY(Config)
TSoftClassPtr<UCommonActivatableWidget> DefaultHUDWidgetClass;
/** @brief 默认 gameplay 层 Tag。 */
UPROPERTY(Config)
FGameplayTag GameplayLayerTag;
/** @brief 默认 HUD 层 Tag。 */
UPROPERTY(Config)
FGameplayTag HUDLayerTag;
/** @brief 默认菜单层 Tag。 */
UPROPERTY(Config)
FGameplayTag MenuLayerTag;
/** @brief 默认模态层 Tag。 */
UPROPERTY(Config)
FGameplayTag ModalLayerTag;
/** @brief 默认 HUD 层名称。 */
UPROPERTY(Config)
FName DefaultHUDLayerName = TEXT("HUD");

View File

@@ -9,6 +9,7 @@
#include "GameplayTags/PHYGameplayTags_Event.h"
#include "GameplayTags/PHYGameplayTags_Input.h"
#include "GameplayTags/PHYGameplayTags_State.h"
#include "GameplayTags/PHYGameplayTags_UI.h"
/**
* @brief PHY 项目的原生 Gameplay Tag 聚合入口。

View File

@@ -0,0 +1,58 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/GUIS_GameUILayout.h"
#include "PHYGameUILayout.generated.h"
class UCommonActivatableWidgetStack;
class UOverlay;
/**
* @brief PHY 原生游戏 UI 根布局。
*
* 以 GenericUISystem 的 UGUIS_GameUILayout 为基类,在 C++ 中创建并注册 Game、HUD、Menu、Modal 四个层级。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYGameUILayout : public UGUIS_GameUILayout
{
GENERATED_BODY()
public:
/** @brief 构造根布局。 */
UPHYGameUILayout(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生 WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
protected:
/** @brief 构造根 Overlay 和各层 Stack。 */
void BuildNativeLayoutTree();
/** @brief 向根 Overlay 添加一个全屏层 Stack。 */
UCommonActivatableWidgetStack* AddLayerStack(UOverlay* RootOverlay, FName StackName) const;
/** @brief 注册 Native GameplayTag 到 GenericUISystem 层级表。 */
void RegisterNativeLayers();
/** @brief Gameplay 常驻层。 */
UPROPERTY(Transient)
TObjectPtr<UCommonActivatableWidgetStack> GameLayerStack;
/** @brief HUD 常驻层。 */
UPROPERTY(Transient)
TObjectPtr<UCommonActivatableWidgetStack> HUDLayerStack;
/** @brief 菜单交互层。 */
UPROPERTY(Transient)
TObjectPtr<UCommonActivatableWidgetStack> MenuLayerStack;
/** @brief 模态最高优先级层。 */
UPROPERTY(Transient)
TObjectPtr<UCommonActivatableWidgetStack> ModalLayerStack;
/** @brief 是否已将当前层 Stack 注册到 GenericUISystem。 */
UPROPERTY(Transient)
bool bNativeLayersRegistered = false;
};

View File

@@ -0,0 +1,26 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/GUIS_GameUIPolicy.h"
#include "PHYGameUIPolicy.generated.h"
/**
* @brief PHY GenericUISystem 策略。
*
* 负责把 GenericUISystem 的根布局指向项目 C++ 布局类,避免在 GameMode 或 HUD 中直接装配 UMG 层级。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYGameUIPolicy : public UGUIS_GameUIPolicy
{
GENERATED_BODY()
public:
/** @brief 构造策略并写入项目根布局类。 */
UPHYGameUIPolicy(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
protected:
/** @brief 将项目配置中的 RootLayoutClass 写入 GenericUISystem 策略基类。 */
void ApplyConfiguredLayoutClass();
};

View File

@@ -0,0 +1,82 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/GUIS_ActivatableWidget.h"
#include "PHYHUDStatusWidget.generated.h"
class UProgressBar;
class UTextBlock;
/**
* @brief PHY 默认 HUD 状态控件。
*
* 作为基础 UI 的 C++ 可见占位,后续战斗、背包、队伍等系统可以替换或扩展为项目资产。
*/
UCLASS(BlueprintType, Blueprintable)
class PHY_API UPHYHUDStatusWidget : public UGUIS_ActivatableWidget
{
GENERATED_BODY()
public:
/** @brief 构造 HUD 状态控件。 */
UPHYHUDStatusWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/** @brief 构建原生 HUD WidgetTree。 */
virtual TSharedRef<SWidget> RebuildWidget() override;
/** @brief 同步文本和进度条显示。 */
virtual void SynchronizeProperties() override;
/** @brief 设置生命值显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetHealth(float NewHealth, float NewMaxHealth);
/** @brief 设置耐力值显示。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void SetStamina(float NewStamina, float NewMaxStamina);
protected:
/** @brief 构造原生 HUD 树。 */
void BuildNativeWidgetTree();
/** @brief 刷新单条资源文本和进度条。 */
void UpdateResourceWidgets();
/** @brief 当前生命值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float Health = 100.0f;
/** @brief 当前最大生命值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float MaxHealth = 100.0f;
/** @brief 当前耐力值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float Stamina = 100.0f;
/** @brief 当前最大耐力值。 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI")
float MaxStamina = 100.0f;
/** @brief 标题文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TitleText;
/** @brief 生命文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> HealthText;
/** @brief 生命进度条。 */
UPROPERTY(Transient)
TObjectPtr<UProgressBar> HealthBar;
/** @brief 耐力文本。 */
UPROPERTY(Transient)
TObjectPtr<UTextBlock> StaminaText;
/** @brief 耐力进度条。 */
UPROPERTY(Transient)
TObjectPtr<UProgressBar> StaminaBar;
};

View File

@@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UI/GUIS_GameUIContext.h"
#include "PHYPlayerUIContext.generated.h"
class APlayerController;
class APawn;
/**
* @brief 本地玩家 UI 上下文。
*
* 用于把 PlayerController、Pawn 等本地只读引用提供给 HUD、菜单和后续扩展界面。
*/
UCLASS(BlueprintType)
class PHY_API UPHYPlayerUIContext : public UGUIS_GameUIContext
{
GENERATED_BODY()
public:
/** @brief 设置本地玩家控制器并刷新 Pawn 引用。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void InitializeForPlayer(APlayerController* InPlayerController);
/** @brief 刷新当前 Pawn 引用。 */
UFUNCTION(BlueprintCallable, Category="PHY|UI")
void RefreshPawn();
/** @brief 获取本地玩家控制器。 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|UI")
APlayerController* GetPlayerController() const { return PlayerController.Get(); }
/** @brief 获取当前 Pawn。 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|UI")
APawn* GetPawn() const { return Pawn.Get(); }
private:
/** @brief 本地玩家控制器弱引用。 */
UPROPERTY(Transient)
TWeakObjectPtr<APlayerController> PlayerController;
/** @brief 当前 Pawn 弱引用。 */
UPROPERTY(Transient)
TWeakObjectPtr<APawn> Pawn;
};