feat(interaction): 接通叙事/拾取通知Widget表现层
P-016: 将EffectManager的ShowNarrative/HideNarrative/ShowPickupNotification 从日志占位改为真实CreateWidget+AddToViewport+淡入淡出生命周期。 - 新增UPickupNotificationWidget C++基类(对称NarrativeTextWidget) - EffectManager新增SetWidgetClasses注入接口与Widget弱引用缓存 - PerformCleanup补齐Narrative/Pickup Widget兜底淡出移除 - ASystemDriver新增两个TSubclassOf配置点,BeginPlay注入给EffectManager - Widget Class未注入时回退日志占位,保证Effect流程不卡 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "Interaction/PushCameraActor.h"
|
||||
#include "Interaction/NarrativeTextWidget.h"
|
||||
#include "Interaction/PickupNotificationWidget.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/DoorActor.h"
|
||||
@@ -28,6 +31,17 @@ void FEffectManager::SetOnItemAddedCallback(FOnItemAdded&& Callback)
|
||||
OnItemAdded = MoveTemp(Callback);
|
||||
}
|
||||
|
||||
void FEffectManager::SetWidgetClasses(TSubclassOf<UNarrativeTextWidget> NarrativeClass,
|
||||
TSubclassOf<UPickupNotificationWidget> PickupClass)
|
||||
{
|
||||
NarrativeWidgetClass = NarrativeClass;
|
||||
PickupWidgetClass = PickupClass;
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: WidgetClasses 注入 — Narrative=%s, Pickup=%s"),
|
||||
NarrativeClass ? *NarrativeClass->GetName() : TEXT("(null,日志占位)"),
|
||||
PickupClass ? *PickupClass->GetName() : TEXT("(null,日志占位)"));
|
||||
}
|
||||
|
||||
void FEffectManager::PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context)
|
||||
{
|
||||
// 如果有正在播放的 Effect,先强制清理
|
||||
@@ -202,8 +216,9 @@ void FEffectManager::ResumeFromWait()
|
||||
|
||||
void FEffectManager::PerformCleanup()
|
||||
{
|
||||
// 1. 文本底框淡出移除(v0.1 日志占位,后续替换为 Widget 操作)
|
||||
// TODO: 如果 NarrativeWidget 显示中,触发淡出动画
|
||||
// 1. 文本底框强制淡出移除(ShowNarrative 曾显示但未 HideNarrative 时兜底)
|
||||
RemoveNarrativeWidget();
|
||||
RemovePickupWidget();
|
||||
|
||||
// 2. 相机回位(如果推镜过)
|
||||
if (CachedPushCamera.IsValid())
|
||||
@@ -391,8 +406,33 @@ void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action)
|
||||
SelectedText = Action.NarrativeTexts[0];
|
||||
}
|
||||
|
||||
// v0.1: 日志输出(后续替换为 Widget 显示)
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative - %s"), *SelectedText);
|
||||
// P-016:接通真实 Widget(半透明底框 + 整句一次显示 + 淡入)
|
||||
if (NarrativeWidgetClass && CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
if (APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController()))
|
||||
{
|
||||
// 若上一次的 Widget 仍在,先移除(避免叠加)
|
||||
RemoveNarrativeWidget();
|
||||
|
||||
UNarrativeTextWidget* Widget = CreateWidget<UNarrativeTextWidget>(PC, NarrativeWidgetClass);
|
||||
if (Widget)
|
||||
{
|
||||
Widget->AddToViewport();
|
||||
Widget->SetNarrativeText(SelectedText);
|
||||
Widget->Show();
|
||||
CachedNarrativeWidget = Widget;
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative (Widget) - %s"), *SelectedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Widget Class 未注入(蓝图未创建/未配置)→ 回退日志占位,保证 Effect 流程不卡
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative (日志占位,WBP_NarrativeText 未配置) - %s"),
|
||||
*SelectedText);
|
||||
}
|
||||
|
||||
if (Action.bWaitForInput)
|
||||
{
|
||||
@@ -403,17 +443,74 @@ void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action)
|
||||
|
||||
void FEffectManager::ExecuteHideNarrative()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 操作)
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative"));
|
||||
// P-016:淡出叙事文本 Widget(执行案要求 0.2s 淡出)
|
||||
// Hide() 播放 FadeOutAnim,0.2s 后 SetVisibility(Hidden)。Widget 实例保留在视口中,
|
||||
// 下次 ShowNarrative 时由 RemoveNarrativeWidget 先行清理避免叠加。
|
||||
if (CachedNarrativeWidget.IsValid())
|
||||
{
|
||||
CachedNarrativeWidget->Hide();
|
||||
CachedNarrativeWidget = nullptr;
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative (Widget 淡出)"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative (无活动 Widget)"));
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowPickupNotification()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 显示)
|
||||
FString DisplayName;
|
||||
if (CurrentContext.TargetComp.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification - %s"),
|
||||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||||
DisplayName = CurrentContext.TargetComp->GetInteractableData().DisplayName;
|
||||
}
|
||||
|
||||
// 执行案 §跨系统交互:拾取通知("找到了 XXX")归物品系统控制,不在交互系统执行案范围。
|
||||
// P-016 范围:仅提供表现能力,v0.1 由交互系统 EffectManager 先行接通(物品系统 AddToBackpack
|
||||
// 接通属于 P-013)。若 Widget Class 已注入则显示,否则日志占位。
|
||||
const FString NotificationText = DisplayName.IsEmpty()
|
||||
? TEXT("找到了东西!")
|
||||
: FString::Printf(TEXT("找到了 %s!"), *DisplayName);
|
||||
|
||||
if (PickupWidgetClass && CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
if (APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController()))
|
||||
{
|
||||
RemovePickupWidget();
|
||||
|
||||
UPickupNotificationWidget* Widget = CreateWidget<UPickupNotificationWidget>(PC, PickupWidgetClass);
|
||||
if (Widget)
|
||||
{
|
||||
Widget->AddToViewport();
|
||||
Widget->SetPickupText(NotificationText);
|
||||
Widget->Show();
|
||||
CachedPickupWidget = Widget;
|
||||
|
||||
// 自动淡出(执行案要求通知短暂停留后消失),2.0s 后 Hide
|
||||
if (UWorld* World = Widget->GetWorld())
|
||||
{
|
||||
TWeakObjectPtr<UPickupNotificationWidget> WeakWidget = Widget;
|
||||
FTimerHandle AutoHideHandle;
|
||||
World->GetTimerManager().SetTimer(AutoHideHandle, [WeakWidget]()
|
||||
{
|
||||
if (WeakWidget.IsValid())
|
||||
{
|
||||
WeakWidget->Hide();
|
||||
}
|
||||
}, 2.0f, false);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification (Widget) - %s"),
|
||||
*NotificationText);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification (日志占位,WBP_PickupNotification 未配置) - %s"),
|
||||
*NotificationText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +525,40 @@ void FEffectManager::ExecuteToggleDoor()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 表现层 Widget 辅助(P-016)
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::RemoveNarrativeWidget()
|
||||
{
|
||||
if (CachedNarrativeWidget.IsValid())
|
||||
{
|
||||
UNarrativeTextWidget* Widget = CachedNarrativeWidget.Get();
|
||||
CachedNarrativeWidget = nullptr;
|
||||
|
||||
// 强制移除并清理(PerformCleanup / 下次 ShowNarrative 调用)
|
||||
// 若 Widget 正在播放淡出动画,RemoveFromParent 会中断它,这是兜底清理的可接受行为
|
||||
if (Widget && Widget->IsInViewport())
|
||||
{
|
||||
Widget->RemoveFromParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::RemovePickupWidget()
|
||||
{
|
||||
if (CachedPickupWidget.IsValid())
|
||||
{
|
||||
UPickupNotificationWidget* Widget = CachedPickupWidget.Get();
|
||||
CachedPickupWidget = nullptr;
|
||||
|
||||
if (Widget && Widget->IsInViewport())
|
||||
{
|
||||
Widget->RemoveFromParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TurnToTarget 插值
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InteractionTypes.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
|
||||
class APushCameraActor;
|
||||
class UNarrativeTextWidget;
|
||||
class UPickupNotificationWidget;
|
||||
|
||||
/**
|
||||
* FEffectManager
|
||||
@@ -35,6 +38,20 @@ public:
|
||||
* 由 ASystemDriver 绑定到 FCollectionManager::OnItemObtained(收集系统技术方案第13节)。 */
|
||||
void SetOnItemAddedCallback(FOnItemAdded&& Callback);
|
||||
|
||||
/**
|
||||
* 设置表现层 Widget 蓝图类(由 ASystemDriver 在 BeginPlay 中注入)。
|
||||
* FEffectManager 是普通 C++ 类,不能持有 UPROPERTY,故 Widget Class 的编辑器配置点
|
||||
* 放在 ASystemDriver 上(TSubclassOf UPROPERTY),运行时通过本接口注入。
|
||||
* UClass 为全局单例永久存活,普通 TSubclassOf 引用安全。
|
||||
*
|
||||
* P-016:接通 ShowNarrative / HideNarrative / ShowPickupNotification 的真实 Widget 显示。
|
||||
*
|
||||
* @param NarrativeClass WBP_NarrativeText 蓝图类(nullptr 时回退日志占位)
|
||||
* @param PickupClass WBP_PickupNotification 蓝图类(nullptr 时回退日志占位)
|
||||
*/
|
||||
void SetWidgetClasses(TSubclassOf<UNarrativeTextWidget> NarrativeClass,
|
||||
TSubclassOf<UPickupNotificationWidget> PickupClass);
|
||||
|
||||
/** 播放 Effect — 初始化状态,开始 Timeline 推进 */
|
||||
void PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context);
|
||||
|
||||
@@ -85,6 +102,20 @@ private:
|
||||
/** 推镜期间 Spawn 的临时摄像机(弱引用,Effect 结束时销毁) */
|
||||
TWeakObjectPtr<APushCameraActor> CachedPushCamera;
|
||||
|
||||
// === 表现层 Widget 缓存(P-016 接通真实 UI)===
|
||||
|
||||
/** NarrativeText Widget 蓝图类(由 ASystemDriver 注入,nullptr 时回退日志占位) */
|
||||
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
|
||||
|
||||
/** PickupNotification Widget 蓝图类(由 ASystemDriver 注入,nullptr 时回退日志占位) */
|
||||
TSubclassOf<UPickupNotificationWidget> PickupWidgetClass;
|
||||
|
||||
/** 当前显示中的叙事文本 Widget(弱引用,HideNarrative / PerformCleanup 时淡出移除) */
|
||||
TWeakObjectPtr<UNarrativeTextWidget> CachedNarrativeWidget;
|
||||
|
||||
/** 当前显示中的拾取通知 Widget(弱引用) */
|
||||
TWeakObjectPtr<UPickupNotificationWidget> CachedPickupWidget;
|
||||
|
||||
// === 回调 ===
|
||||
|
||||
/** Effect 完成回调(正常结束或取消后清理完毕时触发) */
|
||||
@@ -125,6 +156,14 @@ private:
|
||||
void ExecuteShowPickupNotification();
|
||||
void ExecuteToggleDoor();
|
||||
|
||||
// === 表现层 Widget 辅助(P-016)===
|
||||
|
||||
/** 移除并清理缓存的 Narrative Widget(若存在则触发淡出,超时强制移除) */
|
||||
void RemoveNarrativeWidget();
|
||||
|
||||
/** 移除并清理缓存的 Pickup Widget(若存在) */
|
||||
void RemovePickupWidget();
|
||||
|
||||
/** 更新 TurnToTarget 跨帧插值(在 Update 末尾调用) */
|
||||
void UpdateTurn(float DeltaTime);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// ChildHood Project - 拾取通知 UI 基类 实现
|
||||
|
||||
#include "Interaction/PickupNotificationWidget.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
void UPickupNotificationWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
|
||||
// 初始隐藏
|
||||
SetVisibility(ESlateVisibility::Hidden);
|
||||
}
|
||||
|
||||
void UPickupNotificationWidget::SetPickupText(const FString& Text)
|
||||
{
|
||||
if (PickupTextBlock)
|
||||
{
|
||||
PickupTextBlock->SetText(FText::FromString(Text));
|
||||
}
|
||||
}
|
||||
|
||||
void UPickupNotificationWidget::Show()
|
||||
{
|
||||
if (bIsVisible) return;
|
||||
bIsVisible = true;
|
||||
|
||||
SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
|
||||
if (FadeInAnim)
|
||||
{
|
||||
PlayAnimation(FadeInAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void UPickupNotificationWidget::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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// ChildHood Project - 拾取通知 UI 基类
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第7节(WBP_PickupNotification)
|
||||
// 职责:拾取/获得物品时的通知弹窗("找到了 XXX"),带淡入淡出
|
||||
// 注意:执行案 §跨系统交互规定拾取通知归物品系统控制,本基类为交互系统 ShowPickupNotification
|
||||
// Action 提供表现能力,v0.1 暂由 EffectManager 日志占位或本 Widget 简化显示,物品系统
|
||||
// 接通后(P-013 后续)再正式启用。
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "PickupNotificationWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* UPickupNotificationWidget
|
||||
*
|
||||
* 拾取通知 UI 的 C++ 基类。蓝图子类(WBP_PickupNotification)继承此类,
|
||||
* 在 UMG 编辑器中布局可视组件,并确保组件名与 BindWidget 匹配。
|
||||
*
|
||||
* 绑定要求:
|
||||
* - TextBlock 名为 "PickupTextBlock" — 显示拾取通知文字(如 "找到了 旧橡皮!")
|
||||
* - WidgetAnimation 名为 "FadeInAnim" — 淡入动画(0.2s)
|
||||
* - WidgetAnimation 名为 "FadeOutAnim" — 淡出动画(0.2s)
|
||||
*
|
||||
* 布局要求:
|
||||
* - Anchors: 右下角或屏幕中下,与 NarrativeText 错位
|
||||
* - Border: 半透明背景,圆角
|
||||
* - TextBlock: 白色文字,居中
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class UPickupNotificationWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** 设置通知文本内容 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction|Pickup")
|
||||
void SetPickupText(const FString& Text);
|
||||
|
||||
/** 显示通知(播放淡入动画) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction|Pickup")
|
||||
void Show();
|
||||
|
||||
/** 隐藏通知(播放淡出动画) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction|Pickup")
|
||||
void Hide();
|
||||
|
||||
protected:
|
||||
|
||||
/** 通知文字 TextBlock,蓝图子类必须有一个同名 TextBlock */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
UTextBlock* PickupTextBlock;
|
||||
|
||||
/** 淡入动画(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;
|
||||
};
|
||||
@@ -59,6 +59,9 @@ void ASystemDriver::BeginPlay()
|
||||
Callback.BindLambda([this]() { this->OnEffectComplete(); });
|
||||
EffectManager.SetOnEffectComplete(MoveTemp(Callback));
|
||||
|
||||
// P-016:注入表现层 Widget Class(将编辑器配置的 WBP 蓝图类传给 EffectManager)
|
||||
EffectManager.SetWidgetClasses(NarrativeWidgetClass, PickupWidgetClass);
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 初始化完成"));
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#include "Collection/CollectionManager.h"
|
||||
#include "SystemDriver.generated.h"
|
||||
|
||||
class UNarrativeTextWidget;
|
||||
class UPickupNotificationWidget;
|
||||
|
||||
/**
|
||||
* ASystemDriver
|
||||
*
|
||||
@@ -74,6 +77,18 @@ private:
|
||||
/** 收集管理器(收集系统逻辑核心) */
|
||||
FCollectionManager CollectionManager;
|
||||
|
||||
// === 表现层 Widget 配置点(P-016)===
|
||||
// FEffectManager 是普通 C++ 类不能持有 UPROPERTY,编辑器配置点放此处,
|
||||
// BeginPlay 时通过 EffectManager.SetWidgetClasses() 注入。
|
||||
|
||||
/** 叙事文本 Widget 蓝图类(编辑器指定 WBP_NarrativeText)。留空则回退日志占位 */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
|
||||
|
||||
/** 拾取通知 Widget 蓝图类(编辑器指定 WBP_PickupNotification)。留空则回退日志占位 */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UPickupNotificationWidget> PickupWidgetClass;
|
||||
|
||||
// === Effect 完成回调连线 ===
|
||||
|
||||
/** EffectManager 的 OnEffectComplete 回调目标 */
|
||||
|
||||
Reference in New Issue
Block a user