8.0 KiB
8.0 KiB
互动条件校验方案(仅方案,不写代码)
更多“条件/消耗/解锁/UI 提示/指向高亮”的统一说明见:
现状梳理(基于已存在脚本)
- 玩家侧:
PlayerController.PerformInteract()通过 Raycast 命中物体后,查找IInteractable(本体或父物体)并直接调用Interact()。- 目前已接入“条件组件”前置判断:在调用
Interact()前,查找父级上的InteractConditionBehaviour并逐一校验;失败时会输出失败原因日志。
- 目前已接入“条件组件”前置判断:在调用
- 物体侧:
DoorController、LeverController、ToggleRotateInteractable都实现了IInteractable,其Interact()只负责执行动作(切换状态 + DOTween 动画/旋转),并各自提供了GetInteractPrompt()(但玩家侧目前未使用)。 - 道具侧:
HandController负责“手持外观”;ItemUsageSystem负责“使用道具输入 -> 触发特定 UI/消耗逻辑”;两者与“场景交互门/拉杆”无直接条件绑定。
结论:当前架构是“玩家统一触发 -> 物体自行执行”,适合无条件互动;一旦加入“需要特定道具/状态才能互动”,就需要在 Interact() 调用前增加一层“条件判定”。
补充:你新增的 ToggleRotateInteractable 对架构的影响
- 这个脚本本质上把“门/拉杆这类‘开关式旋转交互’”抽象成了一个通用组件:
- 支持多目标(
RotationTarget[])、轴选择、Off/On 两套欧拉角、初始化自动检测、Tween 配置、以及UnityEvent(On/Off/StateChanged)。 - 这意味着“行为层”已经开始走向通用化/组件化,比在每个 Door/Lever 单独写逻辑更利于复用。
- 支持多目标(
- 对“需要特定道具才能互动”的需求来说,这反而更适合采用“独立条件组件”的方案:条件不应该写进
ToggleRotateInteractable,否则它就不再通用。
注意:IInteractable 目前只定义了 Interact()
- 你现在的
IInteractable并不包含GetInteractPrompt()(Door/Lever/ToggleRotateInteractable 只是各自额外提供了这个方法)。 - 因此玩家侧如果想统一显示交互提示/失败原因,建议走“可选接口/可选组件”的方式,例如:
- 提示文本走一个独立的接口(如
IInteractPrompt)或提示组件; - 条件失败原因由“条件组件”提供;
- 玩家侧优先查这些可选能力,查不到就降级为默认提示。
- 提示文本走一个独立的接口(如
推荐方向:新增“条件校验脚本/组件”,并让玩家侧在调用前统一校验(推荐)
核心思路
保持现有 DoorController / LeverController / ToggleRotateInteractable 的职责单一:只做“行为与动画”,不要把“钥匙/任务状态/场景权限”等规则硬塞进每个 Controller。
新增一个专门用于校验互动条件的脚本(组件化挂载在可交互物体上或其父物体上),由玩家的交互入口(PlayerController.PerformInteract())在调用 Interact() 前统一检查:
- 通过:照常调用
IInteractable.Interact()。 - 不通过:不调用
Interact(),并输出一个统一的“失败提示”(后续可接 UI 提示、音效、日志)。
为什么推荐“新增组件”,而不是直接改现有 Controller
- 可复用:同一套条件组件可以给门、箱子、机关、NPC 对话等复用,而不是每个交互脚本都写一遍“检查背包是否有钥匙”。
- 可配置:Inspector 上直接配置“需要的道具ID/数量/是否消耗/需要的剧情标记”等,设计迭代成本更低。
- 可扩展:未来加入“多个条件组合(AND/OR)”“一次性权限”“任务阶段限制”等,只需要扩展条件组件,不必改每个交互物体的行为脚本。
- 更符合分层:玩家侧是统一入口、物体侧是具体行为、条件侧是规则/权限。
你需要“动哪些地方”
- 新增脚本(建议新增到
Interaction或新建Interaction/Conditions目录)- 作为一个“可选组件”:只有需要条件的物体才挂它。
- 轻改
PlayerController.PerformInteract()- 在
interactable.Interact()之前:尝试在命中物体/父物体上查找“条件组件”,存在则执行校验,不存在则默认允许互动。
- 在
- (可选,后续做 UI 时再做)把
GetInteractPrompt()也纳入玩家侧- 由于
IInteractable不包含 prompt,建议用“可选接口/可选组件”的方式整合提示文本。
- 由于
本项目落地实现(当前仓库)
- 条件组件脚本路径:
Assets/_Project/Scripts/Interaction/Conditions/RequireItemsInteractCondition.cs(多道具 OR/AND)Assets/_Project/Scripts/Interaction/Conditions/RequireUnlockedInteractCondition.cs(基于解锁状态的门禁)Assets/_Project/Scripts/Interaction/Conditions/UnlockStatesOnInteractSucceeded.cs(交互成功后解锁目标)Assets/_Project/Scripts/Interaction/InteractUnlockState.cs(解锁状态源,可驱动外观显示/隐藏;可选参与门禁校验并抛出状态事件)
- 玩家交互入口已接入条件校验:
Assets/_Project/Scripts/Player/PlayerController.cs - 使用方式(Inspector 配置):
- 在需要“道具限制”的可交互物体上(或其父物体上)挂载
RequireItemsInteractCondition - 若只需要单道具:
mode=RequireAll+requirements仅配置 1 条(requiredItemId填ItemData.id) - 如需“用掉钥匙/道具”,勾选
consumeOnSuccess - 如需“一次解锁后永久放行(本次运行内)”,勾选
unlockAfterSuccess- 解锁后不会再消耗道具,也不会再校验背包数量
- 可选绑定
unlockState指向InteractUnlockState,将解锁状态暴露给后续 UI/渲染系统
- (可选)设置
failReason作为失败提示文本
- 在需要“道具限制”的可交互物体上(或其父物体上)挂载
补充文档
- 条件交互与消耗型交互的配置与语义说明见:
Assets/_Project/Docs/Interaction_条件交互与消耗型交互.md
条件组件建议的能力范围(不写代码,只定职责)
最小可用版本(MVP):
- 配置“需要某个物品(按
id)与数量” - 返回“是否允许互动”
- 返回“失败原因文本”(例如“需要钥匙”“需要撬棍”)
进阶版本(可逐步加入):
- 是否消耗道具(成功互动后扣除数量)
- 多条件组合(需要 A 与 B / 需要 A 或 B)
- 关联剧情/关卡状态(如某个 Flag/QuestState)
- 互动成功后触发事件(UnityEvent),用于开门同时触发音效/任务推进等
备选方案 1:直接扩展 IInteractable(不推荐作为第一步)
做法:把 IInteractable 升级为 CanInteract(Interactor) / TryInteract(Interactor) 这种形式,让每个交互脚本自己判断并返回失败原因。
缺点:
- 需要修改所有实现
IInteractable的脚本,未来新增交互也必须实现这些判断。 - 条件逻辑会散落在各个 Controller 中,不利于复用与统一配置。
适用场景:项目规模很小、交互类型很少,并且每个交互的条件都高度定制且不可复用。
备选方案 2:新增“InteractionSystem(玩家侧系统)”作为中枢(可选)
做法:把 Raycast、提示、条件判断、调用 Interact 等集中到一个 InteractionSystem(挂在玩家上),PlayerController 只负责移动/视角。
优点:玩家控制与交互解耦更清晰;但这属于结构重构,适合你准备做完整交互 UI 与复杂逻辑时再做。
最终建议(可执行结论)
- 结论:建议新增一个“互动条件校验组件脚本”,并在
PlayerController的交互入口处统一调用它;现有行为脚本只保留行为,不要塞入道具校验。 - 落地策略:先做“需要某个物品ID才能互动 + 失败提示”,跑通全链路;再逐步扩展为“消耗、组合条件、剧情状态、UI 提示”。