Devlog #00:任务系统的设计与实现 Part1

whiteviera2025-09-30

0 前言

请先思考一个这样的问题:如果现在要让你为某个游戏设计一个任务系统,你会怎么做?

这个问题从表面上看可能非常简单。那无非就是加载任务、看玩家做了什么然后更新这些任务,再做点什么花里胡哨的UI特效,对吧?对...对吗?

这篇文章将会尝试回答这个问题,并记录一下我为这个问题给出的答案

叠甲:仅作为思路分享,并不保证完全正确 / 高效率 / ...

1 需求收集

那么,先让我们来想一想一个任务系统都有哪些职责吧。只有我们明确了任务系统需要做什么,才能开始设计它。

1.1 任务的储存和读取

首先肯定是储存和读取任务嘛。一个任务系统总得先把任务读取进来才能管理它们。

那么问题来了:具体的任务数据应该怎么存储呢?一个任务里面有那么多描述、数据、关系、要求,怎么才能让它们以一种结构化的方式存在我们的游戏里呢?

这个时候就要请出这方面的专家了:JSON格式open in new window

让我们看看大D老师怎么说:

json

JSON刚好能够满足我们的要求:既能被程序结构化解析,人也能轻松的看懂。那么我们就用JSON来存储任务吧。

1.2 任务的结构

现在我们解决了“怎么存”这个问题,那自然下一个问题就是“存什么”了。一个任务里面有什么呢?

不妨让我们看看其他游戏是怎么做的。

hd2地狱潜兵2的任务HUD,显示了需要先完成的次要任务和后完成的主要任务

2077赛博朋克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": "当前任务阶段的满足规则" // ???
    }
  ]
}

这里我们又会碰到两个问题:

  • 前置条件的判断方式任务的判断方式 应该怎么定义?目标的内容千变万化,我们又怎么把这么多变的目标写成一个结构呢?
  • 任务前置条件的满足规则任务阶段的满足规则 应该怎么定义?

这里可能也有点不太明确,让我们来举一个例子:

假设进入最终决战需要:

  1. 完成“打败不死人将军”或者“召唤恶灵”两个任务其一;
  2. 角色等级达到100级;
  3. 角色身上不能有“不洁”这个属性。

这种“前置条件之间的关系”就是 “任务前置条件的满足规则”;“任务阶段的满足规则”也是同理。

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 拆解“满足规则”

现在,我们解决了“条件”的问题,那么“满足规则”的问题又该怎么解决呢?

让我们再回到上面提到的例子:

进入最终决战需要:

  1. 完成“打败不死人将军”或者“召唤恶灵”两个任务其一;
  2. 角色等级达到100级;
  3. 角色身上不能有“不洁”这个属性。

按照上一节的内容,这个要求可以拆分成四个条件:

  • A: 完成任务, 打败不死人将军
  • B: 完成任务, 召唤恶灵
  • C: 等级, 100
  • D: 属性, 不洁

那么最终的条件就可以抽象成

“A或者B选一个,并且需要C,并且不能有D”。

猜你在找:布尔逻辑open in new window

让我们来问问大D老师:

bool0bool1bool2bool3bool4bool5

这不就是一个布尔逻辑的表达式嘛!有了这个,我们就可以把上面那个条件表达成这样一个简单的式子:

(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组成的布尔表达式>"
    }
  ]
}

目前看起来没有问题。我们就按照这个思路来实现任务系统吧!

Last Updated 2025/10/4 01:17:22