Compare commits
2 Commits
690cfeb29d
...
c14c63f30d
| Author | SHA1 | Date | |
|---|---|---|---|
| c14c63f30d | |||
| 4bf678b937 |
@@ -1,3 +1,8 @@
|
||||
// ChildHood Project - 玩家角色 实现(v1.1 Effect/Action 架构适配)
|
||||
// 变更:通过 ASystemDriver 访问 FInteractionManager + FEffectManager
|
||||
// 新增 BeginPlay 初始化 PromptComponent
|
||||
// 删除对旧 UInteractionManager 的依赖
|
||||
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "ChildHoodCharacter.h"
|
||||
@@ -11,7 +16,12 @@
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "ChildHood.h"
|
||||
#include "Interaction/InteractionPromptComponent.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "Interaction/EffectManager.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/InteractionTypes.h"
|
||||
|
||||
AChildHoodCharacter::AChildHoodCharacter()
|
||||
{
|
||||
@@ -55,8 +65,22 @@ AChildHoodCharacter::AChildHoodCharacter()
|
||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||
FollowCamera->bUsePawnControlRotation = false;
|
||||
|
||||
// 创建交互管理器组件
|
||||
InteractionManager = CreateDefaultSubobject<UInteractionManager>(TEXT("InteractionManager"));
|
||||
// 创建交互提示组件(替代旧 InteractionManager)
|
||||
PromptComponent = CreateDefaultSubobject<UInteractionPromptComponent>(TEXT("PromptComponent"));
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 初始化 PromptComponent — 将其与 ASystemDriver 关联
|
||||
if (PromptComponent)
|
||||
{
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
PromptComponent->Initialize(Driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
@@ -71,7 +95,7 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput
|
||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||
|
||||
// Interaction — delegates to InteractionManager
|
||||
// Interaction — delegates to ASystemDriver
|
||||
if (InteractAction)
|
||||
{
|
||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
|
||||
@@ -111,30 +135,59 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value)
|
||||
|
||||
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
|
||||
{
|
||||
if (UInteractionManager* IM = GetInteractionManager())
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& Mgr = Driver->GetInteractionManager();
|
||||
auto& EFM = Driver->GetEffectManager();
|
||||
|
||||
// E 键三重功能:
|
||||
// 1. Free 状态:发起交互
|
||||
// 2. WaitForInput 暂停态:恢复 Effect(观察/对话类按 E 继续)
|
||||
// 3. 可取消 Effect 执行中:取消 Effect
|
||||
|
||||
if (Mgr.GetState() == EInteractionState::Free)
|
||||
{
|
||||
// E 键双重功能:Free 发起交互,Active 结束交互
|
||||
IM->BeginInteract();
|
||||
IM->CancelInteract();
|
||||
// 发起交互
|
||||
auto* Selected = Mgr.BeginInteract();
|
||||
if (!Selected) return;
|
||||
|
||||
SetInteracting(true);
|
||||
|
||||
FEffectContext Ctx;
|
||||
Ctx.Instigator = this;
|
||||
Ctx.TargetComp = Selected;
|
||||
Ctx.TargetActor = Selected->GetOwner();
|
||||
EFM.PlayEffect(Selected->GetInteractableData().InteractEffect, Ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Executing 状态 → 尝试取消/恢复
|
||||
EFM.CancelEffect();
|
||||
}
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
|
||||
{
|
||||
if (UInteractionManager* IM = GetInteractionManager())
|
||||
{
|
||||
IM->CancelInteract();
|
||||
}
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& EFM = Driver->GetEffectManager();
|
||||
EFM.CancelEffect();
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value)
|
||||
{
|
||||
if (UInteractionManager* IM = GetInteractionManager())
|
||||
{
|
||||
float DirectionValue = Value.Get<float>();
|
||||
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
||||
IM->SwitchTarget(Direction);
|
||||
}
|
||||
ASystemDriver* Driver = ASystemDriver::Get(this);
|
||||
if (!Driver) return;
|
||||
|
||||
auto& Mgr = Driver->GetInteractionManager();
|
||||
// 交互中不可切换
|
||||
if (Mgr.GetState() != EInteractionState::Free) return;
|
||||
|
||||
float DirectionValue = Value.Get<float>();
|
||||
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
||||
Mgr.SwitchTarget(Direction);
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
||||
@@ -177,9 +230,9 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch)
|
||||
// 交互系统方法
|
||||
// ============================================================================
|
||||
|
||||
UInteractionManager* AChildHoodCharacter::GetInteractionManager() const
|
||||
UInteractionPromptComponent* AChildHoodCharacter::GetPromptComponent() const
|
||||
{
|
||||
return InteractionManager;
|
||||
return PromptComponent;
|
||||
}
|
||||
|
||||
void AChildHoodCharacter::SetInteracting(bool bValue)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// ChildHood Project - 玩家角色(v1.1 Effect/Action 架构适配)
|
||||
// 变更:移除 UInteractionManager,改用 UInteractionPromptComponent
|
||||
// 交互操作通过 ASystemDriver 访问 FInteractionManager + FEffectManager
|
||||
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
@@ -10,7 +14,8 @@
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UInteractionManager;
|
||||
class UInteractionPromptComponent;
|
||||
class ASystemDriver;
|
||||
struct FInputActionValue;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
@@ -18,6 +23,10 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
/**
|
||||
* A simple player-controllable third person character
|
||||
* Implements a controllable orbiting camera
|
||||
*
|
||||
* v1.1 架构变更:
|
||||
* - 交互操作通过 ASystemDriver 全局单例访问 FInteractionManager / FEffectManager
|
||||
* - 身上挂载 UInteractionPromptComponent 负责 Free 状态下的 Prompt UI 追踪
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class AChildHoodCharacter : public ACharacter
|
||||
@@ -58,21 +67,23 @@ protected:
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* SwitchInteractAction;
|
||||
|
||||
/** 交互管理器组件 */
|
||||
/** 交互提示 UI 组件 — Free 状态下追踪选中交互物并显示 Prompt */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
|
||||
UInteractionManager* InteractionManager;
|
||||
UInteractionPromptComponent* PromptComponent;
|
||||
|
||||
/** Whether the character is currently in an interaction */
|
||||
bool bIsInteracting = false;
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
AChildHoodCharacter();
|
||||
|
||||
/** 获取交互管理器组件 */
|
||||
/** 获取交互提示组件 */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
||||
UInteractionManager* GetInteractionManager() const;
|
||||
UInteractionPromptComponent* GetPromptComponent() const;
|
||||
|
||||
/** Set interaction state — called by interaction system */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
@@ -91,21 +102,19 @@ protected:
|
||||
/** Initialize input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for looking input */
|
||||
void Look(const FInputActionValue& Value);
|
||||
|
||||
/** Called for interact input (E) — delegates to InteractionManager */
|
||||
/** Called for interact input (E) — delegates to ASystemDriver */
|
||||
void OnInteract(const FInputActionValue& Value);
|
||||
|
||||
/** Called for cancel input (ESC) — delegates to InteractionManager */
|
||||
/** Called for cancel input (ESC) — delegates to ASystemDriver */
|
||||
void OnCancel(const FInputActionValue& Value);
|
||||
|
||||
/** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */
|
||||
/** Called for switch interact target input (Tab/Wheel) — delegates to ASystemDriver */
|
||||
void OnSwitchInteract(const FInputActionValue& Value);
|
||||
|
||||
public:
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
// ChildHood Project - 跨关卡持久数据持有者 实现
|
||||
// ChildHood Project - GameInstance(v1.1 清理版)
|
||||
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
|
||||
|
||||
#include "ChildHoodGameInstance.h"
|
||||
|
||||
bool UChildHoodGameInstance::IsItemCollected(FName ItemId) const
|
||||
{
|
||||
return CollectedItemIDs.Contains(ItemId);
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::MarkItemCollected(FName ItemId)
|
||||
{
|
||||
CollectedItemIDs.Add(ItemId);
|
||||
}
|
||||
|
||||
bool UChildHoodGameInstance::IsDoorOpen(FName DoorId) const
|
||||
{
|
||||
const bool* Found = DoorStates.Find(DoorId);
|
||||
return Found ? *Found : false;
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::SetDoorOpen(FName DoorId, bool bIsOpen)
|
||||
{
|
||||
DoorStates.Add(DoorId, bIsOpen);
|
||||
}
|
||||
|
||||
void UChildHoodGameInstance::ResetAll()
|
||||
{
|
||||
CollectedItemIDs.Empty();
|
||||
DoorStates.Empty();
|
||||
}
|
||||
// v0.1: 空壳实现,持久数据由 UPersistenceSubsystem 管理
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ChildHood Project - 跨关卡持久数据持有者
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
|
||||
// ChildHood Project - GameInstance(v1.1 清理版)
|
||||
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
/**
|
||||
* UChildHoodGameInstance
|
||||
*
|
||||
* 跨关卡持久数据持有者。UE5 的 OpenLevel() 会销毁整个旧 World(包括
|
||||
* Character 及其所有 UActorComponent),因此需要将持久数据与运行时行为分离。
|
||||
*
|
||||
* v0.1 为单场景原型,此 GameInstance 以空数据初始化。
|
||||
* 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为此类即可。
|
||||
* v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理。
|
||||
* 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能。
|
||||
*/
|
||||
UCLASS()
|
||||
class UChildHoodGameInstance : public UGameInstance
|
||||
@@ -22,35 +19,6 @@ class UChildHoodGameInstance : public UGameInstance
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// === 物品消耗状态 ===
|
||||
|
||||
/** 查询指定物品是否已被收集(跨场景持久) */
|
||||
bool IsItemCollected(FName ItemId) const;
|
||||
|
||||
/** 标记指定物品为已收集(写入持久层) */
|
||||
void MarkItemCollected(FName ItemId);
|
||||
|
||||
// === 门状态 ===
|
||||
|
||||
/** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */
|
||||
bool IsDoorOpen(FName DoorId) const;
|
||||
|
||||
/** 设置指定门的开关状态(写入持久层) */
|
||||
void SetDoorOpen(FName DoorId, bool bIsOpen);
|
||||
|
||||
// === 重置 ===
|
||||
|
||||
/** 重置所有持久数据(新游戏时调用) */
|
||||
void ResetAll();
|
||||
|
||||
private:
|
||||
|
||||
/** 已收集物品ID集合 */
|
||||
UPROPERTY()
|
||||
TSet<FName> CollectedItemIDs;
|
||||
|
||||
/** 门开关状态映射(true=打开,false=关闭) */
|
||||
UPROPERTY()
|
||||
TMap<FName, bool> DoorStates;
|
||||
// v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理
|
||||
// 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// ChildHood Project - 门 Actor 实现
|
||||
|
||||
#include "Interaction/DoorActor.h"
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
ADoorActor::ADoorActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// 基类构造函数已创建 MeshComp(根组件)和 InteractableComp
|
||||
|
||||
// 创建门专用交互组件,替代基类的通用 InteractableComp
|
||||
DoorInteractableComp = CreateDefaultSubobject<UDoorInteractableComponent>(TEXT("DoorInteractableComp"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 交互回调
|
||||
// ============================================================================
|
||||
|
||||
void ADoorActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
|
||||
{
|
||||
// 只响应门组件的交互
|
||||
if (UDoorInteractableComponent* DoorComp = Cast<UDoorInteractableComponent>(Comp))
|
||||
{
|
||||
DoorComp->ToggleDoor();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 视觉更新
|
||||
// ============================================================================
|
||||
|
||||
void ADoorActor::UpdateDoorVisual(bool bIsOpen)
|
||||
{
|
||||
if (!MeshComp) return;
|
||||
|
||||
// 瞬时旋转:开门偏移 OpenYawOffset 度,关门回到 0 度
|
||||
const float TargetYaw = bIsOpen ? OpenYawOffset : 0.f;
|
||||
MeshComp->SetRelativeRotation(FRotator(0.f, TargetYaw, 0.f));
|
||||
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("DoorActor Visual: Yaw=%.0f (%s)"),
|
||||
TargetYaw, bIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// ChildHood Project - 门 Actor
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:门的视觉表现(Mesh 旋转)、交互回调分发
|
||||
// 派生自 AInteractableActor,使用 UDoorInteractableComponent 替代基类组件
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Interaction/InteractableActor.h"
|
||||
#include "DoorActor.generated.h"
|
||||
|
||||
class UDoorInteractableComponent;
|
||||
|
||||
/**
|
||||
* ADoorActor
|
||||
*
|
||||
* 门类型的交互物 Actor。在基类基础上:
|
||||
* - 使用 UDoorInteractableComponent 替代 UInteractableComponent
|
||||
* - OnInteractStarted 中调用 ToggleDoor 并更新 Mesh 旋转
|
||||
* - UpdateDoorVisual 执行实际的 Mesh Yaw 旋转(开门+90, 关门0)
|
||||
*
|
||||
* v0.1 阶段旋转为瞬时 SetRelativeRotation,不使用动画插值。
|
||||
*/
|
||||
UCLASS()
|
||||
class ADoorActor : public AInteractableActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
ADoorActor();
|
||||
|
||||
// === 组件访问 ===
|
||||
|
||||
/** 获取门交互组件 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Door")
|
||||
UDoorInteractableComponent* GetDoorInteractableComp() const { return DoorInteractableComp; }
|
||||
|
||||
// === 交互回调 ===
|
||||
|
||||
/**
|
||||
* 交互开始时调用。
|
||||
* Cast 到 UDoorInteractableComponent 后调用 ToggleDoor,
|
||||
* 由 Component 负责状态切换和持久化,Actor 只负责视觉。
|
||||
*/
|
||||
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp) override;
|
||||
|
||||
// === 视觉更新 ===
|
||||
|
||||
/**
|
||||
* 更新门 Mesh 的视觉旋转。
|
||||
* bIsOpen=true → Yaw +90 度(打开)
|
||||
* bIsOpen=false → Yaw 0 度(关闭)
|
||||
* 由 UDoorInteractableComponent 在 ToggleDoor/RestorePersistentState 中调用。
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Door")
|
||||
void UpdateDoorVisual(bool bIsOpen);
|
||||
|
||||
protected:
|
||||
|
||||
/** 门交互组件(替代基类的 InteractableComp,提供门特有状态和行为) */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
TObjectPtr<UDoorInteractableComponent> DoorInteractableComp;
|
||||
|
||||
/** 门打开时的 Yaw 偏移(度),可在编辑器中调整 */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door", meta = (ClampMin = "0", ClampMax = "180"))
|
||||
float OpenYawOffset = 90.f;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
// ChildHood Project - 门交互组件 实现
|
||||
|
||||
#include "Interaction/DoorInteractableComponent.h"
|
||||
#include "Interaction/DoorActor.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/GameInstance.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
UDoorInteractableComponent::UDoorInteractableComponent()
|
||||
{
|
||||
// 门不需要 Tick,状态变更由交互事件驱动
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 门操作
|
||||
// ============================================================================
|
||||
|
||||
void UDoorInteractableComponent::ToggleDoor()
|
||||
{
|
||||
bDoorIsOpen = !bDoorIsOpen;
|
||||
|
||||
// 通知 ADoorActor 更新视觉旋转
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
|
||||
{
|
||||
DoorActor->UpdateDoorVisual(bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化门状态
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
|
||||
{
|
||||
Persistence->SetDoorOpen(Data.ItemId, bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("Door Toggle: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 提示文本
|
||||
// ============================================================================
|
||||
|
||||
FString UDoorInteractableComponent::GetPromptText_Implementation() const
|
||||
{
|
||||
// 根据门状态选择动词:已打开用 Alt 动词(如"关上"),已关闭用主动词(如"打开")
|
||||
const FString& Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
|
||||
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 跨场景持久状态恢复
|
||||
// ============================================================================
|
||||
|
||||
void UDoorInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence)
|
||||
{
|
||||
if (!Persistence) return;
|
||||
|
||||
// 1. 先调用基类恢复消耗状态
|
||||
Super::RestorePersistentState(Persistence);
|
||||
|
||||
// 2. 恢复门开关状态
|
||||
bDoorIsOpen = Persistence->IsDoorOpen(Data.ItemId);
|
||||
|
||||
// 3. 通知 DoorActor 更新视觉旋转
|
||||
if (AActor* Owner = GetOwner())
|
||||
{
|
||||
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
|
||||
{
|
||||
DoorActor->UpdateDoorVisual(bDoorIsOpen);
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("Door RestorePersistentState: %s → %s"),
|
||||
*Data.ItemId.ToString(),
|
||||
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
// ChildHood Project - Effect Timeline 调度器 实现
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节 + 第6节
|
||||
|
||||
#include "Interaction/EffectManager.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "Interaction/PushCameraActor.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
|
||||
// ============================================================================
|
||||
// 公开接口
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::SetOnEffectComplete(FOnEffectComplete&& Callback)
|
||||
{
|
||||
OnEffectComplete = MoveTemp(Callback);
|
||||
}
|
||||
|
||||
void FEffectManager::PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context)
|
||||
{
|
||||
// 如果有正在播放的 Effect,先强制清理
|
||||
if (bIsPlaying)
|
||||
{
|
||||
PerformCleanup();
|
||||
}
|
||||
|
||||
CurrentEffect = Effect;
|
||||
CurrentContext = Context;
|
||||
ElapsedTime = 0.f;
|
||||
NextActionIndex = 0;
|
||||
bIsPlaying = true;
|
||||
bIsWaitingForInput = false;
|
||||
TurnState.Reset();
|
||||
LastNarrativeIndex = -1;
|
||||
CachedPushCamera = nullptr;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayEffect '%s' (Duration=%.2f, Actions=%d)"),
|
||||
*CurrentEffect.EffectName, CurrentEffect.TotalDuration, CurrentEffect.Actions.Num());
|
||||
}
|
||||
|
||||
void FEffectManager::CancelEffect()
|
||||
{
|
||||
if (!bIsPlaying) return;
|
||||
if (!CurrentEffect.bCancellable) return;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CancelEffect (WaitingForInput=%s)"),
|
||||
bIsWaitingForInput ? TEXT("true") : TEXT("false"));
|
||||
|
||||
if (bIsWaitingForInput)
|
||||
{
|
||||
// 用户输入是期望的恢复信号 -> 走正常恢复
|
||||
ResumeFromWait();
|
||||
return;
|
||||
}
|
||||
|
||||
// 非暂停态强制取消
|
||||
bIsPlaying = false;
|
||||
PerformCleanup();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 每帧更新
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::Update(float DeltaTime)
|
||||
{
|
||||
if (!bIsPlaying) return;
|
||||
if (bIsWaitingForInput) return; // 暂停等待用户输入
|
||||
|
||||
// 1. 推进时间
|
||||
ElapsedTime += DeltaTime;
|
||||
|
||||
// 2. 检查是否到达 TotalDuration
|
||||
if (ElapsedTime >= CurrentEffect.TotalDuration)
|
||||
{
|
||||
FinishEffect();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 触发到时的 Action
|
||||
while (NextActionIndex < CurrentEffect.Actions.Num())
|
||||
{
|
||||
const FInteractAction& Action = CurrentEffect.Actions[NextActionIndex];
|
||||
if (ElapsedTime >= Action.TriggerTime)
|
||||
{
|
||||
ExecuteAction(Action);
|
||||
NextActionIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新 TurnToTarget 插值
|
||||
UpdateTurn(DeltaTime);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action 分发
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::ExecuteAction(const FInteractAction& Action)
|
||||
{
|
||||
switch (Action.Type)
|
||||
{
|
||||
case EInteractActionType::Wait:
|
||||
ExecuteWait(Action);
|
||||
break;
|
||||
case EInteractActionType::TurnToTarget:
|
||||
ExecuteTurnToTarget(Action);
|
||||
break;
|
||||
case EInteractActionType::PlayMontage:
|
||||
ExecutePlayMontage(Action);
|
||||
break;
|
||||
case EInteractActionType::CameraPush:
|
||||
ExecuteCameraPush(Action);
|
||||
break;
|
||||
case EInteractActionType::CameraReturn:
|
||||
ExecuteCameraReturn(Action);
|
||||
break;
|
||||
case EInteractActionType::Consume:
|
||||
ExecuteConsume();
|
||||
break;
|
||||
case EInteractActionType::HideActor:
|
||||
ExecuteHideActor();
|
||||
break;
|
||||
case EInteractActionType::ShowActor:
|
||||
ExecuteShowActor();
|
||||
break;
|
||||
case EInteractActionType::ShowNarrative:
|
||||
ExecuteShowNarrative(Action);
|
||||
break;
|
||||
case EInteractActionType::HideNarrative:
|
||||
ExecuteHideNarrative();
|
||||
break;
|
||||
case EInteractActionType::ShowPickupNotification:
|
||||
ExecuteShowPickupNotification();
|
||||
break;
|
||||
case EInteractActionType::ToggleDoor:
|
||||
ExecuteToggleDoor();
|
||||
break;
|
||||
// v0.1 用日志占位的 Action
|
||||
case EInteractActionType::AddToBackpack:
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s"),
|
||||
CurrentContext.TargetComp.IsValid()
|
||||
? *CurrentContext.TargetComp->GetInteractableData().DisplayName
|
||||
: TEXT("Unknown"));
|
||||
break;
|
||||
case EInteractActionType::PlayMontageOnTarget:
|
||||
case EInteractActionType::PlayDialogue:
|
||||
case EInteractActionType::PlayStory:
|
||||
case EInteractActionType::BroadcastEvent:
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: Action %d not implemented in v0.1"),
|
||||
static_cast<int32>(Action.Type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Effect 结束 / 清理 / 恢复
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::FinishEffect()
|
||||
{
|
||||
bIsPlaying = false;
|
||||
PerformCleanup();
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: FinishEffect '%s'"), *CurrentEffect.EffectName);
|
||||
|
||||
// 广播完成回调
|
||||
if (OnEffectComplete.IsBound())
|
||||
{
|
||||
OnEffectComplete.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ResumeFromWait()
|
||||
{
|
||||
bIsWaitingForInput = false;
|
||||
// Timeline 继续推进,后续 Action 按 TriggerTime 正常触发
|
||||
}
|
||||
|
||||
void FEffectManager::PerformCleanup()
|
||||
{
|
||||
// 1. 文本底框淡出移除(v0.1 日志占位,后续替换为 Widget 操作)
|
||||
// TODO: 如果 NarrativeWidget 显示中,触发淡出动画
|
||||
|
||||
// 2. 相机回位(如果推镜过)
|
||||
if (CachedPushCamera.IsValid())
|
||||
{
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (PC)
|
||||
{
|
||||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), 0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁推镜摄像机
|
||||
APushCameraActor* PushCam = CachedPushCamera.Get();
|
||||
CachedPushCamera = nullptr;
|
||||
if (PushCam && PushCam->GetWorld())
|
||||
{
|
||||
PushCam->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 蒙太奇停止
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||||
{
|
||||
Char->GetMesh()->GetAnimInstance()->Montage_Stop(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. TurnToTarget 未完成 -> 瞬移到目标旋转
|
||||
if (TurnState.bIsActive && CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||||
TurnState.Reset();
|
||||
}
|
||||
|
||||
// 5. 重置所有状态变量
|
||||
ElapsedTime = 0.f;
|
||||
NextActionIndex = 0;
|
||||
bIsWaitingForInput = false;
|
||||
TurnState.Reset();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 各 Action 实现
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::ExecuteWait(const FInteractAction& Action)
|
||||
{
|
||||
// Wait Action 由 Timeline 自然推进处理,此处仅做日志
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Wait (%.2fs)"), Action.WaitDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteTurnToTarget(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid() || !CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
FVector DirToTarget = CurrentContext.TargetActor->GetActorLocation() - Char->GetActorLocation();
|
||||
DirToTarget.Z = 0;
|
||||
DirToTarget.Normalize();
|
||||
|
||||
TurnState.bIsActive = true;
|
||||
TurnState.StartRotation = Char->GetActorRotation();
|
||||
TurnState.TargetRotation = DirToTarget.Rotation();
|
||||
TurnState.ElapsedTime = 0.f;
|
||||
TurnState.Duration = Action.TurnDuration;
|
||||
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: TurnToTarget (Duration=%.2fs)"), Action.TurnDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecutePlayMontage(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
if (!Action.Montage) return;
|
||||
|
||||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||||
{
|
||||
Char->GetMesh()->GetAnimInstance()->Montage_Play(Action.Montage);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayMontage '%s'"),
|
||||
*Action.Montage->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteCameraPush(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (!PC) return;
|
||||
|
||||
// 计算推镜摄像机位置:沿角色到目标方向偏移
|
||||
FVector CharLoc = CurrentContext.Instigator.Get()->GetActorLocation();
|
||||
FVector DirToTarget(0.f, 0.f, 0.f);
|
||||
if (CurrentContext.TargetActor.IsValid())
|
||||
{
|
||||
DirToTarget = (CurrentContext.TargetActor->GetActorLocation() - CharLoc).GetSafeNormal();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有目标 Actor 时使用角色朝向
|
||||
DirToTarget = CurrentContext.Instigator.Get()->GetActorForwardVector();
|
||||
}
|
||||
|
||||
FVector PushLoc = CharLoc + DirToTarget * Action.PushDistance + FVector(0.f, 0.f, 80.f);
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = CurrentContext.Instigator.Get();
|
||||
APushCameraActor* PushCam = CurrentContext.Instigator.Get()->GetWorld()->SpawnActor<APushCameraActor>(
|
||||
PushLoc, DirToTarget.Rotation(), Params);
|
||||
|
||||
if (PushCam)
|
||||
{
|
||||
PC->SetViewTargetWithBlend(PushCam, Action.PushDuration);
|
||||
CachedPushCamera = PushCam;
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraPush (Dist=%.1f, Dur=%.2fs)"),
|
||||
Action.PushDistance, Action.PushDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteCameraReturn(const FInteractAction& Action)
|
||||
{
|
||||
if (!CurrentContext.Instigator.IsValid()) return;
|
||||
|
||||
APlayerController* PC = Cast<APlayerController>(
|
||||
CurrentContext.Instigator.Get()->GetController());
|
||||
if (!PC) return;
|
||||
|
||||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), Action.ReturnDuration);
|
||||
|
||||
// 延迟销毁 PushCamera(在 PerformCleanup 中统一清理)
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraReturn (Dur=%.2fs)"), Action.ReturnDuration);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteConsume()
|
||||
{
|
||||
if (!CurrentContext.TargetComp.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetComp->Consume();
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Consume - %s"),
|
||||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteHideActor()
|
||||
{
|
||||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(true);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideActor"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowActor()
|
||||
{
|
||||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||||
|
||||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(false);
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: ShowActor"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action)
|
||||
{
|
||||
FString SelectedText;
|
||||
if (Action.bRandomSelect && Action.NarrativeTexts.Num() > 1)
|
||||
{
|
||||
SelectedText = PickRandomNarrative(Action.NarrativeTexts);
|
||||
}
|
||||
else if (Action.NarrativeTexts.Num() > 0)
|
||||
{
|
||||
SelectedText = Action.NarrativeTexts[0];
|
||||
}
|
||||
|
||||
// v0.1: 日志输出(后续替换为 Widget 显示)
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative - %s"), *SelectedText);
|
||||
|
||||
if (Action.bWaitForInput)
|
||||
{
|
||||
bIsWaitingForInput = true;
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: WaitForInput paused"));
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteHideNarrative()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 操作)
|
||||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative"));
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteShowPickupNotification()
|
||||
{
|
||||
// v0.1: 日志占位(后续替换为 Widget 显示)
|
||||
if (CurrentContext.TargetComp.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification - %s"),
|
||||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
void FEffectManager::ExecuteToggleDoor()
|
||||
{
|
||||
// v0.1: 日志占位(门逻辑由 UDoorInteractableComponent 派生类实现,Phase 2)
|
||||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ToggleDoor (placeholder)"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TurnToTarget 插值
|
||||
// ============================================================================
|
||||
|
||||
void FEffectManager::UpdateTurn(float DeltaTime)
|
||||
{
|
||||
if (!TurnState.bIsActive) return;
|
||||
|
||||
TurnState.ElapsedTime += DeltaTime;
|
||||
|
||||
if (TurnState.IsComplete())
|
||||
{
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||||
}
|
||||
TurnState.bIsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float Alpha = TurnState.GetAlpha();
|
||||
FRotator NewRot = FMath::Lerp(TurnState.StartRotation, TurnState.TargetRotation, Alpha);
|
||||
if (CurrentContext.Instigator.IsValid())
|
||||
{
|
||||
CurrentContext.Instigator.Get()->SetActorRotation(NewRot);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
FString FEffectManager::PickRandomNarrative(const TArray<FString>& Texts)
|
||||
{
|
||||
if (Texts.Num() == 0) return TEXT("");
|
||||
if (Texts.Num() == 1) return Texts[0];
|
||||
|
||||
int32 CandidateIndex;
|
||||
do
|
||||
{
|
||||
CandidateIndex = FMath::RandRange(0, Texts.Num() - 1);
|
||||
} while (CandidateIndex == LastNarrativeIndex && Texts.Num() > 1);
|
||||
|
||||
LastNarrativeIndex = CandidateIndex;
|
||||
return Texts[CandidateIndex];
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// ChildHood Project - Effect Timeline 调度器(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(复杂接口描述)
|
||||
// 职责:Effect Timeline 调度、Action 执行分发、TurnToTarget 插值、WaitForInput 暂停机制
|
||||
// 注意:普通 C++ 类,非 UObject,由 ASystemDriver 持有并 Tick 驱动
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InteractionTypes.h"
|
||||
|
||||
class APushCameraActor;
|
||||
|
||||
/**
|
||||
* FEffectManager
|
||||
*
|
||||
* Effect Timeline 调度器,管理一次交互 Effect 的完整生命周期:
|
||||
* - 按 TriggerTime 依次触发各 Action
|
||||
* - 维护 TurnToTarget 跨帧插值
|
||||
* - 支持 WaitForInput 暂停/恢复机制
|
||||
* - 支持取消(CancelEffect)
|
||||
* - Effect 结束时通过 OnEffectComplete 回调通知上层
|
||||
*
|
||||
* 生命周期:由 ASystemDriver 在 BeginPlay 中构造,每帧由 ASystemDriver::Tick 调用 Update。
|
||||
*/
|
||||
class FEffectManager
|
||||
{
|
||||
public:
|
||||
|
||||
// === 公开接口 ===
|
||||
|
||||
/** 设置完成回调(由 ASystemDriver 在 BeginPlay 中设置) */
|
||||
void SetOnEffectComplete(FOnEffectComplete&& Callback);
|
||||
|
||||
/** 播放 Effect — 初始化状态,开始 Timeline 推进 */
|
||||
void PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context);
|
||||
|
||||
/** 取消 Effect(用户按 E/ESC) — 区分 WaitForInput 恢复 vs 强制取消 */
|
||||
void CancelEffect();
|
||||
|
||||
/** 当前是否正在播放 Effect */
|
||||
bool IsPlaying() const { return bIsPlaying; }
|
||||
|
||||
/** 当前是否暂停等待用户输入 */
|
||||
bool IsWaitingForInput() const { return bIsWaitingForInput; }
|
||||
|
||||
/** 每帧更新(由 ASystemDriver::Tick 调用) */
|
||||
void Update(float DeltaTime);
|
||||
|
||||
private:
|
||||
|
||||
// === Effect 状态 ===
|
||||
|
||||
/** 当前播放中的 Effect 数据 */
|
||||
FInteractEffect CurrentEffect;
|
||||
|
||||
/** 当前 Effect 的运行时上下文 */
|
||||
FEffectContext CurrentContext;
|
||||
|
||||
/** 从 Effect 开始累计的经过时间 */
|
||||
float ElapsedTime = 0.f;
|
||||
|
||||
/** 下一个待触发的 Action 索引(Actions 按 TriggerTime 排序) */
|
||||
int32 NextActionIndex = 0;
|
||||
|
||||
/** 是否正在播放 Effect */
|
||||
bool bIsPlaying = false;
|
||||
|
||||
/** 是否暂停等待用户输入(ShowNarrative bWaitForInput=true 时设置) */
|
||||
bool bIsWaitingForInput = false;
|
||||
|
||||
/** 上一次随机选取的叙事文本索引(用于避免连续重复) */
|
||||
int32 LastNarrativeIndex = -1;
|
||||
|
||||
// === Turn 插值 ===
|
||||
|
||||
/** TurnToTarget 跨帧插值状态 */
|
||||
FTurnState TurnState;
|
||||
|
||||
// === 推镜缓存 ===
|
||||
|
||||
/** 推镜期间 Spawn 的临时摄像机(弱引用,Effect 结束时销毁) */
|
||||
TWeakObjectPtr<APushCameraActor> CachedPushCamera;
|
||||
|
||||
// === 回调 ===
|
||||
|
||||
/** Effect 完成回调(正常结束或取消后清理完毕时触发) */
|
||||
FOnEffectComplete OnEffectComplete;
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 执行单个 Action(按 Type 分发到对应 ExecuteXxx) */
|
||||
void ExecuteAction(const FInteractAction& Action);
|
||||
|
||||
/** Effect 正常结束 — 清理状态 + 广播回调 */
|
||||
void FinishEffect();
|
||||
|
||||
/** 从 WaitForInput 暂停恢复 */
|
||||
void ResumeFromWait();
|
||||
|
||||
/** 清理所有运行时状态(推镜相机、蒙太奇、转向等) */
|
||||
void PerformCleanup();
|
||||
|
||||
/** 从 NarrativeTexts 中随机选取一条(避免与上次重复) */
|
||||
FString PickRandomNarrative(const TArray<FString>& Texts);
|
||||
|
||||
// === 各 Action 处理 ===
|
||||
|
||||
void ExecuteWait(const FInteractAction& Action);
|
||||
void ExecuteTurnToTarget(const FInteractAction& Action);
|
||||
void ExecutePlayMontage(const FInteractAction& Action);
|
||||
void ExecuteCameraPush(const FInteractAction& Action);
|
||||
void ExecuteCameraReturn(const FInteractAction& Action);
|
||||
void ExecuteConsume();
|
||||
void ExecuteHideActor();
|
||||
void ExecuteShowActor();
|
||||
void ExecuteShowNarrative(const FInteractAction& Action);
|
||||
void ExecuteHideNarrative();
|
||||
void ExecuteShowPickupNotification();
|
||||
void ExecuteToggleDoor();
|
||||
|
||||
/** 更新 TurnToTarget 跨帧插值(在 Update 末尾调用) */
|
||||
void UpdateTurn(float DeltaTime);
|
||||
};
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "Interaction/InteractableComponent.h"
|
||||
#include "Interaction/InteractableActor.h"
|
||||
#include "Interaction/PersistenceSubsystem.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Components/SphereComponent.h"
|
||||
@@ -10,10 +12,6 @@
|
||||
#include "GameFramework/GameInstance.h"
|
||||
#include "GameFramework/Character.h"
|
||||
|
||||
// 前向声明对应的头文件在.cpp中include
|
||||
// ASystemDriver 在 Phase 2 创建,此处前向声明即可
|
||||
class ASystemDriver;
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
@@ -164,9 +162,15 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"),
|
||||
*Data.DisplayName, *OtherActor->GetName());
|
||||
|
||||
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().RegisterInteractable(this)
|
||||
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
|
||||
// TODO(Phase 2): 替换为 ASystemDriver 路径
|
||||
// 通过 ASystemDriver 注册到 InteractionManager
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
Driver->GetInteractionManager().RegisterInteractable(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractableComponent: ASystemDriver 不存在,无法注册 %s"), *Data.DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
|
||||
@@ -182,7 +186,9 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"),
|
||||
*Data.DisplayName, *OtherActor->GetName());
|
||||
|
||||
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this)
|
||||
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
|
||||
// TODO(Phase 2): 替换为 ASystemDriver 路径
|
||||
// 通过 ASystemDriver 从 InteractionManager 注销
|
||||
if (ASystemDriver* Driver = ASystemDriver::Get(this))
|
||||
{
|
||||
Driver->GetInteractionManager().UnregisterInteractable(this);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,225 +1,135 @@
|
||||
// ChildHood Project - 交互管理器核心组件
|
||||
// ChildHood Project - 交互管理器(v1.1 Effect/Action 架构重写)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(行为规则实现)
|
||||
// 职责:交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制
|
||||
// 职责:检测列表维护、选中管理、顶层状态(Free/Executing)
|
||||
// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有
|
||||
// 特点:纯事件驱动无 Tick,所有变化由外部调用触发
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractionTypes.h"
|
||||
#include "InteractionManager.generated.h"
|
||||
|
||||
class UInteractableComponent;
|
||||
class APushCameraActor;
|
||||
class AChildHoodCharacter;
|
||||
class UInteractionPromptWidget;
|
||||
class UNarrativeTextWidget;
|
||||
class ACharacter;
|
||||
|
||||
/**
|
||||
* UInteractionManager
|
||||
* FInteractionManager
|
||||
*
|
||||
* 挂载在 AChildHoodCharacter 上的组件,负责交互全流程管理:
|
||||
* - 层级状态机驱动(Free / Interacting + 子阶段)
|
||||
* - 附近交互物检测列表排序与选中管理
|
||||
* - 四种交互类型分发(观察/拾取/操作/对话)
|
||||
* - 角色自动转向插值
|
||||
* - 摄像机推镜/回位控制
|
||||
* - UI 提示与文本显示控制(v0.1 先用日志)
|
||||
* 普通C++类,负责交互检测列表维护和顶层状态管理。
|
||||
* 由 ASystemDriver 值持有,不参与 UE 反射系统。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractableComponent 碰撞回调 → ASystemDriver → RegisterInteractable/UnregisterInteractable
|
||||
* Character 输入回调 → ASystemDriver → BeginInteract/CancelInteract/SwitchTarget
|
||||
* EffectManager 完成回调 → ASystemDriver → OnInteractionFinished
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UInteractionManager : public UActorComponent
|
||||
class FInteractionManager
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UInteractionManager();
|
||||
/** 初始化管理器,绑定玩家角色引用 */
|
||||
void Initialize(ACharacter* PlayerCharacter);
|
||||
|
||||
// === 公开方法 ===
|
||||
// === 交互物注册(由 InteractableComponent 碰撞回调通过 ASystemDriver 调用)===
|
||||
|
||||
/** 发起交互(E键触发),前置检查通过后进入 Enter 阶段 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void BeginInteract();
|
||||
|
||||
/** 取消/结束交互(ESC/E键在Active阶段触发),AnimLock期间不可取消 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void CancelInteract();
|
||||
|
||||
/** 切换选中交互物(Tab/滚轮),Direction: +1下一个 / -1上一个 */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
void SwitchTarget(int32 Direction);
|
||||
|
||||
/** 注册交互物到检测列表(由 InteractableComponent 的碰撞回调触发) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
/**
|
||||
* 注册交互物到检测列表。
|
||||
* 添加后按水平距离排序,重置选中索引为0,刷新缓存。
|
||||
*/
|
||||
void RegisterInteractable(UInteractableComponent* Interactable);
|
||||
|
||||
/** 从检测列表注销交互物(离开范围或消耗时触发) */
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||
/**
|
||||
* 从检测列表注销交互物。
|
||||
* Executing 状态下如果移除的是 CurrentInteractable,仅从列表移除,
|
||||
* CurrentInteractable 延迟清理(因为 Effect 可能仍在执行)。
|
||||
*/
|
||||
void UnregisterInteractable(UInteractableComponent* Interactable);
|
||||
|
||||
// === 委托 ===
|
||||
// === 交互触发(Character 输入回调中调用)===
|
||||
|
||||
/** 物品拾取事件 — 通知物品系统(Pickup 类型 ExecuteAnimEvent 中广播) */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
|
||||
FMLocalItemEvent OnItemPickedUp;
|
||||
/**
|
||||
* 发起交互(E键触发)。
|
||||
* 前置检查:State==Free、SelectedIndex有效、!IsConsumed()。
|
||||
* 成功后 State=Executing,返回选中的 Component。
|
||||
* 拒绝则返回 nullptr。
|
||||
*/
|
||||
UInteractableComponent* BeginInteract();
|
||||
|
||||
/** 物品获得事件 — 通知收集系统(Search 类型 OnAnimLockComplete 中广播) */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
|
||||
FMLocalItemEvent OnItemFound;
|
||||
/**
|
||||
* 取消交互(ESC/E键在Executing阶段触发)。
|
||||
* v0.1 简化:仅设置状态标记,实际取消由 ASystemDriver 协调 EffectManager。
|
||||
*/
|
||||
void CancelInteract();
|
||||
|
||||
// === 可调参数 ===
|
||||
/**
|
||||
* 切换选中目标(Tab/滚轮)。
|
||||
* Direction: +1下一个 / -1上一个,循环。
|
||||
* 仅在 Free 状态下响应。
|
||||
*/
|
||||
void SwitchTarget(int32 Direction);
|
||||
|
||||
/** 角色转向时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float TurnDuration = 0.2f;
|
||||
// === 状态查询 ===
|
||||
|
||||
/** 提示 UI 淡入时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float PromptFadeInDuration = 0.2f;
|
||||
/** 获取当前顶层状态 */
|
||||
EInteractionState GetState() const { return State; }
|
||||
|
||||
/** 提示 UI 淡出时长(秒) */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
|
||||
float PromptFadeOutDuration = 0.2f;
|
||||
/** 当前是否有有效的选中交互物 */
|
||||
bool HasSelection() const { return CachedSelected.bHasSelection; }
|
||||
|
||||
protected:
|
||||
/** 获取选中交互物的提示文本(从缓存读取,不访问 Component) */
|
||||
FString GetSelectedPromptText() const { return CachedSelected.PromptText; }
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
/** 获取选中交互物的世界坐标(从缓存读取) */
|
||||
FVector GetSelectedWorldPosition() const { return CachedSelected.WorldPosition; }
|
||||
|
||||
/** 获取完整的选中信息缓存 */
|
||||
const FSelectedInfo& GetCachedSelectedInfo() const { return CachedSelected; }
|
||||
|
||||
// === Effect 完成回调(由 EffectManager 通过 ASystemDriver 调用)===
|
||||
|
||||
/**
|
||||
* Effect 执行完毕回调。
|
||||
* State 回到 Free,清理 CurrentInteractable,通知 Character 解除交互,
|
||||
* 重新排序和刷新缓存(因为消耗可能改变了列表)。
|
||||
*/
|
||||
void OnInteractionFinished();
|
||||
|
||||
// === 当前交互物访问(EffectManager 需要访问)===
|
||||
|
||||
/** 获取当前正在交互的交互物(Executing 状态下有效) */
|
||||
UInteractableComponent* GetCurrentInteractable() const;
|
||||
|
||||
private:
|
||||
|
||||
// === 状态机 ===
|
||||
|
||||
/** 交互系统顶层状态 */
|
||||
EInteractionState State = EInteractionState::Free;
|
||||
|
||||
/** Interacting 子阶段 */
|
||||
EInteractPhase Phase = EInteractPhase::None;
|
||||
|
||||
// === 检测与选择 ===
|
||||
|
||||
/** 当前附近的交互物列表,按水平距离排序 */
|
||||
UPROPERTY()
|
||||
TArray<UInteractableComponent*> NearbyInteractables;
|
||||
TArray<TWeakObjectPtr<UInteractableComponent>> NearbyInteractables;
|
||||
|
||||
/** 当前选中的交互物索引 */
|
||||
/** 当前选中的交互物索引(NearbyInteractables 中的下标) */
|
||||
int32 SelectedIndex = 0;
|
||||
|
||||
/** 当前正在交互的交互物(交互过程中锁定) */
|
||||
UPROPERTY()
|
||||
UInteractableComponent* CurrentInteractable = nullptr;
|
||||
/** 当前正在交互的交互物(Executing 状态下锁定) */
|
||||
TWeakObjectPtr<UInteractableComponent> CurrentInteractable;
|
||||
|
||||
// === 摄像机控制 ===
|
||||
/** 玩家角色弱引用(用于排序距离计算和 SetInteracting 调用) */
|
||||
TWeakObjectPtr<ACharacter> PlayerCharacter;
|
||||
|
||||
/** 推镜用临时摄像机 Actor(BeginPlay 中 Spawn) */
|
||||
UPROPERTY()
|
||||
TObjectPtr<APushCameraActor> PushCameraActor;
|
||||
|
||||
/** 推镜是否已发起(Enter阶段一次性标志) */
|
||||
bool bPushStarted = false;
|
||||
|
||||
// === 计时器 ===
|
||||
|
||||
/** 转向已用时间 */
|
||||
float TurnElapsed = 0.0f;
|
||||
|
||||
/** 推镜后等待已用时间 */
|
||||
float WaitElapsed = 0.0f;
|
||||
|
||||
/** 动画事件计时器(如60%时物品消失) */
|
||||
FTimerHandle AnimEventTimerHandle;
|
||||
|
||||
/** 动画结束计时器 */
|
||||
FTimerHandle AnimEndTimerHandle;
|
||||
|
||||
/** 退出阶段回位计时器 */
|
||||
FTimerHandle ExitTimerHandle;
|
||||
|
||||
/** 动画事件是否已触发 */
|
||||
bool bAnimEventFired = false;
|
||||
|
||||
// === UI 配置 ===
|
||||
|
||||
/** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
|
||||
|
||||
/** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
|
||||
|
||||
/** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
float PromptHeightOffset = 80.0f;
|
||||
|
||||
// === UI 实例 ===
|
||||
|
||||
/** 交互提示 Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionPromptWidget> PromptWidget;
|
||||
|
||||
/** 文本底框 Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UNarrativeTextWidget> NarrativeWidget;
|
||||
|
||||
// === 文本选择 ===
|
||||
|
||||
/** 上次显示的叙事文本(用于不重复选取) */
|
||||
FString LastNarrativeText;
|
||||
/** Presentation 层缓存数据 — 选中交互物的摘要信息 */
|
||||
FSelectedInfo CachedSelected;
|
||||
|
||||
// === 内部方法 ===
|
||||
|
||||
/** 更新选中交互物(排序后 SelectedIndex = 0) */
|
||||
/**
|
||||
* 更新选中交互物:按与玩家的水平距离排序,重置 SelectedIndex=0。
|
||||
* 同时清理已失效的弱引用。
|
||||
*/
|
||||
void UpdateSelection();
|
||||
|
||||
/** 更新提示 UI 内容和位置 */
|
||||
void UpdatePromptUI();
|
||||
|
||||
/** 隐藏提示 UI */
|
||||
void HidePrompt();
|
||||
|
||||
/** 更新提示 UI 的屏幕空间位置(跟随交互物世界位置投影) */
|
||||
void UpdatePromptScreenPosition(UInteractableComponent* Interactable);
|
||||
|
||||
/** Enter 阶段 Tick:转向插值 + 推镜 + 等待 */
|
||||
void TickEnterPhase(float DeltaTime);
|
||||
|
||||
/** AnimLock 阶段 Tick(v0.1 由计时器驱动,此处可为空) */
|
||||
void TickAnimLockPhase(float DeltaTime);
|
||||
|
||||
/** Exit 阶段 Tick(v0.1 由定时器驱动回 Free) */
|
||||
void TickExitPhase(float DeltaTime);
|
||||
|
||||
/** Enter 阶段完成后,根据交互类型分发到下一阶段 */
|
||||
void TransitionFromEnter();
|
||||
|
||||
/** 进入 AnimLock 阶段:播放蒙太奇或启动计时器 */
|
||||
void EnterAnimLockPhase();
|
||||
|
||||
/** 动画事件触发点回调(如拾取物消失、门状态切换) */
|
||||
void ExecuteAnimEvent();
|
||||
|
||||
/** 动画结束回调 */
|
||||
void OnAnimLockComplete();
|
||||
|
||||
/** 显示叙事文本(v0.1 用日志输出) */
|
||||
void ShowNarrativeText(const FString& Text);
|
||||
|
||||
/** 隐藏叙事文本(v0.1 用日志输出) */
|
||||
void HideNarrativeText();
|
||||
|
||||
/** 推镜:移动 PushCameraActor 并切换 ViewTarget */
|
||||
void PushCamera(FVector Target, float Duration);
|
||||
|
||||
/** 相机回位:切换回 Character 的 FollowCamera */
|
||||
void ReturnCamera(float Duration);
|
||||
|
||||
/** 计算推镜目标位置 */
|
||||
FVector CalculatePushPosition(UInteractableComponent* Interactable);
|
||||
|
||||
/** 随机选取叙事文本,排除上次 */
|
||||
FString PickRandomNarrative(const TArray<FString>& Texts);
|
||||
|
||||
/** Exit 阶段定时器回调:回到 Free 状态 */
|
||||
void OnExitTimerComplete();
|
||||
/**
|
||||
* 从 Component 读取选中项信息并写入 CachedSelected。
|
||||
* Presentation 层每帧只读 CachedSelected,不直接访问 Component。
|
||||
*/
|
||||
void RefreshCachedSelection();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// ChildHood Project - 交互提示追踪组件 实现
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
|
||||
// 职责:Free 状态下追踪 Manager 缓存,更新 Prompt Widget 位置和文本
|
||||
|
||||
#include "Interaction/InteractionPromptComponent.h"
|
||||
#include "Interaction/InteractionPromptWidget.h"
|
||||
#include "SystemDriver.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "Blueprint/WidgetLayoutLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
UInteractionPromptComponent::UInteractionPromptComponent()
|
||||
{
|
||||
// 启用 Tick,每帧更新 Prompt 位置
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 初始化
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::Initialize(ASystemDriver* InDriver)
|
||||
{
|
||||
Driver = InDriver;
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: 初始化完成, Driver=%s"),
|
||||
InDriver ? *InDriver->GetName() : TEXT("nullptr"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// 根据 PromptWidgetClass 创建 Prompt Widget
|
||||
if (PromptWidgetClass)
|
||||
{
|
||||
if (APlayerController* PC = GetWorld()
|
||||
? GetWorld()->GetFirstPlayerController()
|
||||
: nullptr)
|
||||
{
|
||||
PromptWidget = CreateWidget<UInteractionPromptWidget>(PC, PromptWidgetClass);
|
||||
if (PromptWidget)
|
||||
{
|
||||
PromptWidget->AddToViewport();
|
||||
PromptWidget->SetVisibility(ESlateVisibility::Hidden);
|
||||
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: Prompt Widget 创建完成"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: 无法获取 PlayerController,Prompt Widget 未创建"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: PromptWidgetClass 未设置,Prompt Widget 未创建"));
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionPromptComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// 没有 Widget 则跳过
|
||||
if (!PromptWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 Driver(弱引用检查)
|
||||
ASystemDriver* DriverPtr = Driver.Get();
|
||||
if (!DriverPtr)
|
||||
{
|
||||
HidePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 InteractionManager 引用
|
||||
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
|
||||
|
||||
// 检查 State == Free 且 HasSelection()
|
||||
if (Manager.GetState() == EInteractionState::Free && Manager.HasSelection())
|
||||
{
|
||||
UpdatePrompt();
|
||||
}
|
||||
else
|
||||
{
|
||||
HidePrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Prompt 显示/隐藏
|
||||
// ============================================================================
|
||||
|
||||
void UInteractionPromptComponent::UpdatePrompt()
|
||||
{
|
||||
ASystemDriver* DriverPtr = Driver.Get();
|
||||
if (!DriverPtr || !PromptWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
|
||||
|
||||
// 从 Manager 缓存读取数据
|
||||
FString PromptText = Manager.GetSelectedPromptText();
|
||||
FVector WorldPos = Manager.GetSelectedWorldPosition();
|
||||
|
||||
// 应用高度偏移
|
||||
FVector AdjustedPos = WorldPos + FVector(0.f, 0.f, PromptHeightOffset);
|
||||
|
||||
// ProjectWorldToScreen
|
||||
APlayerController* PC = GetWorld()
|
||||
? GetWorld()->GetFirstPlayerController()
|
||||
: nullptr;
|
||||
if (!PC)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FVector2D ScreenPos;
|
||||
bool bOnScreen = PC->ProjectWorldLocationToScreen(AdjustedPos, ScreenPos, true);
|
||||
|
||||
if (!bOnScreen)
|
||||
{
|
||||
HidePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 Widget 位置和文本
|
||||
PromptWidget->SetPromptText(PromptText);
|
||||
PromptWidget->SetScreenPosition(ScreenPos);
|
||||
PromptWidget->Show();
|
||||
|
||||
bWasShowingPrompt = true;
|
||||
}
|
||||
|
||||
void UInteractionPromptComponent::HidePrompt()
|
||||
{
|
||||
if (PromptWidget && bWasShowingPrompt)
|
||||
{
|
||||
PromptWidget->Hide();
|
||||
bWasShowingPrompt = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// ChildHood Project - 交互提示追踪组件
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
|
||||
// 职责:独立于 Effect 执行,只负责 Free 状态下的 Prompt UI 追踪
|
||||
// 架构:UActorComponent,自有 Tick,只读 InteractionManager 缓存
|
||||
// 挂载:Character 上,由 Character::BeginPlay 中调用 Initialize
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "InteractionPromptComponent.generated.h"
|
||||
|
||||
class ASystemDriver;
|
||||
class UInteractionPromptWidget;
|
||||
|
||||
/**
|
||||
* UInteractionPromptComponent
|
||||
*
|
||||
* 交互提示追踪组件。每帧检查 InteractionManager 的缓存数据,
|
||||
* 在 Free 状态且有选中交互物时显示 Prompt Widget,Executing 时隐藏。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractionManager 缓存 (CachedSelected) → 此组件 Tick 只读 → Prompt Widget 显示/隐藏
|
||||
*
|
||||
* 不直接访问任何 InteractableComponent,所有数据来自 Manager 缓存。
|
||||
*/
|
||||
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
|
||||
class UInteractionPromptComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UInteractionPromptComponent();
|
||||
|
||||
/** 初始化(Character::BeginPlay 中调用) */
|
||||
void Initialize(ASystemDriver* InDriver);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
private:
|
||||
|
||||
/** 弱引用 Driver */
|
||||
TWeakObjectPtr<ASystemDriver> Driver;
|
||||
|
||||
/** Prompt Widget 实例 */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInteractionPromptWidget> PromptWidget;
|
||||
|
||||
/** Prompt Widget 蓝图类(编辑器中指定 WBP_InteractionPrompt) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
|
||||
|
||||
/** 提示 UI 高度偏移(cm) */
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||
float PromptHeightOffset = 80.0f;
|
||||
|
||||
/** 上一次记录的状态(用于检测状态变化) */
|
||||
bool bWasShowingPrompt = false;
|
||||
|
||||
/** 更新 Prompt 显示 */
|
||||
void UpdatePrompt();
|
||||
|
||||
/** 隐藏 Prompt */
|
||||
void HidePrompt();
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
// ChildHood Project - 系统驱动器 实现(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:全局唯一的 World 级驱动器,持有所有 Manager,负责初始化、Tick、跨 Manager 协调
|
||||
|
||||
#include "SystemDriver.h"
|
||||
#include "ChildHood.h"
|
||||
|
||||
// FEffectManager 尚未创建时,此 include 会失败。
|
||||
// 临时处理:条件编译,EffectManager.h 存在时才包含。
|
||||
// 当 EffectManager 子代理完成后,去掉 __has_include 判断直接包含。
|
||||
#if __has_include("Interaction/EffectManager.h")
|
||||
#include "Interaction/EffectManager.h"
|
||||
#define HAS_EFFECT_MANAGER 1
|
||||
#else
|
||||
#define HAS_EFFECT_MANAGER 0
|
||||
#endif
|
||||
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "GameFramework/Character.h"
|
||||
|
||||
// ============================================================================
|
||||
// 构造函数
|
||||
// ============================================================================
|
||||
|
||||
ASystemDriver::ASystemDriver()
|
||||
{
|
||||
// AInfo 默认不需要 Tick,我们在构造函数中开启
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.bStartWithTickEnabled = true;
|
||||
|
||||
// 如果 FEffectManager 可用,在构造时创建实例
|
||||
#if HAS_EFFECT_MANAGER
|
||||
EffectManagerPtr = MakeUnique<FEffectManager>();
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
void ASystemDriver::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// --- 初始化 InteractionManager ---
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (APlayerController* PC = World->GetFirstPlayerController())
|
||||
{
|
||||
if (ACharacter* Char = PC->GetCharacter())
|
||||
{
|
||||
InteractionManager.Initialize(Char);
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: InteractionManager 初始化完成"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: PlayerController 无 Character,InteractionManager 未初始化"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: World 中无 PlayerController"));
|
||||
}
|
||||
}
|
||||
|
||||
// --- 初始化 EffectManager ---
|
||||
#if HAS_EFFECT_MANAGER
|
||||
if (EffectManagerPtr.IsValid())
|
||||
{
|
||||
// 绑定 Effect 完成回调
|
||||
FOnEffectComplete Callback;
|
||||
Callback.BindLambda([this]() { this->OnEffectComplete(); });
|
||||
EffectManagerPtr->SetOnEffectComplete(MoveTemp(Callback));
|
||||
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 初始化完成"));
|
||||
}
|
||||
#else
|
||||
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 尚未创建(EffectManager.h 不存在)"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ASystemDriver::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
// --- Tick EffectManager ---
|
||||
// InteractionManager 无 Tick(纯事件驱动)
|
||||
#if HAS_EFFECT_MANAGER
|
||||
if (EffectManagerPtr.IsValid())
|
||||
{
|
||||
EffectManagerPtr->Update(DeltaTime);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 全局访问
|
||||
// ============================================================================
|
||||
|
||||
ASystemDriver* ASystemDriver::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 使用 TActorIterator 查找场景中的 ASystemDriver
|
||||
// 全局唯一,取第一个即可
|
||||
for (TActorIterator<ASystemDriver> It(World); It; ++It)
|
||||
{
|
||||
return *It;
|
||||
}
|
||||
|
||||
// 场景中不存在时返回 nullptr
|
||||
// 应在关卡中放置一个 ASystemDriver,或在 GameMode::BeginPlay 中 Spawn
|
||||
UE_LOG(LogChildHood, Verbose, TEXT("SystemDriver::Get: 场景中未找到 ASystemDriver"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Manager 访问
|
||||
// ============================================================================
|
||||
|
||||
FEffectManager& ASystemDriver::GetEffectManager()
|
||||
{
|
||||
checkf(EffectManagerPtr.IsValid(),
|
||||
TEXT("ASystemDriver::GetEffectManager: EffectManager 未初始化。")
|
||||
TEXT("请确保 EffectManager.h 已创建且 HAS_EFFECT_MANAGER == 1"));
|
||||
return *EffectManagerPtr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Effect 完成回调
|
||||
// ============================================================================
|
||||
|
||||
void ASystemDriver::OnEffectComplete()
|
||||
{
|
||||
// Effect 完成后通知 InteractionManager 恢复 Free 状态
|
||||
InteractionManager.OnInteractionFinished();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// ChildHood Project - 系统驱动器(v1.1 Effect/Action 架构)
|
||||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
|
||||
// 职责:全局唯一的 World 级驱动器,持有所有 Manager(FInteractionManager, FEffectManager),
|
||||
// 负责初始化、Tick 驱动、跨 Manager 协调。
|
||||
// 架构:AInfo 子类(不渲染、不占场景空间),每关只有一个实例。
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Info.h"
|
||||
#include "Interaction/InteractionManager.h"
|
||||
#include "SystemDriver.generated.h"
|
||||
|
||||
// FEffectManager 由另一个子代理并行创建,此处只前向声明
|
||||
class FEffectManager;
|
||||
|
||||
/**
|
||||
* ASystemDriver
|
||||
*
|
||||
* 全局唯一的 World 级系统驱动器。持有 FInteractionManager 和 FEffectManager,
|
||||
* 负责它们的初始化、Tick 驱动和跨 Manager 协调。
|
||||
*
|
||||
* 访问方式:ASystemDriver::Get(WorldContextObject) 静态方法。
|
||||
*
|
||||
* 数据流:
|
||||
* InteractableComponent 碰撞回调 → Get() → GetInteractionManager().Register/Unregister
|
||||
* Character 输入回调 → Get() → GetInteractionManager().BeginInteract/Cancel/Switch
|
||||
* EffectManager 完成 → OnEffectComplete → GetInteractionManager().OnInteractionFinished
|
||||
*/
|
||||
UCLASS()
|
||||
class ASystemDriver : public AInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
ASystemDriver();
|
||||
|
||||
// === 生命周期 ===
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
|
||||
// === 全局访问 ===
|
||||
|
||||
/**
|
||||
* 获取当前 World 中的 ASystemDriver 实例。
|
||||
* WorldContextObject 用于获取 World 上下文(通常传 this 或 GetWorld())。
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "System", meta = (WorldContext = "WorldContextObject"))
|
||||
static ASystemDriver* Get(const UObject* WorldContextObject);
|
||||
|
||||
// === Manager 访问 ===
|
||||
|
||||
/** 获取交互管理器(值引用) */
|
||||
FInteractionManager& GetInteractionManager() { return InteractionManager; }
|
||||
const FInteractionManager& GetInteractionManager() const { return InteractionManager; }
|
||||
|
||||
/**
|
||||
* 获取效果管理器(值引用)。
|
||||
* FEffectManager 尚未创建时调用会触发链接错误,仅在 EffectManager.h 存在时使用。
|
||||
*/
|
||||
FEffectManager& GetEffectManager();
|
||||
|
||||
private:
|
||||
|
||||
/** 交互管理器 — 普通 C++ 值持有 */
|
||||
FInteractionManager InteractionManager;
|
||||
|
||||
/**
|
||||
* 效果管理器 — 普通 C++ 值持有。
|
||||
* FEffectManager 定义在 Interaction/EffectManager.h 中(由另一个子代理创建)。
|
||||
* 使用 unique_ptr 延迟构造,避免头文件中需要完整定义。
|
||||
*/
|
||||
TUniquePtr<FEffectManager> EffectManagerPtr;
|
||||
|
||||
// === Effect 完成回调连线 ===
|
||||
|
||||
/** EffectManager 的 OnEffectComplete 回调目标 */
|
||||
void OnEffectComplete();
|
||||
};
|
||||
Reference in New Issue
Block a user