7.5 KiB
TaskSystem - 触发器与流程节点方案
本文档描述“任务的添加/删除/状态切换”在项目中的推荐落地方式:通过独立脚本组件(触发器/动作节点)把 TaskSystem 接入任意玩法事件、流程节点或 Timeline/动画/对话节点,而不是把逻辑写死在某个互动脚本里。
适用范围:
- 任务不一定来自交互(门、拉杆等),也可能来自剧情节点、进入区域、时间到、对话选项、成就/教程步骤等。
- 希望策划/关卡在 Inspector 中配置“触发条件 → 任务动作”,尽量减少硬编码与跨模块耦合。
相关基础系统:
Assets/_Project/Scripts/Core/TaskSystem/TaskService.csAssets/_Project/Scripts/Core/TaskSystem/TaskData.csAssets/_Project/Docs/TaskSystem_Technical_Reference.md
1. 总体思路
将任务系统的“动作”拆成可复用的独立组件:
- 动作基类(
GameAction):定义统一的public override void Invoke()接口。 - 动作类(Task Actions):继承自
GameAction,只做一件事,调用TaskService的某个 API。- AddTaskAction
- RemoveTaskAction
- SetTaskStatusAction
- 触发类(Task Triggers):监听某个来源事件,满足条件时调用动作(通过 UnityEvent 绑定
Action.Invoke())。- TriggerVolume (进入区域触发)
- InteractFailedTrigger (交互失败触发)
这样可以保证:
- 任务系统逻辑(添加/移除/状态规则)仍然集中在
TaskService,只通过 API 进入 - 具体玩法/流程只负责“何时触发”,不负责“如何维护任务”
- 同一套任务动作能被任何系统复用(UnityEvent、Timeline、Trigger、脚本回调都能接)
2. 推荐的“任务动作”组件清单(未来要做的脚本)
以下是建议新增的脚本(都应放在 _Project/Scripts/Core/TaskSystem/Actions 或 _Project/Scripts/UI/Tasks/Actions,按你们组织习惯即可),每个脚本尽量只有一个公开入口方法,便于被 UnityEvent 调用。
2.1 AddTaskAction
用途:向任务系统添加一个任务(可选超时秒数)。
Inspector 槽位:
TaskService taskService(槽位引用;不写死单例)TaskData task(要添加的任务定义)float timeoutSeconds = -1(<0 表示不超时)bool addOrUpdate = true(推荐默认 true:避免重复添加失败)bool resetStatusToTodo = false(可选:用于“重复触发时重置为待做”)
公开方法(示例概念):
public void Invoke()
内部应调用:
taskService.AddTask(task, timeoutSeconds)或taskService.AddOrUpdateTask(task, timeoutSeconds, resetStatusToTodo)
2.2 RemoveTaskAction
用途:按任务 ID 移除任务(建议用 TaskData 引用获取 id,避免手写字符串)。
Inspector 槽位:
TaskService taskServiceTaskData task
公开方法:
public void Invoke()
内部应调用:
taskService.RemoveTask(task.id)
2.3 SetTaskStatusAction
用途:把某个任务置为 Completed / Expired / Todo。
Inspector 槽位:
TaskService taskServiceTaskData taskTaskStatus status
公开方法:
public void Invoke()
内部应调用:
taskService.SetTaskStatus(task.id, status)
3. 推荐的“触发器”组件清单(未来要做的脚本)
触发器组件的核心原则:
- 只监听事件 + 过滤条件 + 调用一个或多个动作
- 尽量支持 UnityEvent:让关卡/流程可以在 Inspector 里做绑定
- 必须提供“防重复触发”的选项(例如只触发一次、冷却时间、按任务是否已存在等)
3.1 OnStartTrigger / OnEnableTrigger
用途:场景开始/对象启用时自动添加任务或设置状态(替代临时的 TaskBootstrap)。
建议用法:
- 教程任务:开场添加“打开背包”“查看平板”等
- 剧情开场:进入房间后添加“寻找钥匙”
防重复建议:
onlyOnce(触发一次后禁用自身)requireTaskNotPresent(任务已存在则不触发)
3.2 InteractFailedTrigger(交互失败触发)
用途:当玩家“按下交互键且条件不满足”时触发任务添加。
关键注意点:
- 不要挂在“瞄准提示”的高频检测上,否则会重复触发
- 必须挂在“按键交互尝试”的失败分支上(目前失败分支在 PlayerController 里)
推荐做法:
- PlayerController 暴露一个“交互失败事件”(UnityEvent 或 C# event)
- 参数至少应包含:目标 collider / 交互者 / failReason
- InteractFailedTrigger 订阅该事件,并做过滤:
- 只关心指定门/指定根物体(hit 是否在该物体层级里)
- 可选:只对某类条件失败生效(例如 RequireUnlockedInteractCondition)
3.3 UnlockStateChangedTrigger(解锁状态变化触发)
用途:当 InteractUnlockState 变为 unlocked 时:
- 自动完成某个任务(例如“寻找钥匙”完成)
- 或移除过期任务
当前工程里 InteractUnlockState 没有事件回调,只有 SetUnlocked/Unlock/Lock 改字段并 Apply 视觉。
推荐改造方向(未来做):
- 在
InteractUnlockState增加event Action<bool> Changed或UnityEvent<bool> onChanged - 或者新增一个 Watcher 组件轮询
IsUnlocked(成本更低但不够优雅)
3.4 TriggerVolume / TimelineSignal / DialogueNode
用途:将任务动作接到“流程节点”上:
- 进入触发区:添加任务
- Timeline Signal:切任务状态
- 对话节点:选择某个选项后添加/移除任务
建议统一入口:
- 这些系统最终都调用
AddTaskAction/SetTaskStatusAction/RemoveTaskAction的Invoke()
4. 配置规范(强烈建议遵守)
4.1 不写死引用
所有动作脚本都应该用 Inspector 槽位引用:
TaskService:拖场景里的 TaskService(或者由上层管理注入)TaskData:拖 ScriptableObject 资产
不要在脚本里用:
- 查找路径
transform.Find("...") GameObject.Find(...)- 用字符串写 taskId(除非迫不得已)
4.2 去重策略(默认建议 AddOrUpdate)
任务添加最常见的坑是“重复触发导致重复添加/报错/刷 UI”。
推荐默认策略:
- 触发器 → 调用
AddOrUpdateTask(taskData, ...) - 如确实需要“只添加一次”:在触发器侧增加
onlyOnce或requireTaskNotPresent
4.3 任务生命周期归属
建议统一归属:
- 任务的超时判定:由
TaskService(已实现) - 任务的自动移除:由
TaskService的 autoRemove 配置(已实现,可关) - 任务的产生与完成:由“触发器 + 动作”拼装(关卡/流程可配置)
5. 示例:门未解锁 → 交互失败 → 添加“寻找钥匙”任务
目标:
- 门上挂了 RequireUnlockedInteractCondition,未解锁时交互失败
- 失败后添加任务
task_find_key
推荐配置:
- Scene:
- TaskSystem(TaskService)
- DoorRoot
- InteractUnlockState(unlocked=false)
- RequireUnlockedInteractCondition(failReason=“尚未解锁”)
- InteractFailedTrigger
- targetFailReason=“尚未解锁”(或留空)
- onInteractFailedEvent -> 拖入下方的 AddTaskAction,选择
GameAction.Invoke()
- AddTaskAction
- taskService -> TaskSystem(TaskService)
- task -> task_find_key(TaskData)
6. 临时测试与正式接入的区别
TaskBootstrap:仅用于快速在场景启动时塞入测试任务(方便 UI 验证)- 正式玩法:应该由“触发器 + 动作”驱动任务变化,避免 Start 时就塞进任务导致误判