diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index d102914..c35fcd3 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -2,6 +2,9 @@ [/Script/CommonUI.CommonUISettings] CommonButtonAcceptKeyHandling=TriggerClick +[/Script/GenericUISystem.GUIS_GenericUISystemSettings] +GameUIPolicyClass="/Script/PHY.PHYGameUIPolicy" + [/Script/EngineSettings.GeneralProjectSettings] ProjectID=E7C26E1F4D195F0DBE49C2A3E5017988 CopyrightNotice= diff --git a/Config/DefaultPHYUI.ini b/Config/DefaultPHYUI.ini index d1280c2..b9d2d28 100644 --- a/Config/DefaultPHYUI.ini +++ b/Config/DefaultPHYUI.ini @@ -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 diff --git a/PHY.uproject b/PHY.uproject index 2865156..f55bf6b 100644 --- a/PHY.uproject +++ b/PHY.uproject @@ -77,6 +77,10 @@ { "Name": "EnhancedInput", "Enabled": true + }, + { + "Name": "CommonUI", + "Enabled": true } ] -} \ No newline at end of file +} diff --git a/Source/PHY/PHY.Build.cs b/Source/PHY/PHY.Build.cs index c5c7de0..71f14bd 100644 --- a/Source/PHY/PHY.Build.cs +++ b/Source/PHY/PHY.Build.cs @@ -19,10 +19,16 @@ public class PHY : ModuleRules "GameplayAbilities", "GameplayTasks", "ModularGameplay", + "CommonUI", + "CommonInput", + "UMG", + "Slate", + "SlateCore", "GenericGameplayAbilities", "GenericCombatSystem", "GenericInputSystem", "GenericGameSystem", + "GenericUISystem", "GenericEffectsSystem", "SLSCore", "SmoothLocomotionSystem", diff --git a/Source/PHY/Private/Game/PHYHUD.cpp b/Source/PHY/Private/Game/PHYHUD.cpp index 07f26a7..d2baacf 100644 --- a/Source/PHY/Private/Game/PHYHUD.cpp +++ b/Source/PHY/Private/Game/PHYHUD.cpp @@ -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(); + 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() : nullptr; + if (!GameUISubsystem) + { + return false; + } + + GameUISubsystem->AddPlayer(LocalPlayer); + + if (!PlayerUIContext) + { + PlayerUIContext = NewObject(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(); + TSubclassOf 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(HUDLayerTag, HUDWidgetClass); } diff --git a/Source/PHY/Private/GameplayTags/PHYGameplayTags_UI.cpp b/Source/PHY/Private/GameplayTags/PHYGameplayTags_UI.cpp new file mode 100644 index 0000000..bc50ba2 --- /dev/null +++ b/Source/PHY/Private/GameplayTags/PHYGameplayTags_UI.cpp @@ -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"); +} diff --git a/Source/PHY/Private/UI/PHYGameUILayout.cpp b/Source/PHY/Private/UI/PHYGameUILayout.cpp new file mode 100644 index 0000000..6596a89 --- /dev/null +++ b/Source/PHY/Private/UI/PHYGameUILayout.cpp @@ -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 UPHYGameUILayout::RebuildWidget() +{ + BuildNativeLayoutTree(); + RegisterNativeLayers(); + + return Super::RebuildWidget(); +} + +void UPHYGameUILayout::BuildNativeLayoutTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && GameLayerStack && HUDLayerStack && MenuLayerStack && ModalLayerStack) + { + return; + } + + UOverlay* RootOverlay = WidgetTree->ConstructWidget(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::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; +} diff --git a/Source/PHY/Private/UI/PHYGameUIPolicy.cpp b/Source/PHY/Private/UI/PHYGameUIPolicy.cpp new file mode 100644 index 0000000..28da6c0 --- /dev/null +++ b/Source/PHY/Private/UI/PHYGameUIPolicy.cpp @@ -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 SelectedLayoutClass = UPHYGameUILayout::StaticClass(); + + if (const UPHYUISettings* UISettings = GetDefault()) + { + if (!UISettings->RootLayoutClass.IsNull()) + { + if (UClass* LoadedLayoutClass = UISettings->RootLayoutClass.LoadSynchronous()) + { + SelectedLayoutClass = LoadedLayoutClass; + } + } + } + + // GenericUISystem 将 LayoutClass 设为私有 EditAnywhere 属性;这里通过反射写入 C++ 默认布局,避免创建仅用于赋默认值的蓝图资产。 + if (const FSoftClassProperty* LayoutClassProperty = FindFProperty(UGUIS_GameUIPolicy::StaticClass(), TEXT("LayoutClass"))) + { + LayoutClassProperty->SetPropertyValue_InContainer(this, FSoftObjectPtr(SelectedLayoutClass.Get())); + } +} diff --git a/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp b/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp new file mode 100644 index 0000000..346e829 --- /dev/null +++ b/Source/PHY/Private/UI/PHYHUDStatusWidget.cpp @@ -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 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(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && TitleText && HealthText && HealthBar && StaminaText && StaminaBar) + { + return; + } + + USafeZone* RootSafeZone = WidgetTree->ConstructWidget(USafeZone::StaticClass(), TEXT("RootSafeZone")); + UOverlay* RootOverlay = WidgetTree->ConstructWidget(UOverlay::StaticClass(), TEXT("RootOverlay")); + UVerticalBox* StatusBox = WidgetTree->ConstructWidget(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::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::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::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::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::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)); + } +} diff --git a/Source/PHY/Private/UI/PHYPlayerUIContext.cpp b/Source/PHY/Private/UI/PHYPlayerUIContext.cpp new file mode 100644 index 0000000..8a8f629 --- /dev/null +++ b/Source/PHY/Private/UI/PHYPlayerUIContext.cpp @@ -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; +} diff --git a/Source/PHY/Public/Game/PHYHUD.h b/Source/PHY/Public/Game/PHYHUD.h index 797390c..ddd2053 100644 --- a/Source/PHY/Public/Game/PHYHUD.h +++ b/Source/PHY/Public/Game/PHYHUD.h @@ -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 PlayerUIContext; + + /** @brief 当前本地玩家 UI 上下文注册句柄。 */ + UPROPERTY(Transient) + FGUIS_UIContextBindingHandle PlayerUIContextBindingHandle; + + /** @brief 默认 HUD 控件实例。 */ + UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI") + TObjectPtr DefaultHUDWidget; /** @brief HUD 是否已经完成基础初始化。 */ UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI") bool bHUDInitialized = false; diff --git a/Source/PHY/Public/GameplayTags/PHYGameplayTags_UI.h b/Source/PHY/Public/GameplayTags/PHYGameplayTags_UI.h new file mode 100644 index 0000000..6d064ac --- /dev/null +++ b/Source/PHY/Public/GameplayTags/PHYGameplayTags_UI.h @@ -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); +} diff --git a/Source/PHY/Public/PHYConfigSettings.h b/Source/PHY/Public/PHYConfigSettings.h index 69fd32f..55276a4 100644 --- a/Source/PHY/Public/PHYConfigSettings.h +++ b/Source/PHY/Public/PHYConfigSettings.h @@ -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 RootLayoutClass; + + /** @brief 默认 HUD 控件类,本地玩家 HUD 初始化时推入 HUD 层。 */ + UPROPERTY(Config) + TSoftClassPtr 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"); diff --git a/Source/PHY/Public/PHYGameplayTags.h b/Source/PHY/Public/PHYGameplayTags.h index 55244f7..b172bb8 100644 --- a/Source/PHY/Public/PHYGameplayTags.h +++ b/Source/PHY/Public/PHYGameplayTags.h @@ -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 聚合入口。 diff --git a/Source/PHY/Public/UI/PHYGameUILayout.h b/Source/PHY/Public/UI/PHYGameUILayout.h new file mode 100644 index 0000000..ab0ea81 --- /dev/null +++ b/Source/PHY/Public/UI/PHYGameUILayout.h @@ -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 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 GameLayerStack; + + /** @brief HUD 常驻层。 */ + UPROPERTY(Transient) + TObjectPtr HUDLayerStack; + + /** @brief 菜单交互层。 */ + UPROPERTY(Transient) + TObjectPtr MenuLayerStack; + + /** @brief 模态最高优先级层。 */ + UPROPERTY(Transient) + TObjectPtr ModalLayerStack; + + /** @brief 是否已将当前层 Stack 注册到 GenericUISystem。 */ + UPROPERTY(Transient) + bool bNativeLayersRegistered = false; +}; diff --git a/Source/PHY/Public/UI/PHYGameUIPolicy.h b/Source/PHY/Public/UI/PHYGameUIPolicy.h new file mode 100644 index 0000000..ead6f6d --- /dev/null +++ b/Source/PHY/Public/UI/PHYGameUIPolicy.h @@ -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(); +}; diff --git a/Source/PHY/Public/UI/PHYHUDStatusWidget.h b/Source/PHY/Public/UI/PHYHUDStatusWidget.h new file mode 100644 index 0000000..f1ca125 --- /dev/null +++ b/Source/PHY/Public/UI/PHYHUDStatusWidget.h @@ -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 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 TitleText; + + /** @brief 生命文本。 */ + UPROPERTY(Transient) + TObjectPtr HealthText; + + /** @brief 生命进度条。 */ + UPROPERTY(Transient) + TObjectPtr HealthBar; + + /** @brief 耐力文本。 */ + UPROPERTY(Transient) + TObjectPtr StaminaText; + + /** @brief 耐力进度条。 */ + UPROPERTY(Transient) + TObjectPtr StaminaBar; +}; diff --git a/Source/PHY/Public/UI/PHYPlayerUIContext.h b/Source/PHY/Public/UI/PHYPlayerUIContext.h new file mode 100644 index 0000000..b94ce97 --- /dev/null +++ b/Source/PHY/Public/UI/PHYPlayerUIContext.h @@ -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 PlayerController; + + /** @brief 当前 Pawn 弱引用。 */ + UPROPERTY(Transient) + TWeakObjectPtr Pawn; +};