Files
ChildHood/docs/technical/interaction-system-tech.md
T
starmen1265 150f94c674 docs(technical): v0.1 程序设计——角色控制+交互+收集三份技术方案
P-003 步骤④程序设计产出:

- player-controller-tech.md: 角色控制系统(参数调整+输入绑定,2-3天)
- interaction-system-tech.md: 交互系统核心框架(状态机+推镜+物品中转,5-7天)
- collection-system-tech.md: 收集系统进度追踪+通知UI(1-2天)

覆盖三份执行案全部行为规则,含美术需求清单和技术风险分析。
等待 PM 质量门审查。
2026-06-06 00:30:50 +08:00

31 KiB
Raw Blame History

交互系统技术设计文档(v0.1 原型)

任务ID: P-003(部分) 对应执行案: docs/design/prototype/interaction-system-execution.md 关联系统: 角色控制、物品系统、收集系统 阶段: 原型 日期: 2026-06-06


概述

交互系统是 v0.1 的核心体验系统,让玩家通过"走近并按键"与环境物体、NPC 产生互动。包含四种交互类型(观察/拾取/操作/对话),涉及状态机管理、摄像机推镜、UI 显示、跨系统通知等多个子系统。

实现复杂度:中-高 预估工期:5-7 天 新增代码量:约 800-1200 行 C++ + 2 个 UMG 蓝图


类与组件架构

新增类

类名 类型 职责 位置
IChildHoodInteractable UInterface 交互物统一接口 Interaction/
UInteractableComponent UActorComponent 可附着到任意 Actor,提供交互数据与触发检测 Interaction/
UInteractionManager UActorComponent 交互状态机 + 检测 + 选择 + 摄像机推镜 + UI 管理 Interaction/

修改类

类名 修改内容
AChildHoodCharacter 持有 InteractionManager,提供交互状态查询接口
AChildHoodPlayerController 注册交互相关 IA(E/ESC/Tab/滚轮)

新增蓝图

蓝图名 类型 用途
WBP_InteractionPrompt UMG Widget 交互提示 UI([E] 查看旧冰箱)
WBP_InteractionText UMG Widget 文本底框(半透明背景 + 文字)
BP_InteractableActor Actor Blueprint 可交互物基类蓝图(带 InteractableComponent)
BP_PickupItem Actor Blueprint 拾取物蓝图(继承 BP_InteractableActor)
BP_InteractableDoor Actor Blueprint 门蓝图(继承 BP_InteractableActor,含开关状态)
BP_InteractableNPC Actor Blueprint NPC 蓝图(继承 BP_InteractableActor)

架构关系

AChildHoodPlayerController
├── Enhanced Input
│   ├── IA_Interact (E) ──────────┐
│   ├── IA_Cancel (ESC) ──────────┤
│   └── IA_SwitchInteract (Tab) ──┤
│                                  ▼
AChildHoodCharacter               UInteractionManager
├── CapsuleComponent              ├── 状态机 (Free/Interacting)
├── SkeletalMesh                  ├── 检测列表 (NearbyInteractables)
├── SpringArm + Camera            ├── 当前选中 (SelectedInteractable)
├── UInteractionManager ◄─────────┤── 摄像机推镜控制
└── bIsInteracting 标志           ├── UI 管理 (Prompt + Text)
                                  └── 交互流程驱动

场景中的可交互物:
AActor (任意 Actor)
└── UInteractableComponent (实现 IChildHoodInteractable)
    ├── FInteractableData (配置数据)
    ├── USphereComponent (触发检测范围)
    └── UStaticMeshComponent (占位模型)

核心数据结构

枚举

// 交互类型
UENUM(BlueprintType)
enum class EInteractType : uint8
{
    Examine,  // 观察
    Pickup,   // 拾取
    Operate,  // 操作
    Talk      // 对话
};

// 操作子类型
UENUM(BlueprintType)
enum class EOperateSubType : uint8
{
    None,
    Door,    // 门(开关切换)
    Search   // 翻找
};

// 交互系统顶层状态
UENUM(BlueprintType)
enum class EInteractionState : uint8
{
    Free,        // 自由状态
    Interacting, // 交互中
    SubSystem,   // 子系统(v0.1 预留)
    Cutscene     // 过场(v0.1 预留)
};

// Interacting 子阶段
UENUM(BlueprintType)
enum class EInteractPhase : uint8
{
    None,
    Enter,     // 进入:转向 + 推镜
    AnimLock,  // 动画锁定:等待动画播完
    Active,    // 活跃:等待玩家输入
    Exit       // 退出:回位 + 淡出
};

// 交互结束后的动作
UENUM(BlueprintType)
enum class EPostAction : uint8
{
    Return,      // 回到 Free
    Minigame,    // 进入小游戏(v0.1 预留)
    Dialogue,    // 进入对话(v0.1 预留)
    SceneChange  // 场景切换(v0.1 预留)
};

配置数据结构

USTRUCT(BlueprintType)
struct FInteractableData
{
    GENERATED_BODY()

    // 基本信息
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName ItemId;                    // 物品唯一ID(如 "fridge_home")

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString DisplayName;            // 显示名称(如 "旧冰箱")

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    EInteractType Type = EInteractType::Examine;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    EOperateSubType SubType = EOperateSubType::None;

    // 触发
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float TriggerRadius = 150.0f;   // 触发半径(cm)

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bOneShot = false;          // 一次性消耗

    // 提示文本
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString InteractVerb;           // 动作词(如 "查看"、"拾取")

    // 叙事文本
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<FString> NarrativeTexts; // 文本列表,随机选取

    // 摄像机推镜
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bUseCameraPush = false;    // 是否推镜

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float CameraPushDistance = 100.0f; // 推镜距离(cm)

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float CameraPushHeightOffset = -20.0f; // 推镜高度偏移(略低于物体中心)

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float CameraPushDuration = 0.5f;  // 推镜时长(秒)

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float CameraReturnDuration = 0.3f; // 回位时长(秒)

    // 动画
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    UAnimMontage* InteractionMontage = nullptr; // 交互动画

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float AnimEventProgress = 0.6f; // 动画中触发事件的进度点(0-1)

    // 物品(拾取/翻找)
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName RewardItemId;             // 获得的物品ID

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString RewardItemCategory = TEXT("杂物"); // 物品品类

    // 结束动作
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    EPostAction PostAction = EPostAction::Return;
};

行为规则 → 实现方案映射

一、状态机实现(UInteractionManager)

状态机设计

UCLASS()
class UInteractionManager : public UActorComponent
{
    GENERATED_BODY()

    // 当前状态
    EInteractionState State = EInteractionState::Free;
    EInteractPhase Phase = EInteractPhase::None;

    // 检测
    bool bInDetectionRange = false;
    TArray<TWeakObjectPtr<UInteractableComponent>> NearbyInteractables;
    int32 SelectedIndex = 0;

    // 当前交互
    UPROPERTY()
    UInteractableComponent* CurrentInteractable = nullptr;

    // 摄像机推镜
    UPROPERTY()
    ACameraActor* PushCamera = nullptr;

    // 计时器
    float PhaseTimer = 0.f;
    float TurnProgress = 0.f;

    // UI
    UPROPERTY()
    UUserWidget* PromptWidget = nullptr;
    UPROPERTY()
    UUserWidget* TextWidget = nullptr;
};

状态转换逻辑

Tick 流程:

Free 状态:
  ├── 更新 NearbyInteractables(基于 Overlap 事件维护)
  ├── 按距离排序
  ├── 更新 bInDetectionRange
  ├── 更新 PromptWidget(选中物的提示)
  └── 等 E 键触发

E 键触发:
  Free → Interacting/Enter
  ├── 保存当前摄像机状态
  ├── 设置 bIsInteracting = true(通知角色控制)
  ├── 启动角色转向(0.2s)
  └── 如需推镜,启动推镜

Enter 阶段完成:
  Enter → AnimLock 或 Active(取决于交互类型)
  ├── 有动画 → AnimLock(等动画播完)
  └── 无动画 → Active(等文本显示 + 玩家输入)

AnimLock 结束(动画播完):
  ├── 拾取类 → 执行物品逻辑 → Exit
  ├── 门操作 → 执行状态切换 → Exit
  └── 翻找类 → 结果判定 → Active 或 Exit

Active 阶段(等 E/ESC):
  E/ESC → Exit

Exit 阶段:
  ├── 淡出 UI
  ├── 摄像机回位
  └── 完成后 → Free(检查 postAction,v0.1 全为 Return)

二、通用规则

G1. 触发条件

实现:UInteractableComponent 持有 USphereComponent 作为触发范围

// UInteractableComponent 初始化
TriggerSphere = CreateDefaultSubobject<USphereComponent>("TriggerSphere");
TriggerSphere->SetSphereRadius(Data.TriggerRadius);
TriggerSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
TriggerSphere->SetCollisionResponseToAllChannels(ECR_Ignore);
TriggerSphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
TriggerSphere->SetupAttachment(GetOwner()->GetRootComponent());

// Overlap 事件 → 通知 InteractionManager
TriggerSphere->OnComponentBeginOverlap.AddDynamic(this, &UInteractableComponent::OnOverlapBegin);
TriggerSphere->OnComponentEndOverlap.AddDynamic(this, &UInteractableComponent::OnOverlapEnd);

InteractionManager 维护 NearbyInteractables 列表,按距离排序。

消耗检查:

bool IChildHoodInteractable::CanInteract() const
{
    return !bConsumed; // oneShot 且已消耗则返回 false
}

G2. 提示 UI 显示规则

实现:WBP_InteractionPrompt(UMG Widget)

  • 位置:通过 ProjectWorldLocationToScreen 将交互物注册点投影到屏幕空间
  • 格式:[E] {动词} {物体名}(如 [E] 查看旧冰箱)
  • 淡入/淡出:Widget Animation 0.2s
  • 更新时机:每 Tick 更新位置和内容(仅当选中物变化时刷新内容)

多交互物切换:

void UInteractionManager::SwitchToNext()
{
    if (NearbyInteractables.Num() == 0) return;
    SelectedIndex = (SelectedIndex + 1) % NearbyInteractables.Num();
    UpdatePromptWidget();
}

void UInteractionManager::SwitchToPrevious()
{
    if (NearbyInteractables.Num() == 0) return;
    SelectedIndex = (SelectedIndex - 1 + NearbyInteractables.Num()) % NearbyInteractables.Num();
    UpdatePromptWidget();
}

距离排序:

NearbyInteractables.Sort([this](const TWeakObjectPtr<UInteractableComponent>& A,
                                const TWeakObjectPtr<UInteractableComponent>& B)
{
    float DistA = FVector::Dist2D(Character->GetActorLocation(), A->GetInteractLocation());
    float DistB = FVector::Dist2D(Character->GetActorLocation(), B->GetInteractLocation());
    return DistA < DistB;
});

G3. 交互触发(E 键)

实现:InteractionManager::OnInteractPressed()

void UInteractionManager::OnInteractPressed()
{
    if (State != EInteractionState::Free || !bInDetectionRange) return;

    auto Selected = GetSelectedInteractable();
    if (!Selected || !Selected->CanInteract()) return;

    StartInteraction(Selected);
}

G4. 通用文本显示 UI

实现:WBP_InteractionText(UMG Widget)

  • 位置:屏幕正中偏下(Anchors: Center, Offset Y = +10% 屏幕高度)
  • 底框:半透明黑色背景(Background Color: Black, Opacity 0.6),圆角
  • 文字:白色,居中,内容自适应宽高
  • 显示:整句一次性显示(无打字机效果)
  • 淡入/淡出:Widget Animation 0.2s

三、交互类型一:观察(Examine)

流程实现

void UInteractionManager::StartExamine(UInteractableComponent* Interactable)
{
    Phase = EInteractPhase::Enter;
    PhaseTimer = 0.f;

    // 1. 角色转向(0.2s)
    StartCharacterTurn(Interactable->GetInteractLocation());
    TurnDuration = 0.2f;

    // 2. 推镜(与转向同时,0.5s ease-out)
    StartCameraPush(Interactable, 0.5f);

    // 3. 设置 Enter 阶段完成时间 = max(转向, 推镜) + 0.2s 等待
    EnterCompleteDelay = FMath::Max(0.2f, 0.5f) + 0.2f;
}

Enter → Active 转换:

// Tick 中
if (Phase == EInteractPhase::Enter)
{
    PhaseTimer += DeltaTime;
    UpdateCharacterTurn(DeltaTime);
    UpdateCameraPush(DeltaTime);

    if (PhaseTimer >= EnterCompleteDelay)
    {
        // 4. 显示文本
        FString Text = SelectRandomText(CurrentInteractable);
        ShowInteractionText(Text);
        Phase = EInteractPhase::Active;
    }
}

关闭(E/ESC):

void UInteractionManager::CloseExamine()
{
    Phase = EInteractPhase::Exit;
    HideInteractionText();        // 淡出 0.2s
    StartCameraReturn(0.3f);      // ease-in 回位
    ExitDuration = 0.3f;
}

文本随机选取:

FString UInteractionManager::SelectRandomText(UInteractableComponent* Interactable)
{
    auto& Texts = Interactable->GetNarrativeTexts();
    if (Texts.Num() == 0) return TEXT("");
    if (Texts.Num() == 1) return Texts[0];

    // 不重复上一次
    int32 Index;
    do {
        Index = FMath::RandRange(0, Texts.Num() - 1);
    } while (Index == LastTextIndex && Texts.Num() > 1);

    LastTextIndex = Index;
    return Texts[Index];
}

四、交互类型二:拾取(Pickup)

流程实现

void UInteractionManager::StartPickup(UInteractableComponent* Interactable)
{
    Phase = EInteractPhase::Enter;
    PhaseTimer = 0.f;

    // 1. 角色转向(0.2s)
    StartCharacterTurn(Interactable->GetInteractLocation());

    // 拾取不推镜,相机保持第三人称位置
    // 2. 转向完成后播放动画 → AnimLock
    bPendingAnimLock = true;
    PendingMontage = nullptr; // v0.1 可能无动画
    PendingAnimDuration = 1.0f; // 拾取动画时长

    // 3. 物品消失在动画 60% 处
    AnimEventProgress = 0.6f;
    bAnimEventTriggered = false;
}

// Tick 中处理 AnimLock
if (Phase == EInteractPhase::AnimLock)
{
    PhaseTimer += DeltaTime;
    float Progress = PhaseTimer / PendingAnimDuration;

    // 物品消失时间点
    if (!bAnimEventTriggered && Progress >= AnimEventProgress)
    {
        bAnimEventTriggered = true;
        CurrentInteractable->SetVisibility(false); // 视觉消失

        // 通知物品系统
        NotifyItemSystem(CurrentInteractable->GetRewardItemId(),
                        CurrentInteractable->GetRewardItemCategory());
    }

    // 动画播完
    if (PhaseTimer >= PendingAnimDuration)
    {
        CurrentInteractable->MarkConsumed(); // 标记已消耗
        Phase = EInteractPhase::Exit;
        // Exit 立即完成(无回位动画)
        FinishInteraction();
    }
}

拾取物外观:所有拾取物使用同一个占位模型(简单球形或方块,0.1m 直径)。通过材质颜色区分稀有度(普通=白色,稀有=金色)。

不支持手动关闭:拾取全程 AnimLock,忽略所有按键。


五、交互类型三:操作(Operate)

3A. 门(开关切换)

void UInteractionManager::StartDoor(UInteractableComponent* Interactable)
{
    Phase = EInteractPhase::Enter;
    PhaseTimer = 0.f;

    // 1. 角色转向(0.2s)
    StartCharacterTurn(Interactable->GetInteractLocation());

    // 不推镜
    // 2. 转向完成后播放动画 → AnimLock
    bPendingAnimLock = true;
    PendingAnimDuration = 0.8f; // 门操作动画时长
    AnimEventProgress = 0.4f;   // 40% 处切换状态
}

// AnimLock 中的事件
if (!bAnimEventTriggered && Progress >= AnimEventProgress)
{
    bAnimEventTriggered = true;
    // 通知门的蓝图切换开关状态
    CurrentInteractable->OnInteractEvent();
}

// 动画播完 → Exit
if (PhaseTimer >= PendingAnimDuration)
{
    FinishInteraction(); // 直接回到 Free
}

门状态切换:在 BP_InteractableDoor 蓝图中实现。InteractableComponent 触发事件 → 蓝图响应旋转门 Mesh + 更新提示文本("打开门" ↔ "关上门")。

3B. 废纸箱(翻找)

void UInteractionManager::StartSearch(UInteractableComponent* Interactable)
{
    Phase = EInteractPhase::Enter;
    PhaseTimer = 0.f;

    // 1. 角色转向(0.2s)
    StartCharacterTurn(Interactable->GetInteractLocation());

    // 2. 推镜(与转向同时,0.5s)
    StartCameraPush(Interactable, 0.5f);

    // 3. 转向完成后播放翻找动画 → AnimLock
    bPendingAnimLock = true;
    PendingAnimDuration = 1.5f; // 翻找动画时长
}

// AnimLock 完成后
void UInteractionManager::OnSearchAnimComplete()
{
    if (CurrentInteractable->HasRewardItem())
    {
        // 有物品分支
        NotifyItemSystem(CurrentInteractable->GetRewardItemId(),
                        CurrentInteractable->GetRewardItemCategory());
        CurrentInteractable->MarkConsumed();
        // 物品系统弹出拾取通知,收集系统弹出收集通知
        // 等待 E/ESC
        Phase = EInteractPhase::Active;
    }
    else
    {
        // 无物品分支(架构预留)
        ShowInteractionText(TEXT("什么都没找到。"));
        Phase = EInteractPhase::Active;
    }
}

六、交互类型四:对话(Talk)— 简化版

void UInteractionManager::StartTalk(UInteractableComponent* Interactable)
{
    // 与观察类流程几乎相同
    Phase = EInteractPhase::Enter;
    PhaseTimer = 0.f;

    StartCharacterTurn(Interactable->GetInteractLocation());
    StartCameraPush(Interactable, 0.5f);

    // Enter 完成后显示 NPC 对话文本
    // Active 阶段等 E/ESC 关闭
}

与观察类的区别:

  • 文本来源不同(对话内容 vs 小孩想法)
  • 无功能差异,v0.1 共用同一流程

七、摄像机推镜系统

实现方案:ACameraActor + SetViewTargetWithBlend

原理:交互时创建临时 CameraActor,通过 UE5 内置的摄像机混合系统平滑切换视角。

void UInteractionManager::StartCameraPush(UInteractableComponent* Interactable, float Duration)
{
    if (!Interactable->GetData().bUseCameraPush) return;

    // 计算推镜目标位置
    FVector InteractLoc = Interactable->GetInteractLocation();
    FVector CharacterLoc = GetOwner()->GetActorLocation();
    FVector DirToCharacter = (CharacterLoc - InteractLoc).GetSafeNormal2D();

    // 推镜位置:物体前方指定距离,略低于物体中心
    float PushDist = Interactable->GetData().CameraPushDistance;
    float HeightOffset = Interactable->GetData().CameraPushHeightOffset;

    FVector PushLocation = InteractLoc - DirToCharacter * PushDist;
    PushLocation.Z = InteractLoc.Z + HeightOffset + 80.f; // 80 = 角色站立高度补偿

    FRotator PushRotation = (InteractLoc - PushLocation).Rotation();

    // 创建或复用推镜摄像机
    if (!PushCamera)
    {
        FActorSpawnParameters Params;
        PushCamera = GetWorld()->SpawnActor<ACameraActor>(ACameraActor::StaticClass(),
                                                           PushLocation, PushRotation, Params);
    }
    else
    {
        PushCamera->SetActorLocationAndRotation(PushLocation, PushRotation);
    }

    // 混合到推镜摄像机
    APlayerController* PC = Cast<APlayerController>(Cast<APawn>(GetOwner())->GetController());
    PC->SetViewTargetWithBlend(PushCamera, Duration,
        EViewTargetBlendFunction::VTBlend_EaseInOut, 1.0f);

    bIsCameraPushed = true;
}

void UInteractionManager::StartCameraReturn(float Duration)
{
    if (!bIsCameraPushed) return;

    APlayerController* PC = Cast<APlayerController>(Cast<APawn>(GetOwner())->GetController());
    APawn* Pawn = PC->GetPawn();

    // 混合回角色(SpringArm 恢复控制)
    PC->SetViewTargetWithBlend(Pawn, Duration,
        EViewTargetBlendFunction::VTBlend_EaseInOut, 1.0f);

    bIsCameraPushed = false;
}

生命周期:PushCamera 在 InteractionManager 初始化时创建,整个游戏期间复用,不需要每次交互都 Spawn/Destroy。


八、角色转向实现

void UInteractionManager::StartCharacterTurn(const FVector& TargetLocation)
{
    ACharacter* Character = Cast<ACharacter>(GetOwner());
    if (!Character) return;

    TurnStartRotation = Character->GetActorRotation();
    FVector Direction = (TargetLocation - Character->GetActorLocation());
    Direction.Z = 0.f;
    TurnTargetRotation = Direction.Rotation();
    TurnProgress = 0.f;
    TurnDuration = 0.2f;
    bIsTurning = true;

    // 暂停 CMC 的自动转向,防止冲突
    Character->GetCharacterMovement()->bOrientRotationToMovement = false;
}

void UInteractionManager::UpdateCharacterTurn(float DeltaTime)
{
    if (!bIsTurning) return;

    TurnProgress += DeltaTime / TurnDuration;
    if (TurnProgress >= 1.f)
    {
        TurnProgress = 1.f;
        bIsTurning = false;
        // 不立即恢复 bOrientRotationToMovement,等交互结束
    }

    // Ease-out 插值
    float Alpha = 1.f - FMath::Pow(1.f - TurnProgress, 3.f);
    FRotator NewRotation = FMath::Lerp(TurnStartRotation, TurnTargetRotation, Alpha);
    GetOwner()->SetActorRotation(NewRotation);
}

九、物品系统中转(补充执行案缺失部分)

执行案要求"物品系统(通知中转+拾取通知+背包记录)需在本步骤纳入技术方案"。

UItemManager — 轻量物品管理组件

UCLASS()
class UItemManager : public UActorComponent
{
    GENERATED_BODY()

    // 简单背包:ItemID → 数量
    UPROPERTY()
    TMap<FName, int32> Inventory;

public:
    // 接收交互系统的拾取通知
    void OnItemPickedUp(FName ItemId, const FString& Category, const FString& ItemNumber);

    // 查询
    int32 GetItemCount(FName ItemId) const;
    bool HasItem(FName ItemId) const;

    // 事件:通知 UI 显示拾取通知
    UPROPERTY(BlueprintAssignable)
    FOnItemPickedUpSignature OnItemPickedUpEvent;

    // 事件:通知收集系统
    UPROPERTY(BlueprintAssignable)
    FOnItemCollectedSignature OnItemCollectedEvent;
};

数据流

交互系统 (InteractionManager)
    │ OnItemPickedUp(ItemId, Category, Number)
    ▼
物品系统 (ItemManager)
    ├── Inventory[ItemId]++ (背包记录)
    ├── OnItemPickedUpEvent → WBP_PickupNotification("找到了 XXX")
    └── OnItemCollectedEvent → 收集系统("发现了新的杂物,进度 X/Y")

拾取通知 UI(WBP_PickupNotification)

  • 位置:屏幕右上角
  • 内容:物品名称 + 简短描述
  • 效果:弹出后 2 秒自动淡出
  • v0.1 简化:只显示文字,不显示图标

背包

v0.1 不做背包 UI,仅内部记录。Inventory 使用 TMap<FName, int32> 存储,支持计数查询。


数据流总览

Enhanced Input
    │
    ├── WASD → AChildHoodCharacter::DoMove()
    │         ├── 检查 bIsInteracting(由 InteractionManager 控制)
    │         └── CMC::AddMovementInput()
    │
    ├── 鼠标 → AChildHoodCharacter::DoLook()
    │         └── Controller::AddYawInput/AddPitchInput
    │
    ├── E → UInteractionManager::OnInteractPressed()
    │       ├── Free 状态 → StartInteraction()
    │       └── Active 状态 → CloseCurrentInteraction()
    │
    ├── ESC → UInteractionManager::OnCancelPressed()
    │         └── Active 状态 → CloseCurrentInteraction()
    │
    ├── Tab/滚轮 → UInteractionManager::SwitchToNext/Previous()
    │
    └── Interacting 状态下 WASD/Tab/滚轮被忽略

UInteractableComponent (场景中的交互物)
    │ Overlap 事件
    ▼
UInteractionManager (角色上的组件)
    ├── 维护 NearbyInteractables 列表
    ├── 管理状态机 (Free ↔ Interacting)
    ├── 控制摄像机推镜 (SetViewTargetWithBlend)
    ├── 控制角色转向 (SetActorRotation)
    ├── 控制 UI 显示 (Prompt / Text)
    └── 通知 UItemManager (拾取/翻找获得物品)
        ├── Inventory 记录
        ├── PickupNotification UI
        └── → 收集系统通知

边界情况处理

边界情况 实现方案
快速连续按 E AnimLock 阶段忽略所有输入;Exit 完成才回到 Free
交互物被消耗后范围残留 MarkConsumed() 使 CanInteract() 返回 false;从 NearbyInteractables 移除
推镜期间角色位置 移动输入已被 bIsInteracting 阻断,角色不移动
同一交互物反复进出范围 Prompt 每次淡入淡出,无累积。NearbyInteractables 列表自动增减
切换交互物同时按 E Tab/Scroll 先执行切换,下一帧 E 按下时读取新 SelectedIndex
距离恰好在触发半径 Sphere Overlap 使用 ≤ 语义,边界上视为在范围内
窄空间推镜位置不理想 v0.1 接受不完美;推镜目标位置计算后可做简单碰撞检测(射线检测到阻挡则缩短距离)

美术资源需求

占位模型(第一轮,5 个)

资源类型 名称 数量 规格 触发时机/用途
3D 占位模型 旧冰箱 1 白色方箱体,约 1.7m 高 × 0.7m 宽 × 0.7m 深 家区域观察交互
3D 占位模型 家的门 1 扁平方片(约 2.0m × 0.9m),可绕一侧轴旋转 家区域操作交互(开关)
3D 占位模型 拾取物通用模型 1 简单球形,直径约 0.1m 拾取交互(多个实例,颜色区分)
3D 占位模型 公用电话亭 1 窄高方柱,约 2.0m 高 × 0.8m 宽 街道区域观察交互
3D 占位模型 NPC 老板(简化) 1 简单人形胶囊体,坐姿 小卖部对话交互

占位模型(第二轮,5 个)

资源类型 名称 数量 规格 触发时机/用途
3D 占位模型 老式电视 1 方箱 + 凸起背部,约 0.5m 高 × 0.6m 宽 家区域观察交互
3D 占位模型 零食货架 1 多层架结构,约 1.8m 高 × 1.0m 宽 小卖部观察交互
3D 占位模型 拾取物(水浒卡) 1 小方片,约 0.06m × 0.09m 拾取交互(复用通用拾取模型也可)
3D 占位模型 墙面涂鸦 1 墙面贴花/平面色块,约 0.5m × 0.5m 街道观察交互
3D 占位模型 废纸箱 1 开口方箱,约 0.5m 高 × 0.4m 宽 巷子翻找交互

材质

资源类型 名称 数量 规格 用途
材质 拾取物_普通 1 白色/浅灰,Unlit 或简单 Lit 通用拾取物材质
材质 拾取物_稀有 1 金色,可加简单闪烁 稀有拾取物材质

动画

资源类型 名称 数量 规格 用途
动画 Montage 弯腰拾取 1 约 1.0 秒,从站立→弯腰→站起 拾取交互
动画 Montage 推门 1 约 0.8 秒,伸手推/拉动作 门操作交互
动画 Montage 翻找 1 约 1.5 秒,弯腰翻动物品 废纸箱翻找交互

v0.1 执行案明确允许"若无动画资源,角色在地面滑动也可接受"。动画可简化为角色原地不动(跳过 AnimLock,直接触发事件)。

UI

资源类型 名称 数量 规格 用途
UMG Widget 交互提示 1 [E] 动作词 物体名,白色文字 + 轻描边 显示在交互物上方
UMG Widget 文本底框 1 半透明黑底(60%),白色居中文字,自适应大小 观察/对话/翻找无结果文本
UMG Widget 拾取通知 1 屏幕右上角,物品名称,2 秒淡出 拾取/翻找获得物品提示

技术风险与注意事项

风险 等级 说明 缓解措施
推镜位置计算不准确 🟡 中 贴墙物体的推镜位置可能在墙内 v0.1 接受不完美;加简单射线检测缩短距离
摄像机混合与 SpringArm 冲突 🟡 中 SetViewTargetWithBlend 切换时可能有跳变 使用 ACameraActor 作为中间目标,确保初始位置匹配当前摄像机位置
多交互物切换延迟感 🟢 低 Tab 切换后提示更新需要即时 切换时立即刷新 UI 内容
Overlap 事件遗漏 🟢 低 角色快速移动可能错过 Overlap 事件 用 Tick 补充检测(每隔 N 帧全量扫描)或增大触发范围
动画缺失时的降级处理 🟢 低 无拾取/操作动画时需跳过 AnimLock AnimLock 阶段无 Montage 时直接用定时器模拟时长
蓝图与 C++ 的交互复杂度 🟢 低 交互物具体行为(门旋转、物品消失)在蓝图中实现 C++ 提供虚函数/委托,蓝图 Override

需要注意

  1. 摄像机推镜的初始位置同步:创建 PushCamera 时必须设置到当前摄像机位置,否则混合开始瞬间会有跳变。
  2. bOrientRotationToMovement 的恢复时机:交互结束时必须恢复此标志,否则角色退出交互后不再自动转向。
  3. 交互物 Overlap 性能:每个交互物有独立的 SphereComponent,10 个交互物最多 10 个 Overlap 检测。v0.1 规模无性能问题,后续扩展需考虑优化。
  4. PushCamera 的生命周期:不要每次交互都 Spawn 新的,复用同一个 CameraActor。EndPlay 时清理。

文件结构

Source/ChildHood/
├── Interaction/
│   ├── InteractableInterface.h          # IChildHoodInteractable 接口
│   ├── InteractableComponent.h/.cpp     # 交互物组件(数据 + 触发范围)
│   ├── InteractionManager.h/.cpp        # 交互状态机 + 检测 + 推镜 + UI
│   ├── InteractionTypes.h               # 枚举、结构体定义
│   └── ItemManager.h/.cpp              # 物品系统(背包 + 通知中转)
├── Player/
│   ├── ChildHoodCharacter.h/.cpp        # 修改:添加 InteractionManager
│   └── ChildHoodPlayerController.h/.cpp # 修改:添加交互 IA 注册
└── UI/
    ├── InteractionPromptWidget.h/.cpp   # (可选:C++ 基类,也可纯蓝图)
    └── InteractionTextWidget.h/.cpp     # (可选:C++ 基类,也可纯蓝图)

Content/
├── Blueprints/
│   ├── Interaction/
│   │   ├── BP_InteractableActor         # 可交互物基类蓝图
│   │   ├── BP_PickupItem               # 拾取物蓝图
│   │   ├── BP_InteractableDoor         # 门蓝图
│   │   ├── BP_InteractableSearch       # 翻找物蓝图(废纸箱)
│   │   └── BP_InteractableNPC          # NPC 蓝图
│   └── Characters/
│       └── BP_ChildHoodCharacter        # 玩家角色蓝图(修改)
├── UI/
│   ├── WBP_InteractionPrompt           # 交互提示 Widget
│   ├── WBP_InteractionText             # 文本底框 Widget
│   └── WBP_PickupNotification          # 拾取通知 Widget
└── Input/
    ├── IMC_Default                     # 输入映射上下文(修改:添加交互 IA)
    ├── IA_Move                         # 已有
    ├── IA_MouseLook                    # 已有
    ├── IA_Interact                     # 新增:E 键
    ├── IA_Cancel                       # 新增:ESC 键
    └── IA_SwitchInteract               # 新增:Tab / 鼠标滚轮