简易任务模块设计
任务系统
常规需求分析
任务系统是RPG、模拟经营等游戏引导玩家推进剧情、体验内容的核心系统。以下是这类系统普遍需要覆盖的需求边界。
任务基础属性:唯一id、多语言名称/描述、任务类型(主线/支线/日常/隐藏)、优先级(UI排序)、重复接取上限(0或负数表示无限制)。
任务状态机
1 | NotStarted → InProgress → Completed |
任务接取:支持手动接取(对话/交互触发AcceptQuest(id))和自动接取(游戏内事件匹配后自动触发)。接取前可配置前置条件列表(ConditionManager拦截),接取时可配置事件列表(ActionManager执行)。
任务目标:支持多种目标类型(播放对话、进入场景、收集道具……)且可扩展,目标有进度值和完成状态。多目标支持线性激活(Sequential,完成一个再激活下一个)和并行激活两种模式,每个目标完成时可配置独立事件列表。
任务失败:失败条件只在InProgress期间生效,任务结束后立即解除监听。失败后触发失败事件列表,重置后可重新接取。
任务提交:所有目标完成后触发提交事件列表。奖励、剧情、解锁新任务均通过ActionManager执行,任务模块本身不持有奖励逻辑。
存档:需持久化任务状态、目标进度、已接取次数、当前追踪任务id;读档后必须完整恢复目标事件订阅和失败触发器监听,确保进行中任务行为一致。
UI接口:按状态筛选/排序任务列表、追踪任务进度实时刷新、接取/完成/失败事件通知。
传统实现的痛点
- 目标类型扩展困难:新增类型需要修改核心
switch/if,牵一发而动全身 - 接取逻辑分散:自动接取散落在各系统中形成隐式耦合
- 失败条件泄漏:任务结束后事件监听仍存在,存在错误触发和内存泄漏风险
- 读档后状态不一致:事件订阅是纯运行时状态,读档后未重建则任务卡死
- 自动接取检测效率低:遍历全部配表任务逐一判断,O(n)复杂度
解决思路:配表驱动 + 注册表模式(目标/触发器工厂字典) + Luban后处理器生成反查表(O(1)自动接取检测) + 严格的激活/失活生命周期管理。
模块拆分
| 模块 | 路径 | 职责 |
|---|---|---|
| 核心流程 | QuestManager.cs |
生命周期管理,对外唯一入口 |
| 任务目标 | Objectives/ |
各目标类型实现,继承QuestObjectiveBase |
| 自动接取触发器 | AutoAcceptTriggers/ |
各触发器类型的匹配逻辑,静态方法 |
| 失败触发器 | FailTriggers/ |
各失败条件实现,继承QuestFailTriggerBase |
| 数据模型 | Model/ |
QuestData、QuestObjectiveData,数据序列化 |
配表设计
任务主表(CfgQuest)
配表路径:Luban/Datas/系统_任务.xlsx

| 列 | 说明 |
|---|---|
AutoAeecptType / AutoAeecptParamList |
自动接取触发器类型id及参数,null表示不自动接取 |
AcqCondIdList / AcqActionIdList |
接取前条件 / 接取时执行事件 |
Sequential |
true=线性激活目标,false=并行激活 |
FailType / FailParamList |
失败触发器类型id及参数,null表示无失败条件 |
AcpCntMax |
重复接取上限,0或负数无限制 |
任务目标表(CfgQuestObjective)

| 列 | 说明 |
|---|---|
ShowCount |
是否UI展示进度统计(如”3/5”) |
Type |
目标类型id,对应DefQuestObjectiveType中的常量 |
ObjectiveParamList |
目标参数,格式见程序暴露声明sheet |
程序暴露声明
程序侧在Excel中维护三张##不导出的说明sheet,供策划查阅已注册的类型id及参数格式:
任务目标类型_程序暴露

| id | 描述 | 参数格式 | 参数说明 |
|---|---|---|---|
| 100 | 播放剧情 | string | 对话id |
| 101 | 进入场景 | string | 场景id |
| 102 | 获取灵感道具 | string, string | 道具id, 目标数量 |
自动接取触发器类型_程序暴露

| id | 描述 | 参数格式 | 参数说明 |
|---|---|---|---|
| 100 | 任务提交时 | string | 前置任务id(0=任意任务提交均触发) |
| 101 | 进入场景时 | 无参数 | — |
任务失败触发器类型_程序暴露

| id | 描述 | 参数格式 | 参数说明 |
|---|---|---|---|
| 100 | 离开场景白名单 | string, string, … | 允许停留的场景id列表 |
配置举例
例1:主线任务链,前一个提交后自动接取下一个

任务示例101:主线,,任务100提交后自动触发接取(AutoAeecptType=100, AutoAeecptParamList=100)。
例2:支线任务,进入场景自动接取且有失败条件
任务示例:进入场景后自动接取(AutoAeecptType=101),Sequential=false并行激活所有目标,FailType=100, FailParamList=Scene_Library(只在Scene_Library内有效)。
例3:可重复日常任务
任务示例:AcpCntMax=3,存档内最多接取3次。
程序端扩展
关键设计:自动接取触发器与失败触发器的生命周期完全不同。
- 自动接取触发器:
QuestManager.OnInit()时全局常驻监听游戏事件,因为接取任务前任务 runtime 尚未创建,必须由 Manager 级别的全局监听来驱动AcceptQuest()- 失败触发器:跟随任务 runtime 生命周期,
AcceptQuest()时实例化并激活,任务结束(完成/放弃/失败)时统一失活销毁,事件订阅不超出任务存活期
扩展新的目标类型
以新增击杀敌人目标为例:
第一步:DefQuestObjectiveType.cs 添加常量
1 | public const int KILL_ENEMY = 103; |
第二步:新建 KillEnemyQuestObjective.cs
1 | [] |
RegisterEvents / UnregisterEvents 只写订阅逻辑,基类的_isEventRegistered标志位保证不重复注册;进度通过SetCurrentValue()写入,基类自动检查完成并分发事件。
第三步:QuestManager.QuestObjective.cs 的 RegisterObjectiveTypes() 中注册
1 | RegisterObjective(DefQuestObjectiveType.KILL_ENEMY, KillEnemyQuestObjective.Create); |
最后在程序暴露声明sheet补充类型说明。
扩展新的自动接取触发器
自动接取触发器是无状态的纯函数,只做参数匹配判断,不持有任何运行时状态。QuestManager 全局监听游戏事件,事件触发时通过反查表拿到候选任务列表,再用触发器函数逐一匹配。
以新增获得道具时触发为例:
第一步:DefQuestAutoAcceptTriggerType.cs 添加常量
1 | public const int ITEM_COLLECTED = 102; |
第二步:新建 QuestTriggerOnItemCollected.cs
1 | public static class QuestTriggerOnItemCollected |
第三步:QuestManager.AutoAcceptTrigger.cs 的 RegisterAutoAcceptTrigger() 中注册
1 | Register(DefQuestAutoAcceptTriggerType.ITEM_COLLECTED, (TriggerMatch<string>)QuestTriggerOnItemCollected.AutoAcceptTrigger); |
第四步:QuestManager.Event.cs 监听对应事件并调用 CheckAutoAcceptQuests
1 | // RegisterEventListeners() 中: |
最后在程序暴露声明sheet补充说明,重新运行gen.bat更新反查表。
扩展新的失败触发器
失败触发器是有状态的实例对象,随任务接取时创建、任务结束时销毁,事件订阅严格限定在任务存活期内。实例会随 QuestData 一起序列化存档,读档后通过 OnReload() 恢复事件监听。
以新增计时超时失败为例:
第一步:DefQuestFailType.cs 添加常量
1 | public const int TIME_LIMIT = 101; |
第二步:新建 QuestFailTriggerOnTimeLimit.cs,继承 QuestFailTriggerBase
1 | [] |
第三步:QuestManager.QuestFail.cs 的 RegisterQuestFailTrigger() 中注册
1 | RegisterFailType(DefQuestFailType.TIME_LIMIT, QuestFailTriggerOnTimeLimit.Create); |
内部实现
任务生命周期
1 | AcceptQuest(questId) |
目标事件订阅生命周期
QuestObjectiveBase中的_isEventRegistered标志位严格管控订阅状态:
1 | OnActivated() / OnReload() → if(!_isEventRegistered) → RegisterEvents() → flag=true |
所有结束路径(完成/放弃/失败/退出)均通过OnDeactivated()统一取消订阅,所有激活路径(正常接取/读档恢复)通过OnActivated()/OnReload()统一注册,标志位兜底防止重复注册。
自动接取反查表优化
Luban后处理器在导表阶段预生成DefQuestAutoAcceptTypeReverse,将”触发器类型 → 任务id列表”固化为静态只读字典:
1 | // 由Luban后处理器自动生成,禁止手动修改 |
运行时 CheckAutoAcceptQuests(triggerType, ...) → GetValues(triggerType) O(1)取候选列表 → 逐一做参数匹配。相比遍历全表的O(n),k(该触发器关联的任务数)远小于n,且反查表在导表阶段已构建,运行时零额外开销。
存档与读档
存档内容:_questDict(任务状态、目标进度、运行时目标对象实例、失败触发器实例、已接取次数)和TrackedQuestId。运行时对象直接序列化,读档后无需按配表重建,保留运行时已积累的中间状态。
读档流程:
1 | 1. DeactivateAll(取消全部订阅,清理上次状态) |
注意事项
AcpCntMax <= 0视为无限制;任务失败时AcceptedCount自动-1,失败后仍可重新接取不超限Sequential=true时,ObjectiveList的填写顺序即为目标推进顺序




