From 4fc4a39aa667407de1406e80be67ab3a11f86d07 Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Mon, 15 Jun 2026 05:13:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(collection):=20P-013=E6=94=B6=E9=9B=86?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E4=B8=89=E5=B1=82=E6=9E=B6=E6=9E=84=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- Source/ChildHood/ChildHood.Build.cs | 1 + Source/ChildHood/ChildHoodCharacter.cpp | 18 ++ Source/ChildHood/ChildHoodCharacter.h | 9 + .../Collection/CollectionCompleteWidget.cpp | 58 ++++ .../Collection/CollectionCompleteWidget.h | 69 +++++ .../Collection/CollectionHUDComponent.cpp | 265 ++++++++++++++++++ .../Collection/CollectionHUDComponent.h | 102 +++++++ .../Collection/CollectionManager.cpp | 140 +++++++++ .../ChildHood/Collection/CollectionManager.h | 84 ++++++ .../Collection/CollectionNotifWidget.cpp | 67 +++++ .../Collection/CollectionNotifWidget.h | 75 +++++ .../Collection/CollectionProgressWidget.cpp | 44 +++ .../Collection/CollectionProgressWidget.h | 72 +++++ Source/ChildHood/Collection/CollectionTypes.h | 162 +++++++++++ .../ChildHood/Interaction/EffectManager.cpp | 24 +- Source/ChildHood/Interaction/EffectManager.h | 7 + .../ChildHood/Interaction/InteractionTypes.h | 12 + .../Interaction/PersistenceSubsystem.cpp | 23 ++ .../Interaction/PersistenceSubsystem.h | 26 ++ Source/ChildHood/SystemDriver.cpp | 27 ++ Source/ChildHood/SystemDriver.h | 8 + 21 files changed, 1286 insertions(+), 7 deletions(-) create mode 100644 Source/ChildHood/Collection/CollectionCompleteWidget.cpp create mode 100644 Source/ChildHood/Collection/CollectionCompleteWidget.h create mode 100644 Source/ChildHood/Collection/CollectionHUDComponent.cpp create mode 100644 Source/ChildHood/Collection/CollectionHUDComponent.h create mode 100644 Source/ChildHood/Collection/CollectionManager.cpp create mode 100644 Source/ChildHood/Collection/CollectionManager.h create mode 100644 Source/ChildHood/Collection/CollectionNotifWidget.cpp create mode 100644 Source/ChildHood/Collection/CollectionNotifWidget.h create mode 100644 Source/ChildHood/Collection/CollectionProgressWidget.cpp create mode 100644 Source/ChildHood/Collection/CollectionProgressWidget.h create mode 100644 Source/ChildHood/Collection/CollectionTypes.h diff --git a/Source/ChildHood/ChildHood.Build.cs b/Source/ChildHood/ChildHood.Build.cs index 82fbe55..66e4fb2 100644 --- a/Source/ChildHood/ChildHood.Build.cs +++ b/Source/ChildHood/ChildHood.Build.cs @@ -26,6 +26,7 @@ public class ChildHood : ModuleRules PublicIncludePaths.AddRange(new string[] { "ChildHood", "ChildHood/Interaction", + "ChildHood/Collection", "ChildHood/Variant_Platforming", "ChildHood/Variant_Platforming/Animation", "ChildHood/Variant_Combat", diff --git a/Source/ChildHood/ChildHoodCharacter.cpp b/Source/ChildHood/ChildHoodCharacter.cpp index 2a044d6..6ae4a7b 100644 --- a/Source/ChildHood/ChildHoodCharacter.cpp +++ b/Source/ChildHood/ChildHoodCharacter.cpp @@ -17,6 +17,7 @@ #include "InputActionValue.h" #include "ChildHood.h" #include "Interaction/InteractionPromptComponent.h" +#include "Collection/CollectionHUDComponent.h" #include "SystemDriver.h" #include "Interaction/InteractionManager.h" #include "Interaction/EffectManager.h" @@ -67,6 +68,9 @@ AChildHoodCharacter::AChildHoodCharacter() // 创建交互提示组件(替代旧 InteractionManager) PromptComponent = CreateDefaultSubobject(TEXT("PromptComponent")); + + // 创建收集系统 HUD 组件(表现层,挂在角色上;Tick 兜底查找 Driver) + CollectionHUDComponent = CreateDefaultSubobject(TEXT("CollectionHUDComponent")); } void AChildHoodCharacter::BeginPlay() @@ -81,6 +85,15 @@ void AChildHoodCharacter::BeginPlay() PromptComponent->Initialize(Driver); } } + + // 初始化收集系统 HUD 组件(Driver 弱引用;Tick 会兜底查找,规避 BeginPlay 时序竞争) + if (CollectionHUDComponent) + { + if (ASystemDriver* Driver = ASystemDriver::Get(this)) + { + CollectionHUDComponent->Initialize(Driver); + } + } } void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) @@ -235,6 +248,11 @@ UInteractionPromptComponent* AChildHoodCharacter::GetPromptComponent() const return PromptComponent; } +UCollectionHUDComponent* AChildHoodCharacter::GetCollectionHUDComponent() const +{ + return CollectionHUDComponent; +} + void AChildHoodCharacter::SetInteracting(bool bValue) { bIsInteracting = bValue; diff --git a/Source/ChildHood/ChildHoodCharacter.h b/Source/ChildHood/ChildHoodCharacter.h index 27e0e24..7827665 100644 --- a/Source/ChildHood/ChildHoodCharacter.h +++ b/Source/ChildHood/ChildHoodCharacter.h @@ -15,6 +15,7 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; class UInteractionPromptComponent; +class UCollectionHUDComponent; class ASystemDriver; struct FInputActionValue; @@ -71,6 +72,10 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true")) UInteractionPromptComponent* PromptComponent; + /** 收集系统 HUD 组件 — 表现层,只读 CollectionManager 缓存驱动收集 UI */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collection", meta = (AllowPrivateAccess = "true")) + UCollectionHUDComponent* CollectionHUDComponent; + /** Whether the character is currently in an interaction */ bool bIsInteracting = false; @@ -85,6 +90,10 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction") UInteractionPromptComponent* GetPromptComponent() const; + /** 获取收集系统 HUD 组件 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Collection") + UCollectionHUDComponent* GetCollectionHUDComponent() const; + /** Set interaction state — called by interaction system */ UFUNCTION(BlueprintCallable, Category = "Interaction") void SetInteracting(bool bValue); diff --git a/Source/ChildHood/Collection/CollectionCompleteWidget.cpp b/Source/ChildHood/Collection/CollectionCompleteWidget.cpp new file mode 100644 index 0000000..fbc2d9a --- /dev/null +++ b/Source/ChildHood/Collection/CollectionCompleteWidget.cpp @@ -0,0 +1,58 @@ +// ChildHood Project - 收集完成提示 UI 实现 +// 三段时序:淡入 → 停留 → 淡出 → 移除(自管理,HUDComponent 按需创建后无需追踪) + +#include "Collection/CollectionCompleteWidget.h" +#include "Components/TextBlock.h" +#include "Engine/World.h" +#include "TimerManager.h" + +void UCollectionCompleteWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); +} + +void UCollectionCompleteWidget::ShowComplete(const FString& Text, float DisplayDuration, float InFadeOutDuration) +{ + CachedFadeOutDuration = InFadeOutDuration; + + if (CompleteTextBlock) + { + CompleteTextBlock->SetText(FText::FromString(Text)); + } + + // 淡入 + if (FadeInAnim) + { + PlayAnimation(FadeInAnim); + } + + // 停留时长后触发淡出 + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(StayTimerHandle, [this]() + { + StartFadeOut(); + }, DisplayDuration, false); + } +} + +void UCollectionCompleteWidget::StartFadeOut() +{ + if (FadeOutAnim) + { + PlayAnimation(FadeOutAnim); + } + + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(FadeOutTimerHandle, [this]() + { + OnFadeOutComplete(); + }, CachedFadeOutDuration, false); + } +} + +void UCollectionCompleteWidget::OnFadeOutComplete() +{ + RemoveFromParent(); +} diff --git a/Source/ChildHood/Collection/CollectionCompleteWidget.h b/Source/ChildHood/Collection/CollectionCompleteWidget.h new file mode 100644 index 0000000..03da29a --- /dev/null +++ b/Source/ChildHood/Collection/CollectionCompleteWidget.h @@ -0,0 +1,69 @@ +// ChildHood Project - 收集完成提示 UI 基类 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionCompleteWidget)+ 第7节 +// 职责:全部收集完成时的屏幕中央提示(淡入→停留→淡出→移除) + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CollectionCompleteWidget.generated.h" + +class UTextBlock; + +/** + * UCollectionCompleteWidget — 全部收集完成中心提示(执行案第四节「全部收集完成表现」)。 + * + * 屏幕正中央弹出「杂物全部收集完成!」白色大字,显示 2.0s 后淡出移除。 + * 三段时序:淡入 → 停留(DisplayDuration) → 淡出 → RemoveFromParent。 + * + * 绑定要求(蓝图 WBP_CollectionComplete 继承本类): + * - TextBlock 名 "CompleteTextBlock" — 完成文字 + * - WidgetAnimation 名 "FadeInAnim" — 淡入动画 + * - WidgetAnimation 名 "FadeOutAnim" — 淡出动画 + * + * 布局要求:Anchors Center,白色大字,屏幕正中。 + */ +UCLASS(Abstract, Blueprintable) +class UCollectionCompleteWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** 显示完成提示:设置文字,启动淡入→停留→淡出时序 */ + UFUNCTION(BlueprintCallable, Category = "Collection|Complete") + void ShowComplete(const FString& Text, float DisplayDuration, float InFadeOutDuration); + +protected: + + /** 完成文字 TextBlock,蓝图子类必须有同名 TextBlock */ + UPROPERTY(meta = (BindWidget)) + UTextBlock* CompleteTextBlock; + + /** 淡入动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeInAnim; + + /** 淡出动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeOutAnim; + + /** 停留计时器(淡入结束后启动,到时触发淡出)*/ + FTimerHandle StayTimerHandle; + + /** 淡出计时器(淡出开始后启动,到时移除 Widget)*/ + FTimerHandle FadeOutTimerHandle; + + /** 淡出时长(缓存)*/ + float CachedFadeOutDuration = 0.5f; + + virtual void NativeOnInitialized() override; + +private: + + /** 停留结束 → 播放淡出动画 */ + void StartFadeOut(); + + /** 淡出时长到 → RemoveFromParent */ + void OnFadeOutComplete(); +}; diff --git a/Source/ChildHood/Collection/CollectionHUDComponent.cpp b/Source/ChildHood/Collection/CollectionHUDComponent.cpp new file mode 100644 index 0000000..293ab81 --- /dev/null +++ b/Source/ChildHood/Collection/CollectionHUDComponent.cpp @@ -0,0 +1,265 @@ +// ChildHood Project - 收集系统表现层 实现 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(数据结构)+ 第6节(行为规则实现)+ 第9节 + +#include "Collection/CollectionHUDComponent.h" +#include "SystemDriver.h" +#include "Collection/CollectionManager.h" +#include "Collection/CollectionNotifWidget.h" +#include "Collection/CollectionProgressWidget.h" +#include "Collection/CollectionCompleteWidget.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "GameFramework/PlayerController.h" +#include "Blueprint/UserWidget.h" +#include "ChildHood.h" + +// ============================================================================ +// 构造 / 生命周期 +// ============================================================================ + +UCollectionHUDComponent::UCollectionHUDComponent() +{ + // 需要每帧 Tick 读取 Manager 缓存(开销极低:几个 int32 比较) + PrimaryComponentTick.bCanEveryTick = true; + PrimaryComponentTick.bStartWithTickEnabled = true; +} + +void UCollectionHUDComponent::Initialize(ASystemDriver* InDriver) +{ + DriverPtr = InDriver; +} + +void UCollectionHUDComponent::BeginPlay() +{ + Super::BeginPlay(); +} + +void UCollectionHUDComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // 清理 timer:NotifDelayTimer 的 lambda 与 CompleteTimer 的成员函数绑定均捕获裸 this, + // 组件销毁后 fire 会野指针。显式 ClearTimer 防御(UE5 Widget/Timer 卫生写法,与项目其他 Character 一致)。 + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().ClearTimer(NotifDelayTimerHandle); + World->GetTimerManager().ClearTimer(CompleteTimerHandle); + } + + // 移除挂载的通知 Widget + for (UCollectionNotifWidget* Notif : ActiveNotifications) + { + if (Notif) + { + Notif->RemoveFromParent(); + } + } + ActiveNotifications.Empty(); + + // 移除常驻进度 Widget + if (ProgressWidget) + { + ProgressWidget->RemoveFromParent(); + ProgressWidget = nullptr; + } + + Super::EndPlay(EndPlayReason); +} + +// ============================================================================ +// Tick — 只读 Manager 缓存,检测变化驱动 UI +// ============================================================================ + +void UCollectionHUDComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // 获取 Driver(优先缓存,失效则全局查找并更新缓存) + ASystemDriver* Driver = DriverPtr.Get(); + if (!Driver) + { + Driver = ASystemDriver::Get(GetWorld()); + if (Driver) + { + DriverPtr = Driver; + } + } + if (!Driver) return; + + // 懒创建常驻进度 Widget(首次有效 Tick 时,确保 World 就绪 + 规避 Driver BeginPlay 时序竞争) + if (!ProgressWidget && ProgressWidgetClass) + { + CreateProgressWidget(Driver); + } + if (!ProgressWidget) return; + + const FCollectionManager& CollMgr = Driver->GetCollectionManager(); + const FCollectionCachedInfo& Cache = CollMgr.GetCachedInfo(); + + // === 检测进度变化(步骤4:弹收集通知)=== + // LastSeenProgress < 0 表示首次 Tick,由 CreateProgressWidget 初始化为当前进度,不触发通知 + if (Cache.Progress != LastSeenProgress && LastSeenProgress >= 0) + { + // 更新进度 UI(同步)+ 弹性动画 + UpdateProgress(Cache.Progress, Cache.TotalCount); + + // 弹收集通知(延迟 NotifDelay 后创建,避免与拾取通知视觉重叠 — 执行案第二节) + const float NotifDelay = CollMgr.GetConfig().NotifDelay; + const FString NotifText = Cache.PendingNotificationText; + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(NotifDelayTimerHandle, [this, NotifText]() + { + ShowNotification(NotifText); + }, NotifDelay, false); + } + } + LastSeenProgress = Cache.Progress; + + // === 检测完成(步骤5:完成判定 → 完成表现)=== + if (Cache.bJustCompleted && !bCompletionShown) + { + TriggerCompletionSequence(Driver); + bCompletionShown = true; + } +} + +// ============================================================================ +// 进度 Widget +// ============================================================================ + +void UCollectionHUDComponent::CreateProgressWidget(ASystemDriver* Driver) +{ + UWorld* World = GetWorld(); + if (!World) return; + + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC) return; + + ProgressWidget = CreateWidget(PC, ProgressWidgetClass); + if (!ProgressWidget) return; + + // 常驻显示(ZOrder 5,低于通知/完成提示) + ProgressWidget->AddToViewport(5); + + // 初始显示已恢复的进度(执行案第三节:游戏开始后始终可见) + const FCollectionCachedInfo& Cache = Driver->GetCollectionManager().GetCachedInfo(); + ProgressWidget->UpdateProgress(Cache.Progress, Cache.TotalCount); + if (Cache.bIsComplete) + { + ProgressWidget->SetCompleteStyle(); + } + + // 记录初始进度,避免首次 Tick 误触发通知 + LastSeenProgress = Cache.Progress; + + UE_LOG(LogChildHood, Log, TEXT("CollectionHUDComponent: 进度 Widget 创建完成,初始进度 %d/%d"), + Cache.Progress, Cache.TotalCount); +} + +// ============================================================================ +// 收集通知 +// ============================================================================ + +void UCollectionHUDComponent::ShowNotification(const FString& Text) +{ + ASystemDriver* Driver = DriverPtr.Get(); + if (!Driver) return; + + const FCollectionConfig& Config = Driver->GetCollectionManager().GetConfig(); + + // 同屏最大数量限制(执行案第二节) + if (ActiveNotifications.Num() >= Config.MaxSimultaneousNotifs) + { + UE_LOG(LogChildHood, Verbose, TEXT("CollectionHUDComponent: 通知已达上限 %d,跳过"), Config.MaxSimultaneousNotifs); + return; + } + + UWorld* World = GetWorld(); + if (!NotifWidgetClass || !World) return; + + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC) return; + + UCollectionNotifWidget* NotifWidget = CreateWidget(PC, NotifWidgetClass); + if (!NotifWidget) return; + + // 绑定完成回调(淡出移除时清理 ActiveNotifications) + NotifWidget->OnFinished.BindUFunction(this, FName("OnNotificationRemoved")); + + NotifWidget->InitializeNotif(Text, Config.NotifTextColor, + Config.NotifFadeInDuration, Config.NotifStayDuration, Config.NotifFadeOutDuration); + NotifWidget->AddToViewport(10); + + ActiveNotifications.Add(NotifWidget); +} + +// ============================================================================ +// 进度更新 +// ============================================================================ + +void UCollectionHUDComponent::UpdateProgress(int32 Current, int32 Total) +{ + if (ProgressWidget) + { + ProgressWidget->UpdateProgress(Current, Total); + // 数字弹性动画(执行案第三节) + ProgressWidget->PlayBounceAnimation(); + } +} + +// ============================================================================ +// 完成序列 +// ============================================================================ + +void UCollectionHUDComponent::TriggerCompletionSequence(ASystemDriver* Driver) +{ + if (!ProgressWidget) return; + + const FCollectionConfig& Config = Driver->GetCollectionManager().GetConfig(); + + // 1. 进度 UI 变金色 + 完成弹性动画(执行案第四节) + ProgressWidget->SetCompleteStyle(); + ProgressWidget->PlayCompleteAnimation(); + + // 2. 延迟弹出完成提示(等最后一条通知消失 + CompletePromptDelay) + const float TotalDelay = Config.NotifDelay + Config.NotifStayDuration + + Config.NotifFadeOutDuration + Config.CompletePromptDelay; + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(CompleteTimerHandle, this, + &UCollectionHUDComponent::ShowCompletionPrompt, TotalDelay, false); + } + + UE_LOG(LogChildHood, Log, TEXT("CollectionHUDComponent: 触发完成序列,%.2fs 后弹出完成提示"), TotalDelay); +} + +void UCollectionHUDComponent::ShowCompletionPrompt() +{ + UWorld* World = GetWorld(); + if (!CompleteWidgetClass || !World) return; + + APlayerController* PC = World->GetFirstPlayerController(); + if (!PC) return; + + UCollectionCompleteWidget* CompleteWidget = CreateWidget(PC, CompleteWidgetClass); + if (!CompleteWidget) return; + + // 完成提示参数(执行案第四节:白色大字,显示 2.0s 后淡出) + ASystemDriver* Driver = DriverPtr.Get(); + const float CompleteDuration = Driver ? Driver->GetCollectionManager().GetConfig().CompletePromptDuration : 2.0f; + const float FadeOutDuration = 0.5f; + + CompleteWidget->ShowComplete(TEXT("杂物全部收集完成!"), CompleteDuration, FadeOutDuration); + CompleteWidget->AddToViewport(20); +} + +// ============================================================================ +// 通知清理回调 +// ============================================================================ + +void UCollectionHUDComponent::OnNotificationRemoved(UCollectionNotifWidget* Widget) +{ + if (Widget) + { + ActiveNotifications.RemoveSingle(Widget); + } +} diff --git a/Source/ChildHood/Collection/CollectionHUDComponent.h b/Source/ChildHood/Collection/CollectionHUDComponent.h new file mode 100644 index 0000000..80635bd --- /dev/null +++ b/Source/ChildHood/Collection/CollectionHUDComponent.h @@ -0,0 +1,102 @@ +// ChildHood Project - 收集系统表现层组件 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionHUDComponent)+ 第5节 + 第6节 +// 职责:只读 FCollectionManager 缓存,驱动收集通知/进度/完成 UI +// 架构:UActorComponent(挂在 AChildHoodCharacter 上),自有 TickComponent, +// 只读 Manager 缓存,绝不访问 InteractableComponent 或 UPersistenceSubsystem + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "CollectionHUDComponent.generated.h" + +class ASystemDriver; +class UCollectionNotifWidget; +class UCollectionProgressWidget; +class UCollectionCompleteWidget; + +/** + * UCollectionHUDComponent — 收集系统表现层(执行案第二/三/四节 UI)。 + * + * 数据流(技术方案第9节 Presentation 流): + * TickComponent 每帧 + * → Driver->GetCollectionManager().GetCachedInfo() + * → Cache.Progress != LastSeenProgress? → UpdateProgress + SetTimer(NotifDelay) → ShowNotification + * → Cache.bJustCompleted && !bCompletionShown? → TriggerCompletionSequence + * + * 不持有 Manager 所有权,不写 Manager 状态,不碰 PersistenceSubsystem(技术方案注意事项3)。 + */ +UCLASS(ClassGroup = (Collection), meta = (BlueprintSpawnableComponent)) +class UCollectionHUDComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + + UCollectionHUDComponent(); + + /** 初始化(Character BeginPlay 调用,存 Driver 弱引用;可为空,Tick 兜底查找)*/ + UFUNCTION(BlueprintCallable, Category = "Collection|HUD") + void Initialize(ASystemDriver* InDriver); + +protected: + + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + // === UI 类引用(蓝图/实例设置:创建 WBP 后在此指定)=== + + /** 收集通知 Widget 蓝图类(WBP_CollectionNotification)*/ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI") + TSubclassOf NotifWidgetClass; + + /** 进度 HUD Widget 蓝图类(WBP_CollectionProgress)*/ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI") + TSubclassOf ProgressWidgetClass; + + /** 完成提示 Widget 蓝图类(WBP_CollectionComplete)*/ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI") + TSubclassOf CompleteWidgetClass; + +private: + + // === Driver 弱引用(Tick 时优先用,失效则全局查找)=== + TWeakObjectPtr DriverPtr; + + // === 变化检测(Presentation 层状态)=== + int32 LastSeenProgress = -1; // 上次看到的进度,-1 = 未初始化 + bool bCompletionShown = false; // 完成表现是否已显示 + + // === UI 实例 === + UPROPERTY() + TArray> ActiveNotifications; + + UPROPERTY() + TObjectPtr ProgressWidget = nullptr; + + // === 定时器 === + FTimerHandle NotifDelayTimerHandle; // 通知延迟(0.3s) + FTimerHandle CompleteTimerHandle; // 完成提示延迟 + + // === 内部方法 === + + /** 创建常驻进度 Widget(懒创建:首次有效 Tick 时,避免与 Driver BeginPlay 时序竞争)*/ + void CreateProgressWidget(ASystemDriver* Driver); + + /** 弹出收集通知(检查同屏最大数量限制)*/ + void ShowNotification(const FString& Text); + + /** 更新进度 UI + 弹性动画 */ + void UpdateProgress(int32 Current, int32 Total); + + /** 触发完成序列:进度金色 + 延迟完成提示 */ + void TriggerCompletionSequence(ASystemDriver* Driver); + + /** 显示完成提示(延迟后调用)*/ + void ShowCompletionPrompt(); + + /** 通知 Widget 淡出移除回调 — 从 ActiveNotifications 清理 */ + UFUNCTION() + void OnNotificationRemoved(UCollectionNotifWidget* Widget); +}; diff --git a/Source/ChildHood/Collection/CollectionManager.cpp b/Source/ChildHood/Collection/CollectionManager.cpp new file mode 100644 index 0000000..91a76cb --- /dev/null +++ b/Source/ChildHood/Collection/CollectionManager.cpp @@ -0,0 +1,140 @@ +// ChildHood Project - 收集系统逻辑核心 实现 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(数据结构)+ 第6节(行为规则实现)+ 第9节(数据流) + +#include "Collection/CollectionManager.h" +#include "Interaction/PersistenceSubsystem.h" +#include "ChildHood.h" + +// ============================================================================ +// 初始化 +// ============================================================================ + +void FCollectionManager::Initialize() +{ + // 重置本地运行时状态 + Progress = 0; + CollectedIDs.Empty(); + bIsComplete = false; + + // 初始化缓存(稳定态,无瞬时通知) + CachedInfo.Progress = 0; + CachedInfo.TotalCount = Config.TotalCount; + CachedInfo.bIsComplete = false; + CachedInfo.ResetTransient(); +} + +void FCollectionManager::RestoreFromPersistence(UPersistenceSubsystem* Subsystem) +{ + // 缓存持久层弱引用,供后续 SaveStateToPersistence 使用 + PersistencePtr = Subsystem; + + if (!Subsystem) + { + UE_LOG(LogChildHood, Warning, TEXT("CollectionManager: RestoreFromPersistence - 持久层为空,保持初始状态")); + return; + } + + // 从持久层恢复 + TSet SavedIDs; + int32 SavedProgress = 0; + bool bSavedComplete = false; + Subsystem->GetCollectionState(SavedIDs, SavedProgress, bSavedComplete); + + CollectedIDs = MoveTemp(SavedIDs); + Progress = SavedProgress; + bIsComplete = bSavedComplete; + + // 初始化缓存(恢复后的稳定态,无瞬时通知 — 不触发 UI 动画) + CachedInfo.Progress = Progress; + CachedInfo.TotalCount = Config.TotalCount; + CachedInfo.bIsComplete = bIsComplete; + CachedInfo.ResetTransient(); + + UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 从持久层恢复 - 进度 %d/%d, 完成 %s"), + Progress, Config.TotalCount, bIsComplete ? TEXT("是") : TEXT("否")); +} + +// ============================================================================ +// 物品事件入口 +// ============================================================================ + +void FCollectionManager::OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category) +{ + // 步骤1: 查品类 — 非追踪品类忽略(执行案第一节步骤1) + if (Category != Config.TrackedCategory) + { + UE_LOG(LogChildHood, Verbose, TEXT("CollectionManager: 忽略非追踪品类物品 '%s' (Category=%s)"), + *DisplayName, *Category.ToString()); + return; + } + + ProcessNewCollection(ItemId, DisplayName); +} + +// ============================================================================ +// 内部:处理新收集(步骤2-5) +// ============================================================================ + +void FCollectionManager::ProcessNewCollection(FName ItemId, const FString& DisplayName) +{ + // 步骤2: 查重复 — 已收集则流程结束,不增加 progress,不弹收集通知 + if (CollectedIDs.Contains(ItemId)) + { + UE_LOG(LogChildHood, Verbose, TEXT("CollectionManager: 重复物品 '%s',忽略"), *DisplayName); + return; + } + + // 步骤3: 记录 — 加入集合,Progress 取 Set 大小作为权威值 + CollectedIDs.Add(ItemId); + Progress = CollectedIDs.Num(); + SaveStateToPersistence(); + + // 步骤4: 刷新 Presentation 缓存(供 HUDComponent Tick 检测) + // LastCollectedItemId / bJustCompleted 由本方法控制;RefreshCachedInfo 只刷新进度+通知文本 + CachedInfo.LastCollectedItemId = ItemId; + CachedInfo.bJustCompleted = false; // 默认非完成,步骤5判定时置 true + RefreshCachedInfo(DisplayName); + + UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 新收集 '%s',进度 %d/%d"), + *DisplayName, Progress, Config.TotalCount); + + // 步骤5: 完成判定 — progress 达到 TotalCount 则 isComplete = true + if (Progress >= Config.TotalCount && !bIsComplete) + { + bIsComplete = true; + CachedInfo.bJustCompleted = true; // 触发 HUDComponent 完成表现 + CachedInfo.bIsComplete = true; + SaveStateToPersistence(); + UE_LOG(LogChildHood, Log, TEXT("CollectionManager: 全部收集完成!")); + } +} + +// ============================================================================ +// 内部:刷新 Presentation 缓存 +// ============================================================================ + +void FCollectionManager::RefreshCachedInfo(const FString& DisplayName) +{ + // 更新进度相关字段 + CachedInfo.Progress = Progress; + CachedInfo.TotalCount = Config.TotalCount; + CachedInfo.bIsComplete = bIsComplete; + + // 生成收集通知文本(执行案第二节):「发现了新的杂物,进度 X/Y」 + CachedInfo.PendingNotificationText = FString::Printf( + TEXT("发现了新的杂物,进度 %d/%d"), Progress, Config.TotalCount); + + // 注意:LastCollectedItemId / bJustCompleted 由 ProcessNewCollection 控制,此处不碰 +} + +// ============================================================================ +// 内部:持久化 +// ============================================================================ + +void FCollectionManager::SaveStateToPersistence() +{ + if (UPersistenceSubsystem* Subsystem = PersistencePtr.Get()) + { + Subsystem->SetCollectionState(CollectedIDs, Progress, bIsComplete); + } +} diff --git a/Source/ChildHood/Collection/CollectionManager.h b/Source/ChildHood/Collection/CollectionManager.h new file mode 100644 index 0000000..6673da8 --- /dev/null +++ b/Source/ChildHood/Collection/CollectionManager.h @@ -0,0 +1,84 @@ +// ChildHood Project - 收集系统逻辑核心(v1.0 架构解耦) +// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(类设计)+ 第5节(数据结构)+ 第6节(行为规则实现) +// 职责:品类检查、去重判定、记录持久化、完成判定、缓存 Presentation 数据 +// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有。 +// 纯事件驱动无 Tick,不持有任何 UObject 强引用(与 FInteractionManager/FEffectManager 模式一致)。 + +#pragma once + +#include "CoreMinimal.h" +#include "Collection/CollectionTypes.h" + +class UPersistenceSubsystem; + +/** + * FCollectionManager + * + * 收集系统逻辑核心。追踪单一品类「杂物」的收集进度(执行案 v0.1 范围)。 + * + * 数据流(技术方案第9节): + * FEffectManager AddToBackpack Action + * → ASystemDriver 回调 OnItemAdded(ItemId, DisplayName, Category) + * → OnItemObtained() + * ├─ 步骤1: 查品类 (Category == Config.TrackedCategory?) + * ├─ 步骤2: 查重复 (CollectedIDs.Contains?) + * ├─ 步骤3: 记录 (CollectedIDs.Add + Progress = Num()) + SaveStateToPersistence() + * ├─ 步骤4: 刷新 Presentation 缓存(RefreshCachedInfo) + * └─ 步骤5: 完成判定 (Progress >= TotalCount) → bJustCompleted + * + * Presentation 层(UCollectionHUDComponent)每帧 Tick 只读 GetCachedInfo(), + * 通过比较 LastSeenProgress 检测变化驱动 UI,绝不访问 Manager 内部状态或 Subsystem。 + */ +class FCollectionManager +{ +public: + + // === 初始化 === + + /** 重置本地运行时状态(由 ASystemDriver::BeginPlay 调用,先于 RestoreFromPersistence) */ + void Initialize(); + + /** 从持久层恢复收集状态,并缓存 PersistenceSubsystem 弱引用(供后续写入用)。 + * v0.1 单场景:持久层为空数据,等效于初始化为 0(技术方案第12节)。 */ + void RestoreFromPersistence(UPersistenceSubsystem* Subsystem); + + // === 物品事件入口(由 ASystemDriver 回调触发)=== + + /** 物品获得事件 — 执行查品类→查重复→记录→完成判定(技术方案步骤1-5) */ + void OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category); + + // === Presentation 层访问(只读)=== + + /** 获取 Presentation 缓存(HUDComponent 每帧只读) */ + const FCollectionCachedInfo& GetCachedInfo() const { return CachedInfo; } + + /** 获取配置参数(HUDComponent 读取通知/动画参数) */ + const FCollectionConfig& GetConfig() const { return Config; } + +private: + + // === 配置 === + FCollectionConfig Config; + + // === 运行时状态 === + int32 Progress = 0; // 当前已收集数(= CollectedIDs.Num(),权威值) + TSet CollectedIDs; // 已收集物品 ID 集合(仅追踪品类) + bool bIsComplete = false; // 是否全部收集完成 + + // === Presentation 缓存(供 HUDComponent 只读)=== + FCollectionCachedInfo CachedInfo; + + // === 持久层弱引用(RestoreFromPersistence 时设置,SaveStateToPersistence 用)=== + TWeakObjectPtr PersistencePtr; + + // === 内部方法 === + + /** 处理新收集:记录 + 持久化 + 刷新缓存 + 完成判定(步骤2-5) */ + void ProcessNewCollection(FName ItemId, const FString& DisplayName); + + /** 刷新 Presentation 缓存(更新进度、生成通知文本)— 保持技术方案签名(DisplayName) */ + void RefreshCachedInfo(const FString& DisplayName); + + /** 将当前状态写入持久层(记录后 + 完成判定后调用) */ + void SaveStateToPersistence(); +}; diff --git a/Source/ChildHood/Collection/CollectionNotifWidget.cpp b/Source/ChildHood/Collection/CollectionNotifWidget.cpp new file mode 100644 index 0000000..317136b --- /dev/null +++ b/Source/ChildHood/Collection/CollectionNotifWidget.cpp @@ -0,0 +1,67 @@ +// ChildHood Project - 收集通知 UI 实现 +// 三段时序:淡入 → 停留 → 淡出 → 广播 OnFinished + 移除 + +#include "Collection/CollectionNotifWidget.h" +#include "Components/TextBlock.h" +#include "Engine/World.h" +#include "TimerManager.h" + +void UCollectionNotifWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); +} + +void UCollectionNotifWidget::InitializeNotif(const FString& Text, FLinearColor TextColor, + float InFadeInDuration, float InStayDuration, float InFadeOutDuration) +{ + CachedFadeOutDuration = InFadeOutDuration; + + // 设置文字 + 颜色 + if (NotifTextBlock) + { + NotifTextBlock->SetText(FText::FromString(Text)); + NotifTextBlock->SetColorAndOpacity(TextColor); + } + + // 淡入动画 + if (FadeInAnim) + { + PlayAnimation(FadeInAnim); + } + + // 停留时长后触发淡出 + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(StayTimerHandle, [this]() + { + StartFadeOut(); + }, InStayDuration, false); + } +} + +void UCollectionNotifWidget::StartFadeOut() +{ + // 淡出动画 + if (FadeOutAnim) + { + PlayAnimation(FadeOutAnim); + } + + // 淡出时长后移除(用计时器而非动画完成委托,避免委托绑定复杂度) + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().SetTimer(FadeOutTimerHandle, [this]() + { + OnFadeOutComplete(); + }, CachedFadeOutDuration, false); + } +} + +void UCollectionNotifWidget::OnFadeOutComplete() +{ + // 通知 HUDComponent 从 ActiveNotifications 移除本 Widget + OnFinished.ExecuteIfBound(this); + + // 从视口移除自己 + RemoveFromParent(); +} diff --git a/Source/ChildHood/Collection/CollectionNotifWidget.h b/Source/ChildHood/Collection/CollectionNotifWidget.h new file mode 100644 index 0000000..1ae0854 --- /dev/null +++ b/Source/ChildHood/Collection/CollectionNotifWidget.h @@ -0,0 +1,75 @@ +// ChildHood Project - 收集通知 UI 基类 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionNotifWidget)+ 第7节(美术资源) +// 职责:单条收集通知的三段生命周期(淡入→停留→淡出→移除) + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CollectionNotifWidget.generated.h" + +class UTextBlock; +class UCollectionNotifWidget; + +/** 收集通知单条完成事件 — 淡出移除后广播,HUDComponent 据此从 ActiveNotifications 清理 */ +DECLARE_DELEGATE_OneParam(FOnCollectionNotifFinished, UCollectionNotifWidget*); + +/** + * UCollectionNotifWidget — 收集通知 UI 单条实例(执行案第二节「收集通知 UI」)。 + * + * 三段生命周期:淡入(0.2s) → 停留(2.0s) → 淡出(0.3s) → 广播 OnFinished + RemoveFromParent。 + * 文字浅金色 #F0D060(与拾取通知白色区分)。 + * + * 绑定要求(蓝图 WBP_CollectionNotification 继承本类): + * - TextBlock 名 "NotifTextBlock" — 通知文字 + * - WidgetAnimation 名 "FadeInAnim" — 淡入动画 + * - WidgetAnimation 名 "FadeOutAnim" — 淡出动画 + */ +UCLASS(Abstract, Blueprintable) +class UCollectionNotifWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** 初始化通知:设置文字/颜色,启动淡入→停留→淡出三段时序 */ + UFUNCTION(BlueprintCallable, Category = "Collection|Notification") + void InitializeNotif(const FString& Text, FLinearColor TextColor, + float InFadeInDuration, float InStayDuration, float InFadeOutDuration); + + /** 通知完成事件(淡出移除后广播)— HUDComponent 绑定后清理 ActiveNotifications */ + FOnCollectionNotifFinished OnFinished; + +protected: + + /** 通知文字 TextBlock,蓝图子类必须有同名 TextBlock */ + UPROPERTY(meta = (BindWidget)) + UTextBlock* NotifTextBlock; + + /** 淡入动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeInAnim; + + /** 淡出动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* FadeOutAnim; + + /** 停留计时器(淡入结束后启动,到时触发淡出)*/ + FTimerHandle StayTimerHandle; + + /** 淡出计时器(淡出开始后启动,到时移除 Widget)*/ + FTimerHandle FadeOutTimerHandle; + + /** 淡出时长(缓存,供淡出计时器用)*/ + float CachedFadeOutDuration = 0.3f; + + virtual void NativeOnInitialized() override; + +private: + + /** 停留结束 → 播放淡出动画 */ + void StartFadeOut(); + + /** 淡出时长到 → 广播 OnFinished + RemoveFromParent */ + void OnFadeOutComplete(); +}; diff --git a/Source/ChildHood/Collection/CollectionProgressWidget.cpp b/Source/ChildHood/Collection/CollectionProgressWidget.cpp new file mode 100644 index 0000000..1b7d8ae --- /dev/null +++ b/Source/ChildHood/Collection/CollectionProgressWidget.cpp @@ -0,0 +1,44 @@ +// ChildHood Project - 收集进度 UI 实现 + +#include "Collection/CollectionProgressWidget.h" +#include "Components/TextBlock.h" + +void UCollectionProgressWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); +} + +void UCollectionProgressWidget::UpdateProgress(int32 Current, int32 Total) +{ + if (ProgressTextBlock) + { + // 格式:「📦 杂物 X/Y」(执行案第三节)。图标 emoji 在文本中,品类名 v0.1 固定「杂物」。 + ProgressTextBlock->SetText(FText::FromString( + FString::Printf(TEXT("📦 杂物 %d/%d"), Current, Total))); + } +} + +void UCollectionProgressWidget::SetCompleteStyle() +{ + if (ProgressTextBlock) + { + // 文字变金色(全部收集完成表现) + ProgressTextBlock->SetColorAndOpacity(CompleteColor); + } +} + +void UCollectionProgressWidget::PlayBounceAnimation() +{ + if (BounceAnim) + { + PlayAnimation(BounceAnim); + } +} + +void UCollectionProgressWidget::PlayCompleteAnimation() +{ + if (CompleteAnim) + { + PlayAnimation(CompleteAnim); + } +} diff --git a/Source/ChildHood/Collection/CollectionProgressWidget.h b/Source/ChildHood/Collection/CollectionProgressWidget.h new file mode 100644 index 0000000..e2f377a --- /dev/null +++ b/Source/ChildHood/Collection/CollectionProgressWidget.h @@ -0,0 +1,72 @@ +// ChildHood Project - 收集进度 UI 基类 +// 对应技术方案: docs/technical/收集系统-技术方案.md 第3节(UCollectionProgressWidget)+ 第7节 +// 职责:常驻进度 HUD(图标+品类名+进度文本)+ 数字弹性动画 + 完成金色变色 + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CollectionProgressWidget.generated.h" + +class UTextBlock; + +/** + * UCollectionProgressWidget — 收集进度常驻 HUD(执行案第三节「收集进度 UI」)。 + * + * 显示格式:「📦 杂物 X/Y」,游戏开始后常驻左上角。 + * 进度变化时数字弹性缩放;全部完成时文字变金色 + 完成动画。 + * + * 绑定要求(蓝图 WBP_CollectionProgress 继承本类): + * - TextBlock 名 "ProgressTextBlock" — 进度文本(如 "📦 杂物 3/10") + * - WidgetAnimation 名 "BounceAnim" — 数字弹性动画(1.2x, 0.2s) + * - WidgetAnimation 名 "CompleteAnim" — 完成金色动画(1.4x, 0.5s) + * + * 布局要求:Anchors Top Left,距左/上各 3% 屏幕边距(执行案第三节)。 + */ +UCLASS(Abstract, Blueprintable) +class UCollectionProgressWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + /** 更新进度文本(格式:「📦 杂物 X/Y」)*/ + UFUNCTION(BlueprintCallable, Category = "Collection|Progress") + void UpdateProgress(int32 Current, int32 Total); + + /** 设为完成样式:文字变金色(全部收集完成时)*/ + UFUNCTION(BlueprintCallable, Category = "Collection|Progress") + void SetCompleteStyle(); + + /** 播放数字弹性动画(单次新收集时)*/ + UFUNCTION(BlueprintCallable, Category = "Collection|Progress") + void PlayBounceAnimation(); + + /** 播放完成动画(全部收集完成时,配合 SetCompleteStyle)*/ + UFUNCTION(BlueprintCallable, Category = "Collection|Progress") + void PlayCompleteAnimation(); + +protected: + + /** 进度文本 TextBlock,蓝图子类必须有同名 TextBlock */ + UPROPERTY(meta = (BindWidget)) + UTextBlock* ProgressTextBlock; + + /** 数字弹性动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* BounceAnim; + + /** 完成金色动画,蓝图子类必须有同名 WidgetAnimation */ + UPROPERTY(meta = (BindWidgetAnim), Transient) + UWidgetAnimation* CompleteAnim; + + /** 正常文字颜色(白色)*/ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Collection|Progress") + FLinearColor NormalColor = FLinearColor::White; + + /** 完成文字颜色(浅金色 #F0D060)*/ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Collection|Progress") + FLinearColor CompleteColor = FLinearColor(0.94f, 0.82f, 0.38f, 1.f); + + virtual void NativeOnInitialized() override; +}; diff --git a/Source/ChildHood/Collection/CollectionTypes.h b/Source/ChildHood/Collection/CollectionTypes.h new file mode 100644 index 0000000..fd62d51 --- /dev/null +++ b/Source/ChildHood/Collection/CollectionTypes.h @@ -0,0 +1,162 @@ +// ChildHood Project - 收集系统数据类型定义(v1.0 架构解耦) +// 对应技术方案: docs/technical/收集系统-技术方案.md 第5节(核心数据结构) +// 职责:FCollectionConfig(配置,USTRUCT)+ FCollectionCachedInfo(Presentation 缓存,纯 C++) +// +// 设计说明: +// - FCollectionConfig 为 USTRUCT,承载全部可调参数,默认值与执行案「可调参数汇总」一一对应。 +// v0.1 以代码默认值承载,后续版本可迁入 DataAsset / 配置表(数据驱动原则)。 +// - FCollectionCachedInfo 为普通 struct(非 USTRUCT):仅在 FCollectionManager(普通 C++) +// 与 UCollectionHUDComponent(UActorComponent)之间传递,无需反射。 + +#pragma once + +#include "CoreMinimal.h" +#include "CollectionTypes.generated.h" + +// ============================================================================ +// FCollectionConfig — 收集系统配置(FCollectionManager 使用) +// ============================================================================ + +/** + * FCollectionConfig + * + * 收集系统全部可调参数。承载追踪品类、收集通知、进度 UI、完成表现四组参数。 + * 所有 ClampMin/ClampMax 与执行案「可调参数汇总」的可调范围一致。 + */ +USTRUCT(BlueprintType) +struct FCollectionConfig +{ + GENERATED_BODY() + + // === 追踪品类 === + + /** 追踪的物品品类名称(v0.1 固定为 "杂物") */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Category") + FName TrackedCategory = FName("杂物"); + + /** 品类总收集数(v0.1 = 10) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Category", + meta = (ClampMin = "1")) + int32 TotalCount = 10; + + // === 收集通知参数 === + + /** 拾取通知后到收集通知弹出的间隔(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification", + meta = (ClampMin = "0.1", ClampMax = "0.5")) + float NotifDelay = 0.3f; + + /** 通知淡入时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification", + meta = (ClampMin = "0.1", ClampMax = "0.3")) + float NotifFadeInDuration = 0.2f; + + /** 通知停留时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification", + meta = (ClampMin = "1.5", ClampMax = "3.0")) + float NotifStayDuration = 2.0f; + + /** 通知淡出时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification", + meta = (ClampMin = "0.2", ClampMax = "0.5")) + float NotifFadeOutDuration = 0.3f; + + /** 通知文字颜色(浅金色 #F0D060,与拾取通知白色区分) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification") + FLinearColor NotifTextColor = FLinearColor(0.94f, 0.82f, 0.38f, 1.f); + + /** 最大同屏收集通知数 */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Notification", + meta = (ClampMin = "1", ClampMax = "3")) + int32 MaxSimultaneousNotifs = 2; + + // === 进度 UI 参数 === + + /** 进度 UI 距左边距(屏幕宽度比例) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress", + meta = (ClampMin = "0.02", ClampMax = "0.05")) + float ProgressMarginX = 0.03f; + + /** 进度 UI 距上边距(屏幕高度比例) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress", + meta = (ClampMin = "0.02", ClampMax = "0.05")) + float ProgressMarginY = 0.03f; + + /** 进度数字弹性缩放倍率(单次新收集时) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress", + meta = (ClampMin = "1.1", ClampMax = "1.5")) + float BounceScale = 1.2f; + + /** 进度数字弹性时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Progress", + meta = (ClampMin = "0.1", ClampMax = "0.4")) + float BounceDuration = 0.2f; + + // === 完成表现参数 === + + /** 完成时金色缩放倍率(全部收集完成时) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete", + meta = (ClampMin = "1.2", ClampMax = "1.8")) + float CompleteScale = 1.4f; + + /** 完成缩放时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete", + meta = (ClampMin = "0.3", ClampMax = "0.8")) + float CompleteScaleDuration = 0.5f; + + /** 收集通知消失后到完成提示弹出的间隔(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete", + meta = (ClampMin = "0.3", ClampMax = "1.0")) + float CompletePromptDelay = 0.5f; + + /** 完成提示显示时长(秒) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Collection|Complete", + meta = (ClampMin = "1.5", ClampMax = "3.0")) + float CompletePromptDuration = 2.0f; +}; + +// ============================================================================ +// FCollectionCachedInfo — Presentation 层缓存(非 UObject,纯 C++) +// ============================================================================ + +/** + * FCollectionCachedInfo + * + * 收集系统 Presentation 层缓存。与交互系统的 FSelectedInfo 模式一致。 + * + * 写入方:FCollectionManager 在每次 OnItemObtained 成功记录新收集后,由 + * RefreshCachedInfo() 刷新此结构体。 + * 读取方:UCollectionHUDComponent 的 TickComponent 每帧只读此缓存,通过比较 + * LastSeenProgress 检测变化,驱动 UI 更新。 + * + * 表现层绝不直接访问 FCollectionManager 的内部状态或 UPersistenceSubsystem, + * 只通过此缓存 + GetConfig() 读取参数。 + */ +struct FCollectionCachedInfo +{ + /** 当前进度(已收集数) */ + int32 Progress = 0; + + /** 总数 */ + int32 TotalCount = 10; + + /** 是否已全部完成 */ + bool bIsComplete = false; + + /** 是否刚刚完成(本次 OnItemObtained 触发了完成判定) */ + bool bJustCompleted = false; + + /** 待显示的通知文本(如 "发现了新的杂物,进度 3/10") */ + FString PendingNotificationText; + + /** 本次收集的物品 ID(用于通知回调定位 / 调试) */ + FName LastCollectedItemId; + + /** 重置瞬时态(每次消费后调用,保留 Progress/TotalCount/bIsComplete) */ + void ResetTransient() + { + bJustCompleted = false; + PendingNotificationText.Empty(); + LastCollectedItemId = FName(); + } +}; diff --git a/Source/ChildHood/Interaction/EffectManager.cpp b/Source/ChildHood/Interaction/EffectManager.cpp index 68a8ed2..cace5e5 100644 --- a/Source/ChildHood/Interaction/EffectManager.cpp +++ b/Source/ChildHood/Interaction/EffectManager.cpp @@ -23,6 +23,11 @@ void FEffectManager::SetOnEffectComplete(FOnEffectComplete&& Callback) OnEffectComplete = MoveTemp(Callback); } +void FEffectManager::SetOnItemAddedCallback(FOnItemAdded&& Callback) +{ + OnItemAdded = MoveTemp(Callback); +} + void FEffectManager::PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context) { // 如果有正在播放的 Effect,先强制清理 @@ -147,14 +152,19 @@ void FEffectManager::ExecuteAction(const FInteractAction& Action) case EInteractActionType::ToggleDoor: ExecuteToggleDoor(); break; - // v0.1 用日志占位的 Action + // AddToBackpack:通过 Driver 回调链通知收集系统(收集系统技术方案第13节) case EInteractActionType::AddToBackpack: - // TODO: 后续集成收集系统(Issue #33 P-013 收集系统程序实现)。 - // 届时由 ASystemDriver 回调链通知 FCollectionManager,EffectManager 不直接处理业务逻辑。 - UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s"), - CurrentContext.TargetComp.IsValid() - ? *CurrentContext.TargetComp->GetInteractableData().DisplayName - : TEXT("Unknown")); + if (CurrentContext.TargetComp.IsValid()) + { + const FInteractableData& ItemData = CurrentContext.TargetComp->GetInteractableData(); + if (OnItemAdded.IsBound()) + { + // 回调连线:EffectManager → ASystemDriver → FCollectionManager::OnItemObtained + OnItemAdded.Execute(ItemData.ItemId, ItemData.DisplayName, ItemData.Category); + } + UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s (Category: %s)"), + *ItemData.DisplayName, *ItemData.Category.ToString()); + } break; case EInteractActionType::PlayMontageOnTarget: case EInteractActionType::PlayDialogue: diff --git a/Source/ChildHood/Interaction/EffectManager.h b/Source/ChildHood/Interaction/EffectManager.h index 9cdf872..5da6812 100644 --- a/Source/ChildHood/Interaction/EffectManager.h +++ b/Source/ChildHood/Interaction/EffectManager.h @@ -31,6 +31,10 @@ public: /** 设置完成回调(由 ASystemDriver 在 BeginPlay 中设置) */ void SetOnEffectComplete(FOnEffectComplete&& Callback); + /** 设置物品获得回调(AddToBackpack Action 执行时触发)。 + * 由 ASystemDriver 绑定到 FCollectionManager::OnItemObtained(收集系统技术方案第13节)。 */ + void SetOnItemAddedCallback(FOnItemAdded&& Callback); + /** 播放 Effect — 初始化状态,开始 Timeline 推进 */ void PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context); @@ -86,6 +90,9 @@ private: /** Effect 完成回调(正常结束或取消后清理完毕时触发) */ FOnEffectComplete OnEffectComplete; + /** 物品获得回调(AddToBackpack Action 触发,传 ItemId/DisplayName/Category) */ + FOnItemAdded OnItemAdded; + // === 内部方法 === /** 执行单个 Action(按 Type 分发到对应 ExecuteXxx) */ diff --git a/Source/ChildHood/Interaction/InteractionTypes.h b/Source/ChildHood/Interaction/InteractionTypes.h index 1d3f8b5..eeebd14 100644 --- a/Source/ChildHood/Interaction/InteractionTypes.h +++ b/Source/ChildHood/Interaction/InteractionTypes.h @@ -164,6 +164,12 @@ struct FInteractableData UPROPERTY(EditAnywhere, Category = "Basic") FString DisplayName; + /** 物品品类(如 "杂物"),用于收集系统品类过滤。 + * 拾取/翻找物填 "杂物"(收集系统追踪品类),观察物/门等非收集物留空。 + * P-013 收集系统通过 AddToBackpack 回调读取此字段做「查品类」。 */ + UPROPERTY(EditAnywhere, Category = "Basic") + FName Category; + // === 触发 === /** 触发半径(cm),默认1.5m */ @@ -299,3 +305,9 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( /** Effect 完成回调 */ DECLARE_DELEGATE(FOnEffectComplete); + +/** 物品获得回调 — AddToBackpack Action 执行时触发,参数 (ItemId, DisplayName, Category)。 + * 由 FEffectManager 持有,ASystemDriver 在 BeginPlay 中绑定到 FCollectionManager::OnItemObtained, + * 实现「交互系统 → 收集系统」的解耦连线(收集系统技术方案第13节)。 + * 普通委托(非动态多播):纯 C++ lambda 绑定,EffectManager 为普通 C++ 类。 */ +DECLARE_DELEGATE_ThreeParams(FOnItemAdded, FName, const FString&, const FName&); diff --git a/Source/ChildHood/Interaction/PersistenceSubsystem.cpp b/Source/ChildHood/Interaction/PersistenceSubsystem.cpp index 9c433bb..7b35a89 100644 --- a/Source/ChildHood/Interaction/PersistenceSubsystem.cpp +++ b/Source/ChildHood/Interaction/PersistenceSubsystem.cpp @@ -35,6 +35,26 @@ void UPersistenceSubsystem::SetDoorOpen(FName DoorId, bool bIsOpen) *DoorId.ToString(), bIsOpen ? TEXT("开") : TEXT("关")); } +// ============================================================================ +// 收集系统状态 +// ============================================================================ + +void UPersistenceSubsystem::GetCollectionState(TSet& OutIDs, int32& OutProgress, bool& OutComplete) const +{ + OutIDs = CollectionCollectedIDs; + OutProgress = CollectionProgress; + OutComplete = bCollectionComplete; +} + +void UPersistenceSubsystem::SetCollectionState(const TSet& InIDs, int32 InProgress, bool bInComplete) +{ + CollectionCollectedIDs = InIDs; + CollectionProgress = InProgress; + bCollectionComplete = bInComplete; + UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 收集状态更新 - 进度 %d, 完成 %s"), + CollectionProgress, bCollectionComplete ? TEXT("是") : TEXT("否")); +} + // ============================================================================ // 重置 // ============================================================================ @@ -43,5 +63,8 @@ void UPersistenceSubsystem::ResetAll() { CollectedItemIDs.Empty(); DoorStates.Empty(); + CollectionCollectedIDs.Empty(); + CollectionProgress = 0; + bCollectionComplete = false; UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 所有持久数据已重置")); } diff --git a/Source/ChildHood/Interaction/PersistenceSubsystem.h b/Source/ChildHood/Interaction/PersistenceSubsystem.h index c195687..238cbfb 100644 --- a/Source/ChildHood/Interaction/PersistenceSubsystem.h +++ b/Source/ChildHood/Interaction/PersistenceSubsystem.h @@ -51,6 +51,18 @@ public: UFUNCTION(BlueprintCallable, Category = "Persistence") void ResetAll(); + // === 收集系统状态 === + // 与上方 CollectedItemIDs 区分:CollectedItemIDs 记录交互物消耗状态(交互系统用), + // 下方三个成员记录追踪品类的收集进度(收集系统用,技术方案第12节)。 + + /** 获取收集状态(恢复用):追踪品类的已收集 ID 集合、当前进度、是否全部完成 */ + UFUNCTION(BlueprintCallable, Category = "Persistence|Collection") + void GetCollectionState(TSet& OutIDs, int32& OutProgress, bool& OutComplete) const; + + /** 设置收集状态(收集时写入) */ + UFUNCTION(BlueprintCallable, Category = "Persistence|Collection") + void SetCollectionState(const TSet& InIDs, int32 InProgress, bool bInComplete); + private: /** 已收集物品 ID 集合 */ @@ -60,4 +72,18 @@ private: /** 门开关状态映射(true=打开,false=关闭) */ UPROPERTY() TMap DoorStates; + + // === 收集系统数据 === + + /** 追踪品类的已收集物品 ID 集合(收集系统用) */ + UPROPERTY() + TSet CollectionCollectedIDs; + + /** 当前收集进度(收集系统用) */ + UPROPERTY() + int32 CollectionProgress = 0; + + /** 是否已全部收集完成(收集系统用) */ + UPROPERTY() + bool bCollectionComplete = false; }; diff --git a/Source/ChildHood/SystemDriver.cpp b/Source/ChildHood/SystemDriver.cpp index 8b706a2..b075ef5 100644 --- a/Source/ChildHood/SystemDriver.cpp +++ b/Source/ChildHood/SystemDriver.cpp @@ -11,6 +11,7 @@ #include "GameFramework/PlayerController.h" #include "GameFramework/Character.h" #include "ChildHoodCharacter.h" +#include "Interaction/PersistenceSubsystem.h" // ============================================================================ // 构造函数 @@ -60,6 +61,32 @@ void ASystemDriver::BeginPlay() UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 初始化完成")); } + + // --- 初始化 CollectionManager(收集系统,技术方案第13节回调连线)--- + { + // 1. 本地状态重置 + CollectionManager.Initialize(); + + // 2. 从持久层恢复进度 + 缓存 Subsystem 弱引用 + if (UGameInstance* GI = GetGameInstance()) + { + if (UPersistenceSubsystem* Persistence = GI->GetSubsystem()) + { + CollectionManager.RestoreFromPersistence(Persistence); + } + } + + // 3. 回调连线:EffectManager AddToBackpack Action → CollectionManager.OnItemObtained + // 交互系统与收集系统不直接耦合,通过 Driver 的 lambda 连线(v1.0 解耦核心) + FOnItemAdded ItemAddedCallback; + ItemAddedCallback.BindLambda([this](FName ItemId, const FString& DisplayName, const FName& Category) + { + CollectionManager.OnItemObtained(ItemId, DisplayName, Category); + }); + EffectManager.SetOnItemAddedCallback(MoveTemp(ItemAddedCallback)); + + UE_LOG(LogChildHood, Log, TEXT("SystemDriver: CollectionManager 初始化完成 + AddToBackpack 回调连线")); + } } void ASystemDriver::Tick(float DeltaTime) diff --git a/Source/ChildHood/SystemDriver.h b/Source/ChildHood/SystemDriver.h index b9da23e..2eb256c 100644 --- a/Source/ChildHood/SystemDriver.h +++ b/Source/ChildHood/SystemDriver.h @@ -10,6 +10,7 @@ #include "GameFramework/Info.h" #include "Interaction/InteractionManager.h" #include "Interaction/EffectManager.h" +#include "Collection/CollectionManager.h" #include "SystemDriver.generated.h" /** @@ -58,6 +59,10 @@ public: FEffectManager& GetEffectManager() { return EffectManager; } const FEffectManager& GetEffectManager() const { return EffectManager; } + /** 获取收集管理器(值引用)— 收集系统逻辑核心 */ + FCollectionManager& GetCollectionManager() { return CollectionManager; } + const FCollectionManager& GetCollectionManager() const { return CollectionManager; } + private: /** 交互管理器 */ @@ -66,6 +71,9 @@ private: /** 效果管理器 */ FEffectManager EffectManager; + /** 收集管理器(收集系统逻辑核心) */ + FCollectionManager CollectionManager; + // === Effect 完成回调连线 === /** EffectManager 的 OnEffectComplete 回调目标 */