feat(collection): P-013收集系统三层架构解耦实现

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 05:13:28 +08:00
parent 2f0a2133a1
commit 4fc4a39aa6
21 changed files with 1286 additions and 7 deletions
+1
View File
@@ -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",
+18
View File
@@ -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<UInteractionPromptComponent>(TEXT("PromptComponent"));
// 创建收集系统 HUD 组件(表现层,挂在角色上;Tick 兜底查找 Driver)
CollectionHUDComponent = CreateDefaultSubobject<UCollectionHUDComponent>(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;
+9
View File
@@ -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);
@@ -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();
}
@@ -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();
};
@@ -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<UCollectionProgressWidget>(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<UCollectionNotifWidget>(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<UCollectionCompleteWidget>(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);
}
}
@@ -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<UCollectionNotifWidget> NotifWidgetClass;
/** 进度 HUD Widget 蓝图类(WBP_CollectionProgress)*/
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI")
TSubclassOf<UCollectionProgressWidget> ProgressWidgetClass;
/** 完成提示 Widget 蓝图类(WBP_CollectionComplete)*/
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Collection|UI")
TSubclassOf<UCollectionCompleteWidget> CompleteWidgetClass;
private:
// === Driver 弱引用(Tick 时优先用,失效则全局查找)===
TWeakObjectPtr<ASystemDriver> DriverPtr;
// === 变化检测(Presentation 层状态)===
int32 LastSeenProgress = -1; // 上次看到的进度,-1 = 未初始化
bool bCompletionShown = false; // 完成表现是否已显示
// === UI 实例 ===
UPROPERTY()
TArray<TObjectPtr<UCollectionNotifWidget>> ActiveNotifications;
UPROPERTY()
TObjectPtr<UCollectionProgressWidget> 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);
};
@@ -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<FName> 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);
}
}
@@ -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<FName> CollectedIDs; // 已收集物品 ID 集合(仅追踪品类)
bool bIsComplete = false; // 是否全部收集完成
// === Presentation 缓存(供 HUDComponent 只读)===
FCollectionCachedInfo CachedInfo;
// === 持久层弱引用(RestoreFromPersistence 时设置,SaveStateToPersistence 用)===
TWeakObjectPtr<UPersistenceSubsystem> PersistencePtr;
// === 内部方法 ===
/** 处理新收集:记录 + 持久化 + 刷新缓存 + 完成判定(步骤2-5) */
void ProcessNewCollection(FName ItemId, const FString& DisplayName);
/** 刷新 Presentation 缓存(更新进度、生成通知文本)— 保持技术方案签名(DisplayName) */
void RefreshCachedInfo(const FString& DisplayName);
/** 将当前状态写入持久层(记录后 + 完成判定后调用) */
void SaveStateToPersistence();
};
@@ -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();
}
@@ -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();
};
@@ -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);
}
}
@@ -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;
};
@@ -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();
}
};
+17 -7
View File
@@ -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:
@@ -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) */
@@ -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&);
@@ -35,6 +35,26 @@ void UPersistenceSubsystem::SetDoorOpen(FName DoorId, bool bIsOpen)
*DoorId.ToString(), bIsOpen ? TEXT("开") : TEXT("关"));
}
// ============================================================================
// 收集系统状态
// ============================================================================
void UPersistenceSubsystem::GetCollectionState(TSet<FName>& OutIDs, int32& OutProgress, bool& OutComplete) const
{
OutIDs = CollectionCollectedIDs;
OutProgress = CollectionProgress;
OutComplete = bCollectionComplete;
}
void UPersistenceSubsystem::SetCollectionState(const TSet<FName>& 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: 所有持久数据已重置"));
}
@@ -51,6 +51,18 @@ public:
UFUNCTION(BlueprintCallable, Category = "Persistence")
void ResetAll();
// === 收集系统状态 ===
// 与上方 CollectedItemIDs 区分:CollectedItemIDs 记录交互物消耗状态(交互系统用),
// 下方三个成员记录追踪品类的收集进度(收集系统用,技术方案第12节)。
/** 获取收集状态(恢复用):追踪品类的已收集 ID 集合、当前进度、是否全部完成 */
UFUNCTION(BlueprintCallable, Category = "Persistence|Collection")
void GetCollectionState(TSet<FName>& OutIDs, int32& OutProgress, bool& OutComplete) const;
/** 设置收集状态(收集时写入) */
UFUNCTION(BlueprintCallable, Category = "Persistence|Collection")
void SetCollectionState(const TSet<FName>& InIDs, int32 InProgress, bool bInComplete);
private:
/** 已收集物品 ID 集合 */
@@ -60,4 +72,18 @@ private:
/** 门开关状态映射(true=打开,false=关闭) */
UPROPERTY()
TMap<FName, bool> DoorStates;
// === 收集系统数据 ===
/** 追踪品类的已收集物品 ID 集合(收集系统用) */
UPROPERTY()
TSet<FName> CollectionCollectedIDs;
/** 当前收集进度(收集系统用) */
UPROPERTY()
int32 CollectionProgress = 0;
/** 是否已全部收集完成(收集系统用) */
UPROPERTY()
bool bCollectionComplete = false;
};
+27
View File
@@ -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<UPersistenceSubsystem>())
{
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)
+8
View File
@@ -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 回调目标 */