From 85c2062318b9b59cc0bf49c1ef38496f7982a171 Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Thu, 11 Jun 2026 11:43:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(interaction):=20=E6=96=B0=E5=A2=9EUI=20Wid?= =?UTF-8?q?get=E5=9F=BA=E7=B1=BB=E5=B9=B6=E9=9B=86=E6=88=90=E5=88=B0Intera?= =?UTF-8?q?ctionManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interaction/InteractionManager.cpp | 102 ++++++++++++++++-- .../Interaction/InteractionManager.h | 35 ++++-- .../Interaction/InteractionPromptWidget.cpp | 67 ++++++++++++ .../Interaction/InteractionPromptWidget.h | 65 +++++++++++ .../Interaction/NarrativeTextWidget.cpp | 57 ++++++++++ .../Interaction/NarrativeTextWidget.h | 66 ++++++++++++ 6 files changed, 374 insertions(+), 18 deletions(-) create mode 100644 Source/ChildHood/Interaction/InteractionPromptWidget.cpp create mode 100644 Source/ChildHood/Interaction/InteractionPromptWidget.h create mode 100644 Source/ChildHood/Interaction/NarrativeTextWidget.cpp create mode 100644 Source/ChildHood/Interaction/NarrativeTextWidget.h diff --git a/Source/ChildHood/Interaction/InteractionManager.cpp b/Source/ChildHood/Interaction/InteractionManager.cpp index 5401a32..324919a 100644 --- a/Source/ChildHood/Interaction/InteractionManager.cpp +++ b/Source/ChildHood/Interaction/InteractionManager.cpp @@ -5,6 +5,8 @@ #include "Interaction/InteractableComponent.h" #include "Interaction/InteractableActor.h" #include "Interaction/PushCameraActor.h" +#include "Interaction/InteractionPromptWidget.h" +#include "Interaction/NarrativeTextWidget.h" #include "ChildHoodCharacter.h" #include "ChildHoodGameInstance.h" #include "ChildHood.h" @@ -17,6 +19,7 @@ #include "Engine/World.h" #include "TimerManager.h" #include "Kismet/GameplayStatics.h" +#include "Blueprint/WidgetBlueprintLibrary.h" // 定义交互系统日志类别 DECLARE_LOG_CATEGORY_EXTERN(LogInteraction, Log, All); @@ -56,6 +59,39 @@ void UInteractionManager::BeginPlay() UE_LOG(LogInteraction, Log, TEXT("InteractionManager: BeginPlay 完成, PushCameraActor=%s"), PushCameraActor ? *PushCameraActor->GetName() : TEXT("nullptr")); + + // --- 创建 UI Widget --- + APlayerController* PC = GetWorld()->GetFirstPlayerController(); + if (PC) + { + // 创建提示 UI + if (PromptWidgetClass) + { + PromptWidget = CreateWidget(PC, PromptWidgetClass); + if (PromptWidget) + { + PromptWidget->AddToViewport(10); + } + } + else + { + UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: PromptWidgetClass 未设置,提示 UI 不可用")); + } + + // 创建文本底框 UI + if (NarrativeWidgetClass) + { + NarrativeWidget = CreateWidget(PC, NarrativeWidgetClass); + if (NarrativeWidget) + { + NarrativeWidget->AddToViewport(20); + } + } + else + { + UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: NarrativeWidgetClass 未设置,文本 UI 不可用")); + } + } } // ============================================================================ @@ -66,9 +102,18 @@ void UInteractionManager::TickComponent(float DeltaTime, ELevelTick TickType, FA { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); - // Free 状态不需要 Tick if (State == EInteractionState::Free) { + // Free 状态下更新提示 UI 的屏幕位置 + if (PromptWidget && PromptWidget->IsVisible() && + NearbyInteractables.Num() > 0 && NearbyInteractables.IsValidIndex(SelectedIndex)) + { + UInteractableComponent* Selected = NearbyInteractables[SelectedIndex]; + if (Selected) + { + UpdatePromptScreenPosition(Selected); + } + } return; } @@ -651,32 +696,49 @@ void UInteractionManager::UpdatePromptUI() return; } - // v0.1: 用日志输出替代 UI FString PromptText = Selected->GetPromptText(); - UE_LOG(LogInteraction, Log, TEXT("UpdatePromptUI: [提示] %s"), *PromptText); + + if (PromptWidget) + { + PromptWidget->SetPromptText(PromptText); + PromptWidget->Show(); + UpdatePromptScreenPosition(Selected); + } + + UE_LOG(LogInteraction, Verbose, TEXT("UpdatePromptUI: %s"), *PromptText); } void UInteractionManager::HidePrompt() { - // v0.1: 用日志输出替代 UI - UE_LOG(LogInteraction, Verbose, TEXT("HidePrompt: 隐藏交互提示")); + if (PromptWidget) + { + PromptWidget->Hide(); + } } void UInteractionManager::ShowNarrativeText(const FString& Text) { - // v0.1: 用日志输出替代 UI if (Text.IsEmpty()) { - UE_LOG(LogInteraction, Warning, TEXT("ShowNarrativeText: 文本为空")); + UE_LOG(LogInteraction, Warning, TEXT("ShowNarrativeText: text is empty")); return; } - UE_LOG(LogInteraction, Log, TEXT("ShowNarrativeText: [文本] %s"), *Text); + + if (NarrativeWidget) + { + NarrativeWidget->SetNarrativeText(Text); + NarrativeWidget->Show(); + } + + UE_LOG(LogInteraction, Verbose, TEXT("ShowNarrativeText: %s"), *Text); } void UInteractionManager::HideNarrativeText() { - // v0.1: 用日志输出替代 UI - UE_LOG(LogInteraction, Verbose, TEXT("HideNarrativeText: 隐藏叙事文本")); + if (NarrativeWidget) + { + NarrativeWidget->Hide(); + } } // ============================================================================ @@ -813,3 +875,23 @@ void UInteractionManager::OnExitTimerComplete() UpdatePromptUI(); } } + +// ============================================================================ +// 提示 UI 屏幕位置更新 +// ============================================================================ + +void UInteractionManager::UpdatePromptScreenPosition(UInteractableComponent* Interactable) +{ + if (!Interactable || !PromptWidget) return; + + APlayerController* PC = GetWorld()->GetFirstPlayerController(); + if (!PC) return; + + // 交互物世界位置 + 高度偏移 → 屏幕坐标 + FVector WorldLoc = Interactable->GetWorldLocation() + FVector(0.f, 0.f, PromptHeightOffset); + FVector2D ScreenPos; + if (UGameplayStatics::ProjectWorldToScreen(PC, WorldLoc, ScreenPos, true)) + { + PromptWidget->SetScreenPosition(ScreenPos); + } +} diff --git a/Source/ChildHood/Interaction/InteractionManager.h b/Source/ChildHood/Interaction/InteractionManager.h index a4fb918..433d21c 100644 --- a/Source/ChildHood/Interaction/InteractionManager.h +++ b/Source/ChildHood/Interaction/InteractionManager.h @@ -12,6 +12,8 @@ class UInteractableComponent; class APushCameraActor; class AChildHoodCharacter; +class UInteractionPromptWidget; +class UNarrativeTextWidget; /** * UInteractionManager @@ -136,15 +138,29 @@ private: /** 动画事件是否已触发 */ bool bAnimEventFired = false; - // === UI 引用(v0.1 后续任务创建) === + // === UI 配置 === - /** 交互提示 Widget(v0.1 用日志替代) */ - UPROPERTY() - TObjectPtr PromptWidget; + /** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */ + UPROPERTY(EditAnywhere, Category = "Interaction|UI") + TSubclassOf PromptWidgetClass; - /** 文本底框 Widget(v0.1 用日志替代) */ + /** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */ + UPROPERTY(EditAnywhere, Category = "Interaction|UI") + TSubclassOf NarrativeWidgetClass; + + /** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */ + UPROPERTY(EditAnywhere, Category = "Interaction|UI") + float PromptHeightOffset = 80.0f; + + // === UI 实例 === + + /** 交互提示 Widget 实例 */ UPROPERTY() - TObjectPtr NarrativeWidget; + TObjectPtr PromptWidget; + + /** 文本底框 Widget 实例 */ + UPROPERTY() + TObjectPtr NarrativeWidget; // === 文本选择 === @@ -156,12 +172,15 @@ private: /** 更新选中交互物(排序后 SelectedIndex = 0) */ void UpdateSelection(); - /** 更新提示 UI(v0.1 用日志输出) */ + /** 更新提示 UI 内容和位置 */ void UpdatePromptUI(); - /** 隐藏提示 UI(v0.1 用日志输出) */ + /** 隐藏提示 UI */ void HidePrompt(); + /** 更新提示 UI 的屏幕空间位置(跟随交互物世界位置投影) */ + void UpdatePromptScreenPosition(UInteractableComponent* Interactable); + /** Enter 阶段 Tick:转向插值 + 推镜 + 等待 */ void TickEnterPhase(float DeltaTime); diff --git a/Source/ChildHood/Interaction/InteractionPromptWidget.cpp b/Source/ChildHood/Interaction/InteractionPromptWidget.cpp new file mode 100644 index 0000000..f7cb7af --- /dev/null +++ b/Source/ChildHood/Interaction/InteractionPromptWidget.cpp @@ -0,0 +1,67 @@ +// ChildHood Project - 交互提示 UI 基类 实现 + +#include "Interaction/InteractionPromptWidget.h" +#include "Components/TextBlock.h" + +void UInteractionPromptWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + // 初始隐藏 + SetVisibility(ESlateVisibility::Hidden); +} + +void UInteractionPromptWidget::SetPromptText(const FString& Text) +{ + if (PromptTextBlock) + { + PromptTextBlock->SetText(FText::FromString(Text)); + } +} + +void UInteractionPromptWidget::SetScreenPosition(const FVector2D& Position) +{ + // 使用 RenderTranslation 偏移 Widget 位置 + // 蓝图中 CanvasPanel 的 Anchors 应设置为屏幕左上角 + // 此处直接设置 RenderTranslation 来移动位置 + SetRenderTranslation(Position); +} + +void UInteractionPromptWidget::Show() +{ + if (bIsVisible) return; + bIsVisible = true; + + SetVisibility(ESlateVisibility::HitTestInvisible); + + if (FadeInAnim) + { + PlayAnimation(FadeInAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f); + } +} + +void UInteractionPromptWidget::Hide() +{ + if (!bIsVisible) return; + + if (FadeOutAnim) + { + PlayAnimation(FadeOutAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f); + // 动画播完后隐藏(简单方案:直接设置定时器隐藏) + // 动画时长由蓝图中的 FadeOutAnim 决定,这里用固定 0.2s + FTimerHandle TempHandle; + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(TempHandle, [this]() + { + bIsVisible = false; + SetVisibility(ESlateVisibility::Hidden); + }, 0.2f, false); + } + } + else + { + bIsVisible = false; + SetVisibility(ESlateVisibility::Hidden); + } +} diff --git a/Source/ChildHood/Interaction/InteractionPromptWidget.h b/Source/ChildHood/Interaction/InteractionPromptWidget.h new file mode 100644 index 0000000..2b88ea1 --- /dev/null +++ b/Source/ChildHood/Interaction/InteractionPromptWidget.h @@ -0,0 +1,65 @@ +// ChildHood Project - 交互提示 UI 基类 +// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏) +// 职责:屏幕空间悬浮提示 "[E] 动作词 名称",跟随交互物位置 + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "InteractionPromptWidget.generated.h" + +class UTextBlock; + +/** + * UInteractionPromptWidget + * + * 交互提示 UI 的 C++ 基类。蓝图子类(WBP_InteractionPrompt)继承此类, + * 在 UMG 编辑器中布局可视组件,并确保组件名与 BindWidget 匹配。 + * + * 绑定要求: + * - TextBlock 名为 "PromptTextBlock" — 显示提示文字 + * - WidgetAnimation 名为 "FadeInAnim" — 淡入动画(0.2s) + * - WidgetAnimation 名为 "FadeOutAnim" — 淡出动画(0.2s) + */ +UCLASS(Abstract, Blueprintable) +class UInteractionPromptWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** 设置提示文字内容 */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt") + void SetPromptText(const FString& Text); + + /** 设置屏幕空间位置(由 InteractionManager 每帧调用) */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt") + void SetScreenPosition(const FVector2D& Position); + + /** 显示提示(播放淡入动画) */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt") + void Show(); + + /** 隐藏提示(播放淡出动画) */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt") + void Hide(); + +protected: + + /** 提示文字 TextBlock,蓝图子类必须有一个同名 TextBlock */ + UPROPERTY(meta = (BindWidget)) + UTextBlock* PromptTextBlock; + + /** 淡入动画(0.2s Opacity 0→1),蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeInAnim; + + /** 淡出动画(0.2s Opacity 1→0),蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeOutAnim; + + /** 是否正在显示中 */ + bool bIsVisible = false; + + virtual void NativeOnInitialized() override; +}; diff --git a/Source/ChildHood/Interaction/NarrativeTextWidget.cpp b/Source/ChildHood/Interaction/NarrativeTextWidget.cpp new file mode 100644 index 0000000..14cb308 --- /dev/null +++ b/Source/ChildHood/Interaction/NarrativeTextWidget.cpp @@ -0,0 +1,57 @@ +// ChildHood Project - 文本底框 UI 基类 实现 + +#include "Interaction/NarrativeTextWidget.h" +#include "Components/TextBlock.h" + +void UNarrativeTextWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + // 初始隐藏 + SetVisibility(ESlateVisibility::Hidden); +} + +void UNarrativeTextWidget::SetNarrativeText(const FString& Text) +{ + if (NarrativeTextBlock) + { + NarrativeTextBlock->SetText(FText::FromString(Text)); + } +} + +void UNarrativeTextWidget::Show() +{ + if (bIsVisible) return; + bIsVisible = true; + + SetVisibility(ESlateVisibility::HitTestInvisible); + + if (FadeInAnim) + { + PlayAnimation(FadeInAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f); + } +} + +void UNarrativeTextWidget::Hide() +{ + if (!bIsVisible) return; + + if (FadeOutAnim) + { + PlayAnimation(FadeOutAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f); + FTimerHandle TempHandle; + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(TempHandle, [this]() + { + bIsVisible = false; + SetVisibility(ESlateVisibility::Hidden); + }, 0.2f, false); + } + } + else + { + bIsVisible = false; + SetVisibility(ESlateVisibility::Hidden); + } +} diff --git a/Source/ChildHood/Interaction/NarrativeTextWidget.h b/Source/ChildHood/Interaction/NarrativeTextWidget.h new file mode 100644 index 0000000..1cbead9 --- /dev/null +++ b/Source/ChildHood/Interaction/NarrativeTextWidget.h @@ -0,0 +1,66 @@ +// ChildHood Project - 文本底框 UI 基类 +// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(通用文本显示UI) +// 职责:半透明底框 + 一句话叙事/对话文本,带淡入淡出 + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "NarrativeTextWidget.generated.h" + +class UTextBlock; + +/** + * UNarrativeTextWidget + * + * 文本底框 UI 的 C++ 基类。蓝图子类(WBP_NarrativeText)继承此类, + * 在 UMG 编辑器中布局可视组件,并确保组件名与 BindWidget 匹配。 + * + * 绑定要求: + * - TextBlock 名为 "NarrativeTextBlock" — 显示叙事/对话文字 + * - WidgetAnimation 名为 "FadeInAnim" — 淡入动画(0.2s) + * - WidgetAnimation 名为 "FadeOutAnim" — 淡出动画(0.2s) + * + * 布局要求: + * - Anchors: Horizontal Center, Vertical ~0.6(屏幕中心偏下) + * - Border: 半透明黑色背景(~60%),圆角,SizeToContent + * - TextBlock: 白色文字,居中,自动换行 + */ +UCLASS(Abstract, Blueprintable) +class UNarrativeTextWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** 设置叙事文本内容 */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative") + void SetNarrativeText(const FString& Text); + + /** 显示文本(播放淡入动画) */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative") + void Show(); + + /** 隐藏文本(播放淡出动画) */ + UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative") + void Hide(); + +protected: + + /** 叙事文字 TextBlock,蓝图子类必须有一个同名 TextBlock */ + UPROPERTY(meta = (BindWidget)) + UTextBlock* NarrativeTextBlock; + + /** 淡入动画(0.2s),蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeInAnim; + + /** 淡出动画(0.2s),蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeOutAnim; + + /** 是否正在显示中 */ + bool bIsVisible = false; + + virtual void NativeOnInitialized() override; +};