Files
Echo/Assets/_Project/Docs/InteractionSystem_Technical_Reference.md
2026-07-08 20:42:51 +08:00

14 KiB
Raw Permalink Blame History

交互系统技术参考(条件 / 消耗 / 解锁 / UI 提示 / 指向高亮)

本文档用于汇总当前工程内“交互系统”的落地实现与推荐配置方式,便于后续扩展与排错。

1. 总览

系统目标:

  • 交互行为(开门/机关/拾取等)继续由 IInteractable.Interact() 负责
  • “能不能交互 / 失败原因 / 成功后消耗 / 成功后解锁 / 解锁后外观变化”由可配置的组件承担
  • 玩家侧在调用 Interact() 前统一做条件校验,并把失败原因用于 UI/日志
  • 玩家指向可交互对象时,提供 UI 提示与外轮廓高亮反馈

核心入口:

2. 交互调用链(时序)

  1. 玩家 Raycast 命中交互层物体
  2. 收集命中 collider 与父物体上的 IInteractable(可多个)
  3. 在调用 Interact() 前,获取父级所有 InteractConditionBehaviour 并逐一 CanInteract(interactor, out failReason)
  • 任一失败:中止,不调用 Interact(),可把 failReason 展示到 UI
  1. 全部通过:依次调用每个 IInteractable.Interact()
  2. 交互完成后:逐一回调 OnInteractSucceeded(interactor)(用于消耗、解锁、触发联动)

条件基类与接口:

3. 条件 / 消耗 / 解锁(组件清单)

3.1 道具需求 + 可选消耗 + 可选一次解锁

组件:RequireItemsInteractCondition

用途:

  • 单道具门禁(配置 1 条 requirement
  • 多道具 ANDRequireAll
  • 多道具 ORRequireAny
  • 成功后消耗(consumeOnSuccess
  • 成功后一次解锁(unlockAfterSuccess

关键语义:

  • unlockAfterSuccess + unlocked(或绑定 unlockState)为 true 时,会直接放行,不再校验/不再消耗
    • 常见排错点:测试时如果勾了 Unlocked,即使没有物品也会通过
  • RequireAny + consumeOnSuccess:只消耗本次“触发通过”的那一条需求

3.2 解锁状态源(用于后续 UI/渲染读取、以及外观切换)

组件:InteractUnlockState

用途:

  • 保存“是否已解锁”的状态(IsUnlocked
  • 解锁/上锁时自动激活/隐藏目标物体
    • activateWhenUnlocked:解锁时显示(例:发电机上的电池模型)
    • deactivateWhenUnlocked:解锁时隐藏(例:门锁模型)
  • 可选作为“门禁条件”参与交互校验
    • 勾选 blockInteractionWhenLocked 后:当 unlocked=false 会阻止交互,并返回 lockedFailReason
    • 默认不勾选:仅作为状态源与外观切换,不影响交互(兼容旧场景配置)
  • 可选对外广播状态事件(便于联动 UI/音效/剧情)
    • onUnlocked / onLocked / onValueChanged(bool)

3.3 未解锁则禁止交互(门禁)

组件:RequireUnlockedInteractCondition

用途:

  • 给某个开关/门/机关设置“必须已解锁才允许交互”
  • unlockState 可不填,组件会向父级查找 InteractUnlockState
  • 如果你希望“只要挂了 InteractUnlockState 且未解锁就不能互动”,也可以直接在 InteractUnlockState 上勾选 blockInteractionWhenLocked(无需再挂本条件组件)

3.4 交互成功后解锁(联动)

组件:UnlockStatesOnInteractSucceeded

用途:

  • 在某个交互成功后,解锁自身或其它目标(例如发电机解锁另一个开关)

4. 常用配置范式(推荐挂载位置:交互根节点)

推荐挂载位置:

  • 条件/解锁相关组件挂在“门/机关根节点”(通常是 collider 的父级或同级)
  • 玩家侧用 GetComponentsInParent<InteractConditionBehaviour>(),挂在父级更稳妥

4.1 无条件交互

  • 只挂交互行为组件(Door/Lever/Toggle* 等),不挂任何 InteractConditionBehaviour

4.2 有锁的门:满足条件后隐藏锁,并永久解锁

门根节点:

  • DoorController(或其它 IInteractable
  • InteractUnlockState
    • deactivateWhenUnlocked 填门锁子物体
  • RequireItemsInteractCondition
    • 配置钥匙需求
    • consumeOnSuccess(需要消耗钥匙则勾)
    • unlockAfterSuccess 勾选
    • unlockState 指向同门上的 InteractUnlockState

4.3 发电机放电池:消耗电池、显示电池模型、解锁另一个开关

发电机根节点:

  • RequireItemsInteractCondition(配置电池,勾选消耗 + 解锁)
  • InteractUnlockState
    • activateWhenUnlocked 填“隐藏的电池模型”
  • UnlockStatesOnInteractSucceeded
    • targetStates 填“另一个开关的 InteractUnlockState”

被解锁的开关根节点:

  • InteractUnlockState(初始未解锁)
  • RequireUnlockedInteractCondition(未解锁时禁止交互)

4.4 发电机首次启动:触发一次性旁白/对话(存档持久化)

目标:

  • 发电机第一次“启动/通电”时触发一段旁白或其它事件
  • 该事件只触发一次(跨存档/跨重进场景也不会再触发)

推荐做法(不改玩家交互入口,完全 Inspector 配置):

  1. 让发电机“启动开关”使用 ToggleMoveInteractableToggleRotateInteractable(它们有 onTurnedOn / onTurnedOff 事件)
  2. 在发电机根节点(或同级管理对象)挂 OneShotFlagEvent
    • flagId 建议命名:Generator_<UniqueName>_FirstOn
    • autoSaveAfterTrigger 建议勾选(触发后立刻写入存档)
  3. 把开关脚本的 onTurnedOn 指向 OneShotFlagEvent.Trigger
  4. OneShotFlagEvent.onFirstTriggered 上挂你要触发的事件:
    • 播放旁白:使用 NarrationPlayByIdAction(配置 sequenceId),或直接调用 NarrationSystem.PlayById
    • 触发其它联动:任意 UnityEvent 目标方法都可以

运行依赖(自动注入):

5. UI:指向提示(Reach Feed Notification

脚本落点:PlayerController.cs

用途:

  • 玩家指向可交互对象时:
    • 条件通过:显示“可互动”通知
    • 条件失败:显示“不可互动”通知(可选显示 failReason)

Reach 参考:

如何加速弹出动画(FeedNotification):

  • 推荐改 Animator Controller 状态 SpeedAssets/ThirdParty/UI/Reach - Complete Sci-Fi UI/Animations/HUD/FeedNotification.controller
  • 或缩短动画片段时长:FeedNotification_In.anim / FeedNotification_Out.anim
  • 或运行时提高 Animator.speedPlayerController 已提供字段)

6. 视觉:指向高亮(URP 外轮廓)

实现方式:

  • 指向时给目标 Renderer 追加一个“外轮廓材质”,并用材质属性控制颜色/宽度

组件:

Shader

颜色规则(默认实现):

  • 条件全部通过:绿(可自定义)
  • 条件失败且属于“未解锁门禁”:橙(可自定义)
  • 其它失败:使用 blockedColor(可自定义)

7. 物理状态控制 (防掉落/穿模)

组件:ItemPhysicsState

用途:

  • 解决场景初始放置的物品穿模问题:当互动物品(如药瓶、道具)放置在只有一个大 BoxCollider 的架子或柜子内部时,直接启用刚体会导致物品在场景一开始就被物理引擎“挤出”或弹飞。
  • 配置方法:将该组件挂载在拥有 Rigidbody 的互动物体上,并勾选 startKinematic = true
  • 机制
    • 场景加载(Awake)时,该物体会自动锁定为 isKinematic = true,不受重力和碰撞弹出的影响。
    • 当玩家拾取该物体存入背包,后续再次丢弃(Drop)到场景时,丢弃逻辑会自动调用 ReleaseKinematic() 将其恢复为完全受重力影响的物理状态,玩家此时踢到它或将其扔向墙壁都会有正常的物理反馈。
  • 注意:默认状态下 startKinematic = false,此时不产生任何影响。这是一项附加配置选项,不会干扰原有交互校验逻辑链。

8. 物品使用事件触发器 (监听左键使用)

组件:ItemUseEventTrigger

用途:

  • 监听特定物品在背包/手中被“使用”:当玩家在场景中任意位置,选中特定物品并按下“使用”键(通常为左键)时,触发场景中的指定事件(例如:首次吃下某种药丸后触发旁白、首次阅读特定文件后开启某扇门)。

配置方法:

  1. 在场景中创建一个空物体(或在任意管理节点上)。
  2. 挂载 ItemUseEventTrigger 组件。
  3. Target Item:指定你要监听的物品数据(ItemData)。
  4. Trigger Only Once:勾选则表示仅在第一次使用该物品时触发(默认开启)。
  5. On Target Item Used:配置你需要触发的 UnityEvent(如播放旁白、启用对象等)。

实现机制:

  • ItemUsageSystem 核心脚本中新增了 OnItemUsedGlobal 静态全局事件。每次有物品被成功使用时会广播该事件。
  • ItemUseEventTriggerOnEnable 时自动订阅,纯旁路监听,完全不影响任何现有的物品消耗、装备或UI显示逻辑

9. 交互计数器系统 (配合 Interact 累加并触发事件)

组件:

用途:

  • 解决“交互对象会在 Interact 后被销毁/替换(例如拾取物 Destroy 自身)导致后挂脚本失效”的场景。
  • 被交互对象 在一次“交互成功”时向 场景中的系统对象 累加计数;当计数达到阈值后,由系统对象触发一组 UnityEvent

配置方法:

  1. 在场景中创建一个空物体(或在任意管理节点上)挂 InteractCountThresholdSystem
  2. 设置 Threshold 为你希望触发事件所需的累计次数,并在 On Threshold Reached 中配置事件列表。
  3. 在你想要计数的“被交互对象根节点”(通常是 collider 的父级或同级)挂 InteractCountAccumulator
  4. Target System 指向第 1 步的系统对象;Add Amount 设置每次成功交互累加多少(默认 1)。

触发语义:

  • InteractCountAccumulator 走的是 InteractConditionBehaviour.OnInteractSucceeded 回调,只会在交互条件全部通过且 Interact() 已执行后触发;因此适合拾取类物体在销毁前完成计数与触发。
  • InteractCountAccumulator 只有在“从未绑定过 Target System”的情况下才会自动查找场景中的 InteractCountThresholdSystem;如果运行过程中 Target System 被销毁导致引用变空,则不会再随意改投到其它阈值系统上。

10. 排错清单

  • “没有物品也能交互成功”
    • 检查 RequireItemsInteractCondition 是否勾选了 unlockAfterSuccessunlocked(或 unlockState.unlocked)为 true
    • 检查 requirement 是否为空(为空会被视为无需资格直接放行)
  • “提示/高亮不出现”
    • 检查交互层(interactLayer)与射线范围(interactRange
    • 检查目标是否存在 IInteractable(本体或父物体)
    • 外轮廓:目标是否挂了 InteractOutlineTarget,玩家是否挂了 PlayerAimOutlineHighlighter 且绑定了 outlineMaterial

11. 相关文档索引