客户端程序员视角下的任务系统设计
核心目标:设计稳定、高效、可维护的任务系统,确保数据规则与逻辑意图能够准确传播给所有协作者
一、什么是任务
广义理解
游戏是对世界的抽象,任务就是这个抽象世界里玩家出生、成长、发挥影响力过程中一个又一个的普世目标和存在意义。
- 一方面,玩家不得不完成它
- 另一方面,玩家完成它又会获得这个抽象世界的普世认可
狭义理解(RPG游戏)
RPG游戏是一个故事,任务就是故事中一条或多条故事线上的**(时间、事件、状态)节点**,用于:
- 驱动故事发展
- 记录玩家历程
任务系统的核心功能
基于从业经验归纳,任务系统本身及相关联系统对任务提出的核心要求:
| 序号 | 核心功能 | 说明 |
|---|---|---|
| 1 | 关键时间点记录 | 记录玩家行为数据,标记游戏进程 |
| 2 | 状态管理 | 未接取、进行中、已达成、已结束、已失败 |
| 3 | 上下文连续性 | 链式结构支持连续性,最好支持分支(树形结构) |
| 4 | 多类型条件组合 | 支持逻辑关系(AND、OR)运算和优先级(括号) |
| 5 | 多种条件检查方式 | 改变任务状态的不同触发机制 |
| 6 | 多种筛选规则 | 用于分类、显示、统计等 |
| 7 | 多种文本表现形式 | 适应不同场景的表现需求 |
二、任务的逻辑分类
基于当前游戏任务系统,从逻辑层面归纳以下分类:
mindmap
root((任务分类))
剧情任务
故事线的核心元素
驱动游戏主线发展
指引任务
剧情中的一部分
建立角色能力认知
游戏本身的目标引导
日常任务
短小重复
量变产生质变
易于达成的小目标
平台期目标
类似日常但跨度更长
需要几天完成
具有方向性指导
随机任务
枯燥世界中的小巧合
增加探索新鲜感
合作任务
群体参与改造世界
增加玩家间互动
分类说明
| 类型 | 特点 | 设计目的 |
|---|---|---|
| 剧情 | 核心元素 | 推动故事发展 |
| 指引 | 复杂,可内嵌剧情 | 建立玩家对游戏机制的认知 |
| 日常 | 短小重复 | 提供可达成的小目标 |
| 平台期目标 | 跨度数天 | 提供长期方向性指导 |
| 随机任务 | 偶然触发 | 增加探索新鲜感 |
| 合作任务 | 多人参与 | 增强玩家间互动与参与感 |
三、任务系统的关联系统
以下系统直接或间接与任务系统有关联:
flowchart TB
subgraph 核心系统
Quest[任务系统]
end
subgraph 直接依赖
Guide[指引和解锁系统]
Explore[探索/调查/对话]
Achievement[成就系统]
end
subgraph 功能耦合
Monster[怪物召唤]
NPC[NPC显隐]
Animation[动画播放]
end
subgraph 表现层
Text[进度文本]
Nav[目标导航]
Highlight[NPC高亮]
end
subgraph 其他
Robot[主角助手]
Activity[活动管理器]
end
Quest <-->|双向关联| Achievement
Quest -->|时间点/进度| Guide
Quest -->|记录进度| Explore
Quest -->|任务中配置| Monster
Quest -->|剧情状态| NPC
Quest <-->|状态相互利用| Animation
Quest -->|耦合配置| Text
Quest -->|耦合配置| Nav
Quest -->|耦合配置| Highlight
Quest -->|时间点驱动| Robot
Quest <-->|相互引用| Activity
关联系统详解
| 系统 | 关联方式 | 存在问题 |
|---|---|---|
| 指引和解锁 | 依赖任务的时间点和进度记录能力 | — |
| 探索/调查/对话 | 需要进度记录和奖励能力 | 对话系统与任务对话令人困惑 |
| 成就 | 双向关联,理论上不应独立 | 原任务配置过于复杂导致分离 |
| 怪物召唤 | 任务中直接配置 | 其他玩家看到”与空气战斗”的怪异行为 |
| NPC显隐 | 为剧情状态服务 | — |
| 动画播放 | 相互利用状态与时间点 | 断线重连时状态基准冲突 |
| 进度文本/导航/高亮 | 耦合到任务配置 | — |
| 主角助手 | 以任务时间点驱动行为 | — |
| 活动管理器 | 相互引用 | 任务配置引用活动,活动触发任务 |
四、遇到的问题与困难
4.1 潜规则(最致命问题)
“来吧老哥,先这么搞,你加个默认规则不就行了么!”
“改一个字段不是还要全部刷数据么,单独加个判断特殊处理一下!”
“先跑起来!“
典型案例:任务目标显示规则
早期实现:
| 目标类型 | 默认显示规则 |
|---|---|
| 杀怪 | ”杀怪某某 x/n” |
| 物品 | ”收集物品某某 x/n” |
| … | … |
| 任务完成 | ”找到某某某” |
问题演化:
- 早期只有 3-4 条目标类型
- 项目发展后膨胀到 8-9 条目标类型
- 需要自定义格式化字符串
- 默认规则 + 自定义逻辑并行运行
最终爆发:
打包前夜报错 → 默认显示规则引用怪物包数据 → 怪物包没有名字定义 → 进不去游戏
根本原因:
- 怪物配置升级为怪物包配置时,使用自定义格式未暴露问题
- 使用默认规则时触发空引用错误
💡 教训:如果开始就按照最原子的逻辑实现,返工与Bug会少很多。
4.2 空白逻辑持续发挥余热
现象
- 2017年9月编写的逻辑,2019年4月仍未使用
- 大量空白字段被所有任务携带,但只有极个别任务使用
具体例子
| 字段 | 状态 |
|---|---|
| 接取条件:玩家温度上下限 | 从未使用 |
| 接取时需求技能id数组 | 从未使用 |
| 大部分空白逻辑字段 | 极少数任务使用 |
带来的问题
- 占用屏幕空间 - 打开配置表需要不断按右方向键寻找关键列
- 代码冗余 - 仅接取条件判断就写了 200+ 行
- 空引用错误 - 老数据缺少新条件项
- Debug困难 - 配置同学、程序同学信息不完整
4.3 完全手动的跨表引用
典型案例:提交任务配置
任务配置表
↓ 配置完成提交NPC
提交服务配置
↓ 加到NPC服务组件
NPC服务组件
↓ 填到NPC表
NPC表
以上步骤缺一不可,否则分分钟报错。
高耦合问题
| 场景 | 影响 |
|---|---|
| 地编重新种NPC并导出 | 其他任务表要同步修改位置 |
| 区域触发任务 | 区域信息中记录,任务配置表还要记录区域id |
文本信息跨表混乱
| 文本类型 | 处理方式 |
|---|---|
| 任务目标 | 游戏程序逻辑分别处理 |
| 任务说明 | 配置表导出工具替换 |
| 对话文本 | 未处理 |
风险:某系统元素改名 → 显示问题或额外工作量
4.4 导出数据与数据源不一致
分类一:导出落后于Excel
现象:游戏内表现与期望不一致,Excel数据源没问题,但导出数据有差别
多发场景:多人合作中,某人修改内容未及时同步导出数据
直接影响:打包节点报错,需等下一个包才能继续工作
分类二:数据结构演化未全刷表
原因:最后修改数据结构的同学对全刷表正确性没信心
背景:
- 几千条数据
- 数个同学在一两个月版本迭代中陆续增删改
- 部分数据因功能、测试环境等改变而无效/过期
风险:
- 承担巨大核对工作量
- 或承担出包后数据错误的责任风险
集合对应不合理
| 数据源集合 | 导出数据集合 |
|---|---|
| 正式数据 | 正式数据(理想) |
| 测试数据 | 测试数据(开发需要) |
| 废弃数据 | ❌ 废弃数据(版本演化残留) |
| 开发中数据 | ❌ 不在数据源中的数据 |
缺失:数据源中缺乏数据类别管理,导出数据没有一对一映射方式
4.5 链式依赖
典型案例
任务A完成条件 → 依赖成就B达成
成就B达成条件 → 依赖任务C/D/E/F...完成
难点
如何显示任务A的进度?
| 方案 | 问题 |
|---|---|
| 显示”需要达成某成就” | 需另跨页面查看成就进度 |
| 直接显示成就进度 | 与原逻辑不合,通用性不强 |
4.6 任务的多个接取时机
被动条件(轮询)
| 条件类型 | 说明 |
|---|---|
| 玩家等级、声望、成就 | 基础属性检查 |
| 游戏内时间、昼夜 | 时间相关 |
| 前置、互斥条件 | 任务关系 |
触发方式:NPC携带任务,玩家交互时轮询
主动触发
| 触发方式 | 说明 |
|---|---|
| 进入区域 | 区域触发 |
| 活动开启 | 活动触发 |
组合问题
案例:进入区域 + 只有夜晚才能接取
问题场景:
- 玩家白天进入区域
- 挂机等到晚上
- 原本被动的接取条件需要考虑主动触发
风险:放开任意组合,补丁逻辑将非常复杂
4.7 断线以后的连续性
问题场景
任务完成条件:观看指定动画
正常流程:
进入区域 → 接取任务 → 播放动画 → 动画完成 → 完成任务
断线后问题:
- 任务已在玩家身上 → 不会播放接取动画
- 没播放指定动画 → 无法完成需要观看动画的任务
死循环
解决方案
进入区域 → 接取任务
离开地图 → 放弃任务
任务接取 → 播放动画 → 动画完成 → 完成任务
原理:
- 掉线 = 离开地图 → 放弃任务
- 上线 = 首次进入区域 → 重新接取 → 完整播放动画
遗留问题:
- 其他控制动画的逻辑(区域)
- 断线后的位置同步问题
- 逻辑冲突
五、优化设计方案
5.1 任务状态与转移条件
状态定义
stateDiagram-v2
[*] --> 未接取
未接取 --> 进行中: 接取条件满足
进行中 --> 已达成: 达成条件满足
进行中 --> 已失败: 失败条件满足
已达成 --> 已结束: 提交条件满足
已达成 --> 进行中: 回退达成条件
已失败 --> [*]
已结束 --> [*]
未接取 --> [*]: 放弃(隐藏状态)
状态说明
| 状态 | 说明 |
|---|---|
| 未接取 | 任务存在但玩家未接取 |
| 进行中 | 玩家已接取,正在完成 |
| 已达成 | 完成条件满足,等待提交 |
| 已结束 | 任务完成,流程结束 |
| 已失败 | 任务失败,流程结束 |
| 已放弃(隐藏) | 等同于未接取 |
状态转移触发机制
| 转移 | 触发时机 |
|---|---|
| 未接取 → 进行中 | ”接取时机”触发 |
| 进行中 → 已达成 | ”达成条件”满足 |
| 进行中 → 已失败 | ”失败条件”满足 |
| 已达成 → 已结束 | ”提交条件”满足 |
| 已达成 → 进行中 | ”回退达成条件”(特殊) |
注:省略”任务更新”时机,改为监听各个”条件更新”
5.2 任务之间的组织关系
树形结构
graph TD
A[主线任务: 启程] --> B[子任务: 收集物资]
A --> C[子任务: 寻找向导]
B --> D[子子任务: 购买工具]
B --> E[子子任务: 采集草药]
C --> F[分支: 选择向导A]
C --> G[分支: 选择向导B]
style A fill:#e3f2fd
style F fill:#fff3e0
style G fill:#fff3e0
树形结构的优势
| 优势 | 说明 |
|---|---|
| 连续上下文 | 父子任务提供逻辑连续性 |
| 分支能力 | 兄弟任务提供额外分支 |
| 分组逻辑 | 无需额外的”任务包”配置表 |
注意事项
- 需要完善的配表工具
- 否则配置相对吃力
5.3 条件系统设计
当前系统的条件数量
| 条件类别 | 数量 | 说明 |
|---|---|---|
| 接取类型 | 11 | — |
| 接取条件 | 16+ | 与接取类型组合 |
| 完成/提交条件 | 15 | 最多3条,AND/OR逻辑 |
| 失败条件 | 1 | 超时失败 |
| 重做条件 | 1 | 限次包 |
| 放弃条件 | 2 | 手动放弃、离开地图 |
| 成就首次达成 | 10 | 升级、创建公会等 |
| 成就积累型 | 48 | 收集物品、杀怪等 |
⚠️ 这些类型在可预见的将来还会不可控制地迅速膨胀
条件抽象方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 完整脚本(Lua) | 非常灵活 | 配置难度大,依赖关系难整理 |
| DSL/表达式 | 兼顾可读性与表达性 | 需要解析程序,触发类型条件难处理 |
| 类型枚举+参数 | Excel配置方便 | 逻辑组织局限(默认AND) |
推荐方案:条件配置表 + 逻辑表达式
设计思路:
// 任务数据
{
"2001": {
"accept_condition": "(010001 and 010002) or 010003",
"complete_condition": "xxxxxx"
}
}
// 条件数据
{
"010001": {"type": 1, "param_1": 10},
"010002": {"type": 2, "param_1": 11, "param_2": 7},
"010003": {"type": 0, "param_1": 5, "param_2": 1010101}
}
解析方式:
class Condition {
virtual bool Match(param);
};
class AND : public Condition {
virtual bool Match(param) {
return left.Match(param) && right.Match(param);
}
};
// 解析表达式树
Condition* condition = Parse("(010001 and 010002) or 010003");
if (condition->Match(param)) {
// 状态转移
}
客户端与服务器的不对称性
| mask | 定义 |
|---|---|
| 0x00 | 没有忽略 |
| 0x01 | 服务器忽略,返回 False |
| 0x02 | 服务器忽略,返回 True |
| 0x04 | 客户端忽略,返回 False |
| 0x08 | 客户端忽略,返回 True |
示例:
"010004": {
"type": 0,
"param_1": 5,
"param_2": 1010101,
"ignore": "0x06" // 服务器和客户端都忽略
}
5.4 条件的检查方式
flowchart LR
A[条件检查] --> B{检查方式}
B -->|定期执行| C[轮询]
B -->|事件驱动| D[触发]
C --> C1[逻辑简单]
C --> C2[不适合大量检查]
D --> D1[逻辑复杂]
D --> D2[功能灵活]
style C fill:#fff3e0
style D fill:#e3f2fd
检查方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 轮询 | 逻辑简单 | 不适合大量检查 | NPC交互任务 |
| 触发 | 功能灵活 | 逻辑复杂 | 区域、活动触发 |
当前系统的检查方式分布
| 任务类型 | 检查方式 |
|---|---|
| NPC身上交接的任务 | 轮询 |
| 区域、活动触发的任务 | 触发 |
| 进行中的达成条件 | 触发 |
⚠️ 警告:混乱地不加定义地使用不同检查方式,会遇到解决不完的问题
5.5 条件计数(执行进度)
计数数据的存在位置
| 类型 | 处理方式 |
|---|---|
| 需要记录过去触发状态 | 服务器更新计数 |
| 客户端无对应数据 | 服务器更新计数 |
| 客户端可自己判断 | 本地更新计数 |
设计建议
- 明确定义哪些条件需要服务器计数
- 哪些条件客户端可以本地处理
- 状态变化可与其他类型条件复用
5.6 状态切换时的行为
建议整合的行为
| 行为 | 当前实现 | 建议整合 |
|---|---|---|
| 任务完成后接取新任务 | 默认行为 | ✅ 保留 |
| 任务完成后没收物品 | 明确定义 | ✅ 保留 |
| 发奖 | 单独规定 | 🔧 整合到状态切换行为 |
整合发奖逻辑的优势
- 逻辑表达更顺畅
- 避免程序开天窗写特例
- 支持任务分支逻辑
- 完成后走分支A
- 失败后走分支B
5.7 相似功能的整合
对话系统
当前问题:
| 功能 | 问题 |
|---|---|
| 对话 | 单独记录进度,散乱逻辑 |
| 任务对话 | 接取前/完成前/完成后显示 |
| 合并问题 | 两个UI可能叠在一起 |
建议方案:
flowchart TD
A[统一对话系统] --> B[定义分支行为]
B --> C{任务状态}
C -->|接取前| D[显示接取对话]
C -->|进行中| E[显示未完成对话]
C -->|完成后| F[显示完成对话]
D --> G[触发任务接取条件]
F --> H[触发任务完成]
核心思想:
- 任务是玩家游戏进程的条件与状态转移链
- 对话引用玩家任务的完成状态
- 避免逻辑与表现混在一起
成就系统
当前问题:
- 成就与任务双向关联
- “完成任务A,B,C…时完成成就D”
- “完成成就D时完成任务E”
- 车轱辘逻辑,程序开天窗打补丁
建议方案:
成就是一系列特殊的任务:
| 方案 | 实现 |
|---|---|
| 静态配置 | 角色创建后天生携带的任务id |
| 动态配置 | 达到某阶段接取某系列任务 |
显示:只是状态的包装,不应与逻辑冲突
动画播放
建议:归纳到任务状态切换时的行为
- 特定时间
- 特定区域
- 播放行为
- 包括动画完成传送等功能
断线问题:通过粒度更小的任务逻辑节点,保存动画播放进度
5.8 任务的显示与包装
任务标签(Tag)
设计目的:解决”它是什么也是什么”的多维度分类问题
{
"id": 2001,
"tag": ["Secondary", "Story", "Visable", "Night"],
"comment": "玩家看得见的夜间才能完成的支线剧情任务"
},
{
"id": 2002,
"tag": ["Trophy", "Stage1"],
"comment": "玩家在阶段1的成就任务"
}
对比:
| 方式 | 类比 |
|---|---|
| 任务类型 | QQ朋友分组(单一维度) |
| 任务标签 | 微信标签(多维度) |
剧情信息
建议:任务不应包含显示文本信息
| 原因 | 说明 |
|---|---|
| 1 | 大量功能任务不需要文本,避免浪费 |
| 2 | 单独设计文本树,任务状态控制分支 |
| 3 | 解耦表现与底层逻辑 |
进度文本与导航
当前配置问题:
{
"id": 2001,
"commit_npc": 10000,
"commit_info": {"pos": "x,y,z", "map_id": 11},
"commit_comment": "balabala",
"complete_condition": [
{
"target_comment": "balabala",
"target_param_xxx": 1,
"target_info": {"pos": "x,y,z", "map_id": 11, ...}
}
]
}
理想配置:
{
"id": 2001,
"comment": "balabala",
"map_id": 11,
"pos": "x,y,z",
"npc_ids": [10001]
}
优势:
- 导航配置可给其他系统使用(资源收集导航)
- 通过任务树包装表现任务
六、总结
6.1 核心优化方向
mindmap
root((优化方向))
明确状态定义
5个关键状态
7种转移条件
清晰的触发机制
树形组织结构
父子提供连续性
兄弟提供分支
减少额外配置表
独立条件配置
条件配置表
逻辑表达式
支持AND/OR
统一检查方式
明确定义轮询/触发
避免混合使用
整合相似功能
对话统一
成就即任务
动画纳入状态行为
标签化显示
多维度分类
解耦逻辑与表现
6.2 关键原则
| 原则 | 说明 |
|---|---|
| 明确定义 | 状态、条件、检查方式都要有清晰定义 |
| 缩减整合 | 合并重叠功能,减少配置复杂度 |
| 解耦分离 | 逻辑与表现分离,降低耦合度 |
| 工具支持 | 持续推进有效的配置程序插件 |
6.3 关于数据与视图
真实存储的数据与看到的数据不一定一致
- 数据库:3范式下的无冗余关系表
- 终端用户(策划):关联过的视图表
- 进一步拓展:针对不同策划角色的专用视图
- 剧情策划:条件、文本、导航、奖励、父子节点
- 成就策划:简单条件与初始化接取
- 关卡策划:动画播放与进度记录时机