docs(technical): 收集系统技术方案 v1.0:Effect/Action 解耦架构修订

This commit is contained in:
2026-06-11 23:08:27 +08:00
parent 8a75e682c5
commit 690cfeb29d
12 changed files with 1463 additions and 790 deletions
@@ -0,0 +1,64 @@
// ChildHood Project - 门交互组件
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:门的开关状态管理、动态提示文本、跨场景持久状态恢复
// 派生自 UInteractableComponent,增加门特有的状态和行为
#pragma once
#include "CoreMinimal.h"
#include "Interaction/InteractableComponent.h"
#include "DoorInteractableComponent.generated.h"
class UPersistenceSubsystem;
/**
* UDoorInteractableComponent
*
* 挂载在 ADoorActor 上的门交互组件。
* 在基类(消耗状态、触发检测、Effect 配置)基础上,增加:
* - 门开关状态 (bDoorIsOpen)
* - 切换门 (ToggleDoor)
* - 动态提示文本(根据开关状态选择 InteractVerb 或 InteractVerbAlt)
* - 持久状态恢复(门的开关状态)
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UDoorInteractableComponent : public UInteractableComponent
{
GENERATED_BODY()
public:
UDoorInteractableComponent();
// === 门操作 ===
/** 切换门的开关状态,并通过 GetOwner() 通知 ADoorActor 更新视觉 */
UFUNCTION(BlueprintCallable, Category = "Door")
void ToggleDoor();
/** 查询门当前是否打开 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Door")
bool IsDoorOpen() const { return bDoorIsOpen; }
// === 基类覆盖 ===
/**
* 根据门开关状态选择提示动词:
* 已打开 → InteractVerbAlt(如"关上")
* 已关闭 → InteractVerb(如"打开")
*/
virtual FString GetPromptText_Implementation() const override;
/**
* 恢复跨场景持久状态:
* 1. 调用 Super 恢复消耗状态
* 2. 从 PersistenceSubsystem 恢复门开关状态
* 3. 通知 DoorActor 更新视觉旋转
*/
virtual void RestorePersistentState(UPersistenceSubsystem* Persistence) override;
private:
/** 门开关状态(true=打开,false=关闭) */
bool bDoorIsOpen = false;
};
@@ -1,4 +1,4 @@
// ChildHood Project - 通用交互物 Actor 基类 实现
// ChildHood Project - 通用交互物 Actor 基类 实现(v1.1 Effect/Action 架构)
#include "Interaction/InteractableActor.h"
#include "Interaction/InteractableComponent.h"
@@ -28,17 +28,7 @@ AInteractableActor::AInteractableActor()
void AInteractableActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
{
if (!Comp || !MeshComp) return;
const FInteractableData& Data = Comp->GetInteractableData();
// 门类型:根据开关状态旋转 Mesh
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
FRotator CurrentRot = MeshComp->GetRelativeRotation();
float TargetYaw = Comp->IsDoorOpen() ? 90.0f : 0.0f;
MeshComp->SetRelativeRotation(FRotator(CurrentRot.Pitch, TargetYaw, CurrentRot.Roll));
}
// 默认空实现,子类可覆盖(如 ADoorActor 旋转 Mesh)
}
void AInteractableActor::OnInteractEnded_Implementation(UInteractableComponent* Comp)
@@ -1,6 +1,7 @@
// ChildHood Project - 通用交互物 Actor 基类
// ChildHood Project - 通用交互物 Actor 基类(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:持有 Mesh + InteractableComponent,提供可被子类覆盖的交互回调
// 核心变更(v0.1 → v1.1):去除门旋转逻辑,只保留干净的基类
#pragma once
@@ -9,6 +10,7 @@
#include "InteractableActor.generated.h"
class UInteractableComponent;
class UStaticMeshComponent;
/**
* AInteractableActor
@@ -21,7 +23,7 @@ class UInteractableComponent;
* - UInteractableComponent* InteractableComp(交互逻辑与数据)
*
* 子类可覆盖 OnInteractStarted / OnInteractEnded 以实现类型特定行为,
* 如门开关时的 Mesh 旋转、拾取物的消失效果等。
* 如门开关时的 Mesh 旋转(ADoorActor)、拾取物的消失效果等。
*/
UCLASS()
class AInteractableActor : public AActor
@@ -45,15 +47,20 @@ public:
// === 可被子类覆盖的交互回调 ===
/**
* 交互开始时调用(门开关切换、状态恢复时也调用)。
* 默认实现:门类型根据开关状态对 Mesh 做 Yaw 旋转(开门+90度/关门0度)。
* 交互开始时调用。
* BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。
* 默认为空实现。
*/
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Started"))
void OnInteractStarted(UInteractableComponent* Comp);
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp);
/** 交互结束时调用。默认为空实现,子类可覆盖。 */
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
/**
* 交互结束时调用。
* BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。
* 默认为空实现。
*/
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Ended"))
void OnInteractEnded(UInteractableComponent* Comp);
virtual void OnInteractEnded_Implementation(UInteractableComponent* Comp);
@@ -61,9 +68,9 @@ protected:
/** 交互物的可视模型 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* MeshComp;
TObjectPtr<UStaticMeshComponent> MeshComp;
/** 交互物的交互组件(数据 + 检测 + 状态) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UInteractableComponent* InteractableComp;
TObjectPtr<UInteractableComponent> InteractableComp;
};
@@ -1,15 +1,18 @@
// ChildHood Project - 可交互物组件 实现
// ChildHood Project - 可交互物组件 实现(v1.1 Effect/Action 架构)
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractableActor.h"
#include "Interaction/InteractionManager.h"
#include "ChildHoodGameInstance.h"
#include "Interaction/PersistenceSubsystem.h"
#include "ChildHood.h"
#include "Components/SphereComponent.h"
#include "Engine/World.h"
#include "GameFramework/GameInstance.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
// 前向声明对应的头文件在.cpp中include
// ASystemDriver 在 Phase 2 创建,此处前向声明即可
class ASystemDriver;
// ============================================================================
// 构造函数
@@ -36,7 +39,7 @@ void UInteractableComponent::BeginPlay()
return;
}
// --- 创建检测球 ---
// --- 创建检测球(BeginPlay 中动态创建,因为需要 Data.TriggerRadius)---
DetectionSphere = NewObject<USphereComponent>(this, USphereComponent::StaticClass(), TEXT("DetectionSphere"));
if (DetectionSphere)
{
@@ -58,14 +61,14 @@ void UInteractableComponent::BeginPlay()
}
// --- 恢复跨场景持久状态 ---
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
if (UPersistenceSubsystem* Persistence = GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
{
RestorePersistentState(GI);
RestorePersistentState(Persistence);
}
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (Type=%s, Radius=%f)"),
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (ItemId=%s, Radius=%.0f)"),
Data.DisplayName.IsEmpty() ? TEXT("(unnamed)") : *Data.DisplayName,
*UEnum::GetValueAsString(Data.Type),
*Data.ItemId.ToString(),
Data.TriggerRadius);
}
@@ -73,22 +76,16 @@ void UInteractableComponent::BeginPlay()
// 提示文本
// ============================================================================
FString UInteractableComponent::GetPromptText() const
FString UInteractableComponent::GetPromptText_Implementation() const
{
FString Verb = Data.InteractVerb;
// 门类型:根据当前开关状态选择动词
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
}
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
return FString::Printf(TEXT("[E] %s %s"), *Data.InteractVerb, *Data.DisplayName);
}
// ============================================================================
// 消耗管理
// ============================================================================
void UInteractableComponent::Consume()
void UInteractableComponent::Consume_Implementation()
{
bIsConsumed = true;
@@ -101,27 +98,15 @@ void UInteractableComponent::Consume()
// 写入持久层
if (UWorld* World = GetWorld())
{
if (UChildHoodGameInstance* GI = World->GetGameInstance<UChildHoodGameInstance>())
if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
{
GI->MarkItemCollected(Data.ItemId);
Persistence->MarkItemCollected(Data.ItemId);
}
}
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 已消耗: %s"), *Data.ItemId.ToString());
}
// ============================================================================
// 门状态管理
// ============================================================================
void UInteractableComponent::ToggleDoor()
{
bDoorIsOpen = !bDoorIsOpen;
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 门状态切换: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("开") : TEXT("关"));
}
// ============================================================================
// 位置查询
// ============================================================================
@@ -136,38 +121,24 @@ FVector UInteractableComponent::GetWorldLocation() const
// 跨场景持久状态恢复
// ============================================================================
void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI)
void UInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence)
{
if (!GI) return;
if (!Persistence) return;
// oneShot 物品:已消耗 → 隐藏 + 禁用检测
if (Data.bOneShot && GI->IsItemCollected(Data.ItemId))
if (Data.bOneShot && Persistence->IsItemCollected(Data.ItemId))
{
bIsConsumed = true;
if (DetectionSphere)
{
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
if (AActor* Owner = GetOwner())
{
Owner->SetActorHiddenInGame(true);
}
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 已消耗物品隐藏: %s"), *Data.ItemId.ToString());
}
// 门:恢复开关状态
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
// 通知 Actor 应用视觉状态(Mesh 旋转)
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
{
Owner->OnInteractStarted(this);
}
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 门状态恢复: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("开") : TEXT("关"));
}
}
// ============================================================================
@@ -193,11 +164,9 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla
UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// 获取角色的 InteractionManager 并注册自己
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
{
IM->RegisterInteractable(this);
}
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().RegisterInteractable(this)
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
// TODO(Phase 2): 替换为 ASystemDriver 路径
}
void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
@@ -213,9 +182,7 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp
UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// 获取角色的 InteractionManager 并注销自己
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
{
IM->UnregisterInteractable(this);
}
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this)
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
// TODO(Phase 2): 替换为 ASystemDriver 路径
}
@@ -1,6 +1,7 @@
// ChildHood Project - 可交互物组件
// ChildHood Project - 可交互物组件(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第12节(跨场景持久化)
// 职责:单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复
// 核心变更(v0.1 → v1.1):去除所有门逻辑,只保留通用交互物功能
#pragma once
@@ -9,15 +10,21 @@
#include "InteractionTypes.h"
#include "InteractableComponent.generated.h"
class UChildHoodGameInstance;
class UPersistenceSubsystem;
class USphereComponent;
class ASystemDriver;
/**
* UInteractableComponent
*
* 挂载在 AInteractableActor(或其子类)上的组件。
* 持有该交互物的全部配置数据(FInteractableData)、运行时触发检测球、
* 消耗/门状态管理,以及跨场景持久状态恢复。
* 消耗状态管理,以及跨场景持久状态恢复。
*
* 碰撞回调通过 ASystemDriver::Get() 获取 Driver,
* 再调用 GetInteractionManager().RegisterInteractable(this) 注册。
*
* 门逻辑已移除,由 UDoorInteractableComponent 派生类实现(Phase 2)。
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UInteractableComponent : public UActorComponent
@@ -41,29 +48,31 @@ public:
/**
* 获取提示文本,格式:"[E] 动作词 名称"
* 门类型根据当前开关状态选择动词
* 子类可覆盖以实现动态动词(如门的"打开"/"关上")
*/
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Get Prompt Text"))
FString GetPromptText() const;
virtual FString GetPromptText_Implementation() const;
/** 该交互物是否已被消耗(oneShot 模式) */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact")
bool IsConsumed() const { return bIsConsumed; }
/** 标记为已消耗,禁用检测球,写入持久层 */
/** 标记为已消耗,禁用检测球 */
UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Consume"))
void Consume();
// === 门状态管理 ===
/** 切换门开关状态 */
void ToggleDoor();
/** 门是否处于打开状态 */
bool IsDoorOpen() const { return bDoorIsOpen; }
virtual void Consume_Implementation();
/** 获取交互物在世界空间的位置 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact")
FVector GetWorldLocation() const;
/** 从 GameInstance 恢复跨场景持久状态(BeginPlay 中调用) */
void RestorePersistentState(UChildHoodGameInstance* GI);
/**
* 从持久层恢复跨场景持久状态(BeginPlay 中调用)。
* oneShot 物品已消耗时隐藏 Actor 并禁用检测。
* 子类可覆盖以恢复额外状态(如门的开关状态)。
*/
virtual void RestorePersistentState(UPersistenceSubsystem* Persistence);
protected:
@@ -73,17 +82,14 @@ private:
/** 运行时创建的检测球,用于触发玩家进入/离开交互范围 */
UPROPERTY()
USphereComponent* DetectionSphere;
TObjectPtr<USphereComponent> DetectionSphere;
/** oneShot 物品是否已被消耗 */
bool bIsConsumed = false;
/** 门是否处于打开状态(仅 Operate+Door 类型使用) */
bool bDoorIsOpen = false;
// === 碰撞回调 ===
/** 玩家进入检测范围 */
/** 玩家进入检测范围 — 通过 ASystemDriver 注册到 InteractionManager */
UFUNCTION()
void OnDetectionOverlapBegin(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
@@ -92,7 +98,7 @@ private:
bool bFromSweep,
const FHitResult& SweepResult);
/** 玩家离开检测范围 */
/** 玩家离开检测范围 — 通过 ASystemDriver 从 InteractionManager 注销 */
UFUNCTION()
void OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
+243 -120
View File
@@ -1,5 +1,5 @@
// ChildHood Project - 交互系统数据类型定义
// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)+ 第12节(跨场景持久化)
// ChildHood Project - 交互系统数据类型定义(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)
#pragma once
@@ -10,169 +10,292 @@
// 枚举
// ============================================================================
/** 交互类型 */
UENUM(BlueprintType)
enum class EInteractType : uint8
{
Examine UMETA(DisplayName = "观察"),
Pickup UMETA(DisplayName = "拾取"),
Operate UMETA(DisplayName = "操作"),
Talk UMETA(DisplayName = "对话")
};
/** 操作子类型(仅 Operate 类型有效) */
UENUM(BlueprintType)
enum class EOperateSubType : uint8
{
Door UMETA(DisplayName = "门(开关切换)"),
Search UMETA(DisplayName = "翻找")
};
/** 交互系统顶层状态 */
/** 交互系统顶层状态(简化为 2 阶段) */
UENUM(BlueprintType)
enum class EInteractionState : uint8
{
Free UMETA(DisplayName = "自由状态"),
Interacting UMETA(DisplayName = "交互中"),
SubSystem UMETA(DisplayName = "子系统(v0.1预留)"),
Cutscene UMETA(DisplayName = "过场(v0.1预留)")
Free, // 自由状态,检测/选择/Prompt
Executing, // 执行 Effect 中
};
/** Interacting 子阶段 */
/** Action 类型 */
UENUM(BlueprintType)
enum class EInteractPhase : uint8
enum class EInteractActionType : uint8
{
None UMETA(DisplayName = "无"),
Enter UMETA(DisplayName = "进入(转向+推镜)"),
AnimLock UMETA(DisplayName = "动画锁定"),
Active UMETA(DisplayName = "活跃(等待输入)"),
Exit UMETA(DisplayName = "退出(回位)")
};
/** 交互结束后动作 */
UENUM(BlueprintType)
enum class EPostAction : uint8
{
Return UMETA(DisplayName = "回到Free"),
Minigame UMETA(DisplayName = "小游戏(v0.1预留)"),
Dialogue UMETA(DisplayName = "对话(v0.1预留)"),
SceneChange UMETA(DisplayName = "场景切换(v0.1预留)")
Wait UMETA(DisplayName = "等待"),
TurnToTarget UMETA(DisplayName = "转向交互物"),
PlayMontage UMETA(DisplayName = "播放人物动画"),
PlayMontageOnTarget UMETA(DisplayName = "播放交互物动画"),
CameraPush UMETA(DisplayName = "相机推镜"),
CameraReturn UMETA(DisplayName = "相机回位"),
Consume UMETA(DisplayName = "消耗交互物"),
HideActor UMETA(DisplayName = "隐藏交互物"),
ShowActor UMETA(DisplayName = "显示交互物"),
AddToBackpack UMETA(DisplayName = "物品进背包"),
ShowNarrative UMETA(DisplayName = "显示叙事文本"),
HideNarrative UMETA(DisplayName = "隐藏叙事文本"),
ShowPickupNotification UMETA(DisplayName = "显示拾取通知"),
ToggleDoor UMETA(DisplayName = "切换门状态"),
PlayDialogue UMETA(DisplayName = "播放对话"),
PlayStory UMETA(DisplayName = "播放小剧情"),
BroadcastEvent UMETA(DisplayName = "广播逻辑事件"),
};
// ============================================================================
// 配置数据结构
// 前向声明
// ============================================================================
/** 可交互物配置数据 — 每个交互物实例独立配置 */
class UInteractableComponent;
// ============================================================================
// Action — 原子操作定义
// ============================================================================
/** 原子操作定义 — Effect 内按 TriggerTime 编排的最小执行单元 */
USTRUCT(BlueprintType)
struct FInteractAction
{
GENERATED_BODY()
// === 编排 ===
/** Action 类型 */
UPROPERTY(EditAnywhere, Category = "Action")
EInteractActionType Type = EInteractActionType::Wait;
/** 相对于 Effect 开始的触发时间(秒) */
UPROPERTY(EditAnywhere, Category = "Action", meta = (ClampMin = "0"))
float TriggerTime = 0.f;
// === 各类型参数(按 EditCondition 折叠显示) ===
// --- Wait ---
/** 等待时长(秒) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::Wait", EditConditionHides))
float WaitDuration = 0.5f;
// --- TurnToTarget ---
/** 转向时长(秒) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::TurnToTarget", EditConditionHides))
float TurnDuration = 0.2f;
// --- PlayMontage ---
/** 人物蒙太奇动画 */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontage", EditConditionHides))
TObjectPtr<UAnimMontage> Montage;
// --- PlayMontageOnTarget ---
/** 交互物蒙太奇动画 */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontageOnTarget", EditConditionHides))
TObjectPtr<UAnimMontage> TargetMontage;
// --- CameraPush ---
/** 推镜距离(cm) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides))
float PushDistance = 80.f;
/** 推镜过渡时长(秒) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides))
float PushDuration = 0.5f;
// --- CameraReturn ---
/** 相机回位时长(秒) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraReturn", EditConditionHides))
float ReturnDuration = 0.3f;
// --- ShowNarrative ---
/** 叙事文本列表 */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
TArray<FString> NarrativeTexts;
/** 是否等待用户按 E/ESC 后再继续后续 Action */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
bool bWaitForInput = false;
/** 是否从 NarrativeTexts 中随机选取(不重复上一次) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides))
bool bRandomSelect = true;
};
// ============================================================================
// Effect — 交互效果定义
// ============================================================================
/** Effect 定义 — 由多个 Action 按时间线编排而成的交互效果配方 */
USTRUCT(BlueprintType)
struct FInteractEffect
{
GENERATED_BODY()
/** Effect 名称(调试用) */
UPROPERTY(EditAnywhere, Category = "Effect")
FString EffectName;
/** Effect 总时长(秒),到时自动结束 */
UPROPERTY(EditAnywhere, Category = "Effect", meta = (ClampMin = "0.1", ClampMax = "10.0"))
float TotalDuration = 2.0f;
/** 是否可取消(ESC 中断) */
UPROPERTY(EditAnywhere, Category = "Effect")
bool bCancellable = true;
/** Action 列表 — 按 TriggerTime 编排,同时间并行 */
UPROPERTY(EditAnywhere, Category = "Effect")
TArray<FInteractAction> Actions;
};
// ============================================================================
// 可交互物配置数据
// ============================================================================
/** 可交互物配置数据 — 每个交互物实例独立配置,不再包含类型分发字段 */
USTRUCT(BlueprintType)
struct FInteractableData
{
GENERATED_BODY()
GENERATED_BODY()
// === 基本信息 ===
// === 基本信息 ===
/** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
FName ItemId;
/** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */
UPROPERTY(EditAnywhere, Category = "Basic")
FName ItemId;
/** 显示名称(如 "旧冰箱"),用于提示UI */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
FString DisplayName;
/** 显示名称(如 "旧冰箱"),用于提示UI */
UPROPERTY(EditAnywhere, Category = "Basic")
FString DisplayName;
/** 交互类型 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
EInteractType Type = EInteractType::Examine;
// === 触发 ===
/** 操作子类型,仅 Type==Operate 时可见可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic",
meta = (EditCondition = "Type == EInteractType::Operate", EditConditionHides))
EOperateSubType OperateSubType = EOperateSubType::Door;
/** 触发半径(cm),默认1.5m */
UPROPERTY(EditAnywhere, Category = "Trigger", meta = (ClampMin = "50.0", ClampMax = "500.0"))
float TriggerRadius = 150.f;
// === 触发 ===
/** 是否一次性消耗(消耗后不再可交互) */
UPROPERTY(EditAnywhere, Category = "Trigger")
bool bOneShot = false;
/** 触发半径(cm),默认1.5m */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger",
meta = (ClampMin = "50.0", ClampMax = "500.0"))
float TriggerRadius = 150.0f;
// === 提示文本 ===
/** 是否一次性消耗(消耗后不再可交互) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
bool bOneShot = false;
/** 动作词(如 "查看"、"拾取"、"打开") */
UPROPERTY(EditAnywhere, Category = "Prompt")
FString InteractVerb;
// === 提示文本 ===
/** 备选动词(如 "关上"),门打开时使用 */
UPROPERTY(EditAnywhere, Category = "Prompt")
FString InteractVerbAlt;
/** 动作词(如 "查看"、"拾取"、"打开") */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt")
FString InteractVerb;
// === 交互 Effect ===
/** 备选动词(如 "关上"),门打开时使用,仅 Operate+Door 时可见 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt",
meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door"))
FString InteractVerbAlt;
/** 交互效果定义 — 所有行为由 Effect 内的 Action 定义 */
UPROPERTY(EditAnywhere, Category = "Effect")
FInteractEffect InteractEffect;
// === 叙事/对话文本 ===
// 已删除(由 Effect 内 Action 替代):
// EInteractType Type, EOperateSubType OperateSubType
// bUseCameraPush, CameraPushDistance/Duration, PushWaitDelay, CameraReturnDuration
// InteractMontage, AnimDuration, AnimEventTimepoint
// NarrativeTexts, EPostAction PostAction
};
/** 文本列表,随机选取,不重复上次 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text")
TArray<FString> NarrativeTexts;
// ============================================================================
// 运行时上下文
// ============================================================================
// === 摄像机推镜 ===
/** Effect 运行时上下文 — EffectManager 执行时传入,持有弱引用 */
USTRUCT()
struct FEffectContext
{
GENERATED_BODY()
/** 是否使用摄像机推镜 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera")
bool bUseCameraPush = false;
/** 触发交互的角色 */
TWeakObjectPtr<ACharacter> Instigator;
/** 推镜距离(cm),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "10.0", ClampMax = "300.0"))
float CameraPushDistance = 80.0f;
/** 交互物组件(配置数据来源) */
TWeakObjectPtr<UInteractableComponent> TargetComp;
/** 推镜过渡时长(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
float CameraPushDuration = 0.5f;
/** 交互物 Actor(视觉操作目标) */
TWeakObjectPtr<AActor> TargetActor;
};
/** 相机回位时长(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
float CameraReturnDuration = 0.3f;
// ============================================================================
// Presentation 层缓存数据(非 UObject,纯 C++)
// ============================================================================
/** 推镜完成后到文本显示的等待时间(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.0", ClampMax = "1.0"))
float PushWaitDelay = 0.2f;
/** Presentation 层缓存数据 — Manager 在选择变化时从 Component 读一次并写入,
* Prompt 组件每帧只读此缓存,不直接访问 Component */
struct FSelectedInfo
{
/** 是否有有效选中 */
bool bHasSelection = false;
// === 动画 ===
/** 选中交互物的提示文本(如 "[E] 查看旧冰箱") */
FString PromptText;
/** 交互动画蒙太奇(可选,为空时用计时器模拟) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
TObjectPtr<UAnimMontage> InteractMontage;
/** 选中交互物的世界坐标(用于屏幕空间投影) */
FVector WorldPosition = FVector::ZeroVector;
/** 无蒙太奇时的动画持续时间(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
meta = (ClampMin = "0.1", ClampMax = "5.0"))
float AnimDuration = 1.0f;
/** 选中交互物是否已消耗(消耗后不再显示提示) */
bool bIsConsumed = false;
/** 动画事件触发点(0-1比例),如0.6表示动画播放至60%时触发物品消失 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AnimEventTimepoint = 0.6f;
void Reset()
{
bHasSelection = false;
PromptText.Empty();
WorldPosition = FVector::ZeroVector;
bIsConsumed = false;
}
};
// === 结束动作 ===
// ============================================================================
// TurnToTarget 跨帧插值状态(非 UObject,纯 C++)
// ============================================================================
/** 交互结束后的动作 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Post")
EPostAction PostAction = EPostAction::Return;
/** TurnToTarget 跨帧插值状态 — EffectManager 在 Update 中维护,
* 每帧 RInterpTo 驱动旋转,不能一帧完成 */
struct FTurnState
{
/** 是否正在转向 */
bool bIsActive = false;
/** 转向起始旋转 */
FRotator StartRotation = FRotator::ZeroRotator;
/** 转向目标旋转(面朝交互物方向) */
FRotator TargetRotation = FRotator::ZeroRotator;
/** 已用时间(秒) */
float ElapsedTime = 0.f;
/** 转向总时长(秒) */
float Duration = 0.2f;
/** 是否已完成 */
bool IsComplete() const { return ElapsedTime >= Duration; }
/** 获取当前插值 Alpha [0,1] */
float GetAlpha() const { return FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f); }
void Reset()
{
bIsActive = false;
StartRotation = FRotator::ZeroRotator;
TargetRotation = FRotator::ZeroRotator;
ElapsedTime = 0.f;
Duration = 0.2f;
}
};
// ============================================================================
// 委托
// ============================================================================
/** 物品拾取/获得事件 — 通知物品系统和收集系统 */
/** 物品消耗事件(逻辑事件,Persistence/Backpack 订阅) */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FMLocalItemEvent,
FName, ItemId,
const FString&, DisplayName,
const FName&, Category
FMLocalItemEvent,
FName, ItemId,
const FString&, DisplayName,
const FName&, Category
);
/** Effect 完成回调 */
DECLARE_DELEGATE(FOnEffectComplete);
@@ -0,0 +1,47 @@
// ChildHood Project - 跨场景持久化子系统 实现
#include "Interaction/PersistenceSubsystem.h"
#include "ChildHood.h"
// ============================================================================
// 物品消耗状态
// ============================================================================
bool UPersistenceSubsystem::IsItemCollected(FName ItemId) const
{
return CollectedItemIDs.Contains(ItemId);
}
void UPersistenceSubsystem::MarkItemCollected(FName ItemId)
{
CollectedItemIDs.Add(ItemId);
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 物品已收集: %s"), *ItemId.ToString());
}
// ============================================================================
// 门状态
// ============================================================================
bool UPersistenceSubsystem::IsDoorOpen(FName DoorId) const
{
const bool* Found = DoorStates.Find(DoorId);
return Found ? *Found : false;
}
void UPersistenceSubsystem::SetDoorOpen(FName DoorId, bool bIsOpen)
{
DoorStates.Add(DoorId, bIsOpen);
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 门状态更新: %s → %s"),
*DoorId.ToString(), bIsOpen ? TEXT("开") : TEXT("关"));
}
// ============================================================================
// 重置
// ============================================================================
void UPersistenceSubsystem::ResetAll()
{
CollectedItemIDs.Empty();
DoorStates.Empty();
UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 所有持久数据已重置"));
}
@@ -0,0 +1,63 @@
// ChildHood Project - 跨场景持久化子系统
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "PersistenceSubsystem.generated.h"
/**
* UPersistenceSubsystem
*
* UGameInstanceSubsystem,跨场景持久数据持有者。
* UE5 的 OpenLevel() 会销毁整个旧 World(包括所有 Actor 和 Component),
* 因此将持久数据放在 GameInstanceSubsystem 中,与 World 生命周期解耦。
*
* 当前持有:
* - CollectedItemIDs: 已收集物品 ID 集合
* - DoorStates: 门开关状态映射
*/
UCLASS()
class UPersistenceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
// === 物品消耗状态 ===
/** 查询指定物品是否已被收集(跨场景持久) */
UFUNCTION(BlueprintCallable, Category = "Persistence")
bool IsItemCollected(FName ItemId) const;
/** 标记指定物品为已收集(写入持久层) */
UFUNCTION(BlueprintCallable, Category = "Persistence")
void MarkItemCollected(FName ItemId);
// === 门状态 ===
/** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */
UFUNCTION(BlueprintCallable, Category = "Persistence")
bool IsDoorOpen(FName DoorId) const;
/** 设置指定门的开关状态(写入持久层) */
UFUNCTION(BlueprintCallable, Category = "Persistence")
void SetDoorOpen(FName DoorId, bool bIsOpen);
// === 重置 ===
/** 重置所有持久数据(新游戏时调用) */
UFUNCTION(BlueprintCallable, Category = "Persistence")
void ResetAll();
private:
/** 已收集物品 ID 集合 */
UPROPERTY()
TSet<FName> CollectedItemIDs;
/** 门开关状态映射(true=打开,false=关闭) */
UPROPERTY()
TMap<FName, bool> DoorStates;
};