Devlog #00:任务系统的设计与实现 Part1
0 前言
请先思考一个这样的问题:如果现在要让你为某个游戏设计一个任务系统,你会怎么做?
这个问题从表面上看可能非常简单。那无非就是加载任务、看玩家做了什么然后更新这些任务,再做点什么花里胡哨的UI特效,对吧?对...对吗?
这篇文章将会尝试回答这个问题,并记录一下我为这个问题给出的答案。
叠甲:仅作为思路分享,并不保证完全正确 / 高效率 / ...
1 需求收集
那么,先让我们来想一想一个任务系统都有哪些职责吧。只有我们明确了任务系统需要做什么,才能开始设计它。
1.1 任务的储存和读取
首先肯定是储存和读取任务嘛。一个任务系统总得先把任务读取进来才能管理它们。
那么问题来了:具体的任务数据应该怎么存储呢?一个任务里面有那么多描述、数据、关系、要求,怎么才能让它们以一种结构化的方式存在我们的游戏里呢?
这个时候就要请出这方面的专家了:JSON格式。
让我们看看大D老师怎么说:

JSON刚好能够满足我们的要求:既能被程序结构化解析,人也能轻松的看懂。那么我们就用JSON来存储任务吧。
1.2 任务的结构
现在我们解决了“怎么存”这个问题,那自然下一个问题就是“存什么”了。一个任务里面有什么呢?
不妨让我们看看其他游戏是怎么做的。
地狱潜兵2的任务HUD,显示了需要先完成的次要任务和后完成的主要任务
赛博朋克2077的任务HUD,显示了当前任务和这个任务当前阶段的所有目标
我们可以大致总结一下这些任务的共性:
- 一个 任务 由 多个任务阶段 组成。
- 每个 任务阶段 中都存在一系列需要玩家完成的 目标。
- 每个目标都有 目标描述 和 完成条件。
- 当完成一定的目标以后,任务从 一个阶段进入下一个阶段。
- 当完成了任务的 所有阶段以后,这个任务就算完成了。
- 一个任务可以拥有一定的 前提条件。只有当 前提条件 满足一定的规则时,这个任务才能被玩家接受(处于“解锁状态”)。
- 一个任务还可以拥有一个 接取条件,只有当满足这个接取条件时,任务才算真正的被玩家“接受”了。
上述两种条件的区别可能会有点让人困惑。举一个例子:
一个“清剿某地区怪物”的任务可能需要玩家满足 “到达等级10” 和 “探索过XX地区” 这两个条件,才会在当地的冒险家协会NPC头顶上 显示有可以接取的任务 。 当玩家和这个NPC 对话结束 ,这个任务才正式的被玩家所接受。
在这个例子里,“到达等级10”和“探索过XX地区”就是这个任务的 前提条件 ,“和冒险家协会NPC完成关于城郊怪物的对话”就是这个任务的 接取条件 。
到这里,任务的 内容 部分我们已经总结的差不多了。但是为了让我们的任务系统能够正确的读入、分类和管理这些任务,我们还需要问自己几个问题:
- 有没有什么简单可靠的方法来 区分和辨别不同的任务 呢?
- 是不是会有主线任务、支线任务、委托任务等 任务种类 的区别呢?
为了回答上面这两个问题,我们可以再为任务添加两个属性:
- 每个任务都有自己唯一的 任务ID。这样,只要通过判断ID,我们就能辨别不同的任务了。
- 每个任务都有一个 任务类型。
那么,现在我们的任务结构就变成了这样:
- 任务
- 任务ID(必需)
- 任务类型(必需)
- 前置条件(多个,可选)
- 接取条件(一个,可选)
- 任务阶段(一个或多个,必需)
- 本阶段的任务目标
- 目标描述
- 完成条件
- 本阶段的完成条件
- 本阶段的任务目标
现在,让我们来试着定义一下一个任务的JSON应该是什么样的吧。
{
"QuestID": "任务ID",
"QuestType": "任务类型",
"QuestInfo": { // 任务本身的相关信息
"QuestTitle": "任务标题",
"QuestDescription": "任务描述"
},
"QuestConditions": [
{
"ConditionID": "前置条件ID",
// 前置条件的判断方式
}
],
"QuestConditionEvaluation": "任务前置条件的满足规则", // ???
"QuestObjectiveGroups": [ // 任务的不同阶段,这里取名为“目标组”更加直观
{
"Objectives": { // 这个目标组中的所有目标
"ObjectiveID": "目标ID",
"ObjectiveDescription": "目标描述",
// 目标的判断方式
},
"ObjectivesEvaluation": "当前任务阶段的满足规则" // ???
}
]
}
这里我们又会碰到两个问题:
- 前置条件的判断方式 和 任务的判断方式 应该怎么定义?目标的内容千变万化,我们又怎么把这么多变的目标写成一个结构呢?
- 任务前置条件的满足规则 和 任务阶段的满足规则 应该怎么定义?
这里可能也有点不太明确,让我们来举一个例子:
假设进入最终决战需要:
- 完成“打败不死人将军”或者“召唤恶灵”两个任务其一;
- 角色等级达到100级;
- 角色身上不能有“不洁”这个属性。
这种“前置条件之间的关系”就是 “任务前置条件的满足规则”;“任务阶段的满足规则”也是同理。
1.3 拆解“条件”
上面提到的第一个问题,其实我们可以抽象成一个根本的问题:如何把一个“条件”拆解成一个可序列化的结构。
有些什么条件呢?我们可以列举一下:
- 对于玩家角色自身属性的要求(如,等级)
- 需要玩家持有某个物品(如,“秘密房间的钥匙” / “逝去英雄的佩剑” / ...)
- 需要玩家完成某个前置任务
- 需要玩家进行某个互动(如“打开墙上的电闸”)
- 需要玩家完成某个对话 / 剧情过场(如“和老铁匠交谈”)
- 需要玩家击杀指定的NPC (如“杀死3个史莱姆”)
可以发现,这些条件都可以被抽象成一种 “做什么”+参数的形式。以上面列举的为例:
- 要求玩家等级 >= 5
等级, 5
- 要求玩家持有3个“失落的残片”
拥有物品, 失落的残片, 5
- 需要玩家完成某个前置任务
完成任务, <任务ID>
- 需要玩家打开墙上的电闸
场景互动, 电闸
- 需要玩家和老铁匠交谈
完成对话, 老铁匠, <对话ID>
- 需要玩家杀死3个史莱姆
杀死敌人, 史莱姆, 3
那么,我们就可以把“条件”抽象成一个这样的结构:
{
"QuestID": "任务ID",
"QuestType": "任务类型",
"QuestInfo": { // 任务本身的相关信息
"QuestTitle": "任务标题",
"QuestDescription": "任务描述"
},
"QuestConditions": [
{
"ConditionID": "前置条件ID",
"ConditionType": "条件类型",
"ConditionParams": {
"参数1": "参数1的值",
"参数2": "参数2的值"
// ...
}
}
],
"QuestConditionEvaluation": "任务前置条件的满足规则", // ???
"QuestObjectiveGroups": [ // 任务的不同阶段,这里取名为“目标组”更加直观
{
"Objectives": { // 这个目标组中的所有目标
"ObjectiveID": "目标ID",
"ObjectiveDescription": "目标描述",
"ObjectiveType": "目标类型",
"ObjectiveParams": {
"参数1": "参数1的值",
"参数2": "参数2的值"
// ...
}
},
"ObjectivesEvaluation": "当前任务阶段的满足规则" // ???
}
]
}
1.4 拆解“满足规则”
现在,我们解决了“条件”的问题,那么“满足规则”的问题又该怎么解决呢?
让我们再回到上面提到的例子:
进入最终决战需要:
- 完成“打败不死人将军”或者“召唤恶灵”两个任务其一;
- 角色等级达到100级;
- 角色身上不能有“不洁”这个属性。
按照上一节的内容,这个要求可以拆分成四个条件:
- A:
完成任务, 打败不死人将军 - B:
完成任务, 召唤恶灵 - C:
等级, 100 - D:
属性, 不洁
那么最终的条件就可以抽象成
“A或者B选一个,并且需要C,并且不能有D”。
猜你在找:布尔逻辑
让我们来问问大D老师:






这不就是一个布尔逻辑的表达式嘛!有了这个,我们就可以把上面那个条件表达成这样一个简单的式子:
(A || B) && C && !D
把ABCD字母替换成对应的条件ID,我们现在就可以明确的定义任意复杂的满足规则了。
让我们来完善一下我们的任务JSON:
{
"QuestID": "任务ID",
"QuestType": "任务类型",
"QuestInfo": { // 任务本身的相关信息
"QuestTitle": "任务标题",
"QuestDescription": "任务描述"
},
"QuestConditions": [
{
"ConditionID": "前置条件ID",
"ConditionType": "条件类型",
"ConditionParams": {
"参数1": "参数1的值",
"参数2": "参数2的值"
// ...
}
}
],
"QuestConditionEvaluation": "<条件ID组成的的布尔表达式>",
"QuestObjectiveGroups": [ // 任务的不同阶段,这里取名为“目标组”更加直观
{
"Objectives": { // 这个目标组中的所有目标
"ObjectiveID": "目标ID",
"ObjectiveDescription": "目标描述",
"ObjectiveType": "目标类型",
"ObjectiveParams": {
"参数1": "参数1的值",
"参数2": "参数2的值"
// ...
}
},
"ObjectivesEvaluation": "<目标ID组成的布尔表达式>"
}
]
}
目前看起来没有问题。我们就按照这个思路来实现任务系统吧!