Files
ChildHood/Source/ChildHood/Collection/CollectionHUDComponent.cpp
T
2026-06-15 05:13:28 +08:00

266 lines
8.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}