3cc3ac0d7f
P-013遗留: PrimaryComponentTick.bCanEveryTick 是不存在的属性 (正确拼写为 bCanEverTick),导致该组件Tick实际未启用。本次随P-016 编译验证一并修正。 Co-Authored-By: Claude Opus 5.8 <noreply@anthropic.com>
266 lines
8.8 KiB
C++
266 lines
8.8 KiB
C++
// 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.bCanEverTick = 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);
|
||
}
|
||
}
|