在 Node.js 中编写多人文本冒险引擎(第 1 部分)
已发表: 2022-03-10文字冒险是最早的数字角色扮演游戏之一,当时游戏没有图形,你所拥有的只是你自己的想象力和你在 CRT 显示器的黑屏上阅读的描述。
如果我们想怀旧,也许 Colossal Cave Adventure(或者只是 Adventure,因为它最初的名字)这个名字敲响了警钟。 那是有史以来第一款文字冒险游戏。
上图是您实际看到的游戏,与我们当前的顶级 AAA 冒险游戏相去甚远。 话虽如此,它们玩起来很有趣,并且会占用你数百小时的时间,因为你独自坐在文本前,试图弄清楚如何击败它。
可以理解的是,这些年来,文字冒险已经被呈现更好视觉效果的游戏所取代(尽管有人可能会说,其中很多游戏为了图形而牺牲了故事),尤其是在过去的几年里,与其他人合作的能力越来越强朋友一起玩。 这个特殊的功能是原始文本冒险所缺乏的,也是我想在本文中带回的一个。
本系列的其他部分
- 第 2 部分:游戏引擎服务器设计
- 第 3 部分:创建终端客户端
- 第 4 部分:将聊天添加到我们的游戏中
我们的目标
正如您现在可能从本文的标题中猜到的那样,这项工作的全部意义在于创建一个文本冒险引擎,允许您与朋友分享冒险,使您能够与他们进行类似的协作龙与地下城游戏(其中,就像好的 ol' 文字冒险一样,没有图形可看)。
在创建引擎时,聊天服务器和客户端是相当多的工作。 在本文中,我将向您展示设计阶段,解释诸如引擎背后的体系结构、客户端如何与服务器交互以及这个游戏的规则等内容。
只是为了给你一些视觉帮助,这将是什么样子,这是我的目标:
这就是我们的目标。 一旦我们到达那里,您将获得屏幕截图,而不是快速而肮脏的模型。 所以,让我们来看看这个过程。 我们将介绍的第一件事是整个事物的设计。 然后,我们将介绍我将用来编写代码的最相关的工具。 最后,我将向您展示一些最相关的代码(当然还有完整存储库的链接)。
希望到最后,您会发现自己正在创建新的文字冒险,并与朋友一起尝试!
设计阶段
对于设计阶段,我将介绍我们的整体蓝图。 我会尽力不让你厌烦至死,但同时,我认为在编写第一行代码之前展示一些需要发生的幕后工作是很重要的。
我想在这里详细介绍的四个组件是:
- 引擎
这将是主要的游戏服务器。 游戏规则将在此处实施,它将为任何类型的客户端提供一个与技术无关的界面来使用。 我们将实现一个终端客户端,但您可以使用 Web 浏览器客户端或您想要的任何其他类型执行相同的操作。 - 聊天服务器
因为它足够复杂,可以拥有自己的文章,所以该服务也将拥有自己的模块。 聊天服务器将负责让玩家在游戏过程中相互交流。 - 客户端
如前所述,这将是一个终端客户端,理想情况下,它看起来类似于之前的模型。 它将利用引擎和聊天服务器提供的服务。 - 游戏(JSON 文件)
最后,我将讨论实际游戏的定义。 这样做的重点是创建一个可以运行任何游戏的引擎,只要您的游戏文件符合引擎的要求。 因此,即使这不需要编码,我也会解释如何构建冒险文件,以便将来编写我们自己的冒险。
引擎
游戏引擎或游戏服务器将是一个 REST API,并将提供所有必需的功能。
我选择 REST API 只是因为——对于这种类型的游戏——HTTP 增加的延迟及其异步特性不会造成任何问题。 但是,我们将不得不为聊天服务器采用不同的路线。 但是在我们开始为我们的 API 定义端点之前,我们需要定义引擎的能力。 所以,让我们开始吧。
特征 | 描述 |
---|---|
加入游戏 | 玩家可以通过指定游戏的 ID 来加入游戏。 |
创建一个新游戏 | 玩家也可以创建一个新的游戏实例。 引擎应该返回一个 ID,以便其他人可以使用它来加入。 |
返回场景 | 此功能应返回聚会所在的当前场景。 基本上,它将返回描述以及所有相关信息(可能的操作、其中的对象等)。 |
与场景互动 | 这将是最复杂的之一,因为它将接受来自客户端的命令并执行该操作——例如移动、推送、获取、查看、读取等等。 |
检查库存 | 虽然这是一种与游戏交互的方式,但它并不直接与场景相关。 因此,检查每个玩家的库存将被视为不同的操作。 |
关于运动的一句话
我们需要一种在游戏中测量距离的方法,因为在冒险中移动是玩家可以采取的核心行动之一。 我们将使用这个数字来衡量时间,只是为了简化游戏玩法。 考虑到此类游戏具有回合制动作(例如战斗),使用实际时钟测量时间可能不是最好的。 相反,我们将使用距离来衡量时间(这意味着 8 的距离将需要比 2 之一更多的时间来遍历,因此我们可以做一些事情,比如为玩家添加持续一定数量“距离点”的效果)。
关于运动要考虑的另一个重要方面是我们不是一个人玩。 为简单起见,引擎不会让玩家分裂队伍(尽管这可能是未来的一个有趣的改进)。 这个模块的初始版本只会让每个人都可以移动到大多数党派决定的地方。 因此,移动必须以协商一致的方式进行,这意味着每一个移动动作都将等待大多数一方的请求才能发生。
战斗
战斗是这类游戏的另一个非常重要的方面,我们必须考虑将其添加到我们的引擎中; 否则,我们最终会错过一些乐趣。
老实说,这不是需要重新发明的东西。 回合制派对战斗已经存在了几十年,所以我们将只实现该机制的一个版本。 我们将把它与龙与地下城的“主动”概念混合在一起,滚动一个随机数字以保持战斗更有活力。
换句话说,参与战斗的每个人选择行动的顺序将是随机的,其中包括敌人。
最后(尽管我将在下面更详细地讨论这一点),您将拥有可以使用设定的“伤害”数字拾取的物品。 这些是您可以在战斗中使用的物品; 任何没有该属性的东西都会对你的敌人造成0伤害。 当您尝试使用这些物品进行战斗时,我们可能会添加一条消息,以便您知道您尝试做的事情毫无意义。
客户端-服务器交互
现在让我们看看给定的客户端如何使用之前定义的功能与我们的服务器交互(还没有考虑端点,但我们将在几秒钟内到达那里):
客户端和服务器的初始交互(从服务器的角度来看)是一个新游戏的开始,其步骤如下:
- 创建一个新游戏。
客户端向服务器请求创建一个新游戏。 - 创建聊天室。
虽然名称没有具体说明,但服务器不仅仅是在聊天服务器中创建一个聊天室,而且还设置了它所需的一切,以便允许一组玩家进行冒险。 - 返回游戏的元数据。
一旦服务器创建了游戏并且为玩家准备了聊天室,客户端将需要该信息来进行后续请求。 这主要是一组 ID,客户可以用来识别自己和他们想要加入的当前游戏(稍后会详细介绍)。 - 手动分享游戏ID 。
这一步必须由玩家自己完成。 我们可以提出某种共享机制,但我会将其留在愿望清单上,以备将来改进。 - 加入游戏。
这个很简单。 由于每个人都有游戏 ID,他们将使用他们的客户端应用程序加入冒险。 - 加入他们的聊天室。
最后,玩家的客户端应用程序将使用游戏的元数据加入他们冒险的聊天室。 这是赛前所需的最后一步。 一旦这一切都完成了,那么玩家就可以开始冒险了!
一旦满足所有先决条件,玩家就可以开始冒险,通过聚会聊天分享他们的想法并推进故事。 上图显示了为此所需的四个步骤。
以下步骤将作为游戏循环的一部分运行,这意味着它们将不断重复直到游戏结束。
- 请求场景。
客户端应用程序将请求当前场景的元数据。 这是循环每次迭代的第一步。 - 返回元数据。
服务器将依次发回当前场景的元数据。 这些信息将包括一般描述、在其中找到的对象以及它们之间的关系等内容。 - 发送命令。
这就是乐趣的开始。 这是玩家的主要输入。 它将包含他们想要执行的动作,以及该动作的目标(例如,吹蜡烛、抓石头等)(可选)。 - 返回对发送的命令的反应。
这可能只是第二步,但为了清楚起见,我将其添加为额外的步骤。 主要区别在于第二步可以被认为是这个循环的开始,而这一步考虑到你已经在玩了,因此,服务器需要了解这个动作将影响谁(或者是单个玩家或所有玩家)。
作为一个额外的步骤,虽然不是真正的流程的一部分,但服务器将通知客户端与它们相关的状态更新。
这个额外重复步骤的原因是因为玩家可以从其他玩家的动作中接收到更新。 回想从一个地方搬到另一个地方的要求; 正如我之前所说,一旦大多数玩家选择了一个方向,那么所有玩家都会移动(不需要所有玩家的输入)。
这里有趣的一点是 HTTP(我们已经提到服务器将成为 REST API)不允许这种类型的行为。 所以,我们的选择是:
- 从客户端每隔 X 秒执行一次轮询,
- 使用某种与客户端-服务器连接并行工作的通知系统。
根据我的经验,我倾向于选择选项 2。事实上,我会(并且将在本文中)将 Redis 用于这种行为。
下图演示了服务之间的依赖关系。
聊天服务器
我将把这个模块的设计细节留给开发阶段(这不是本文的一部分)。 话虽如此,有些事情我们可以决定。
我们可以定义的一件事是服务器的一组限制,这将简化我们的工作。 如果我们打得好,我们最终可能会得到一个提供强大接口的服务,从而最终允许我们扩展甚至更改实现以提供更少的限制而不会影响游戏。
- 每个聚会只有一个房间。
我们不会创建子组。 这与不让党分裂密切相关。 也许一旦我们实施了这种增强,允许创建子组和自定义聊天室将是一个好主意。 - 不会有私信。
这纯粹是为了简化目的,但是群聊已经足够了; 我们现在不需要私人消息。 请记住,每当您在开发最小可行产品时,请尽量避免陷入不必要的功能的陷阱; 这是一条危险的道路,而且很难摆脱。 - 我们不会持久化消息。
换句话说,如果您离开聚会,您将丢失消息。 这将极大地简化我们的任务,因为我们不必处理任何类型的数据存储,也不必浪费时间决定存储和恢复旧消息的最佳数据结构。 它都将存在于内存中,只要聊天室处于活动状态,它就会一直存在。 一旦它关闭,我们将简单地和他们说再见! - 通信将通过套接字完成。
遗憾的是,我们的客户端将不得不处理双重通信通道:一个用于游戏引擎的 RESTful 通道和一个用于聊天服务器的套接字。 这可能会稍微增加客户端的复杂性,但与此同时,它将为每个模块使用最佳的通信方法。 (在我们的聊天服务器上强制 REST 或在我们的游戏服务器上强制套接字没有真正意义。这种方法会增加服务器端代码的复杂性,而服务器端代码也处理业务逻辑,所以让我们专注于那一边目前。)
这就是聊天服务器。 毕竟,它不会很复杂,至少最初不会。 在开始编写代码时还有更多工作要做,但对于本文而言,这些信息已经绰绰有余。
客户端
这是需要编码的最后一个模块,它将成为我们最愚蠢的模块之一。 根据经验,我更喜欢让我的客户愚蠢而我的服务器聪明。 这样,为服务器创建新客户端变得更加容易。
就像我们在同一页面上一样,这是我们最终应该得到的高级架构。
我们简单的 CLI 客户端不会实现任何非常复杂的东西。 事实上,我们必须处理的最复杂的部分是实际的 UI,因为它是基于文本的界面。
话虽如此,客户端应用程序必须实现的功能如下:
- 创建一个新游戏。
因为我想让事情尽可能简单,所以只能通过 CLI 界面来完成。 实际的 UI 只有在加入游戏后才会使用,这将我们带到下一点。 - 加入现有游戏。
鉴于从前一点返回的游戏代码,玩家可以使用它来加入。同样,这是您应该能够在没有 UI 的情况下执行的操作,因此此功能将成为开始使用文本 UI 所需过程的一部分。 - 解析游戏定义文件。
我们稍后会讨论这些,但客户端应该能够理解这些文件,以便知道要显示什么并知道如何使用这些数据。 - 与冒险互动。
基本上,这使玩家能够在任何给定时间与描述的环境进行交互。 - 为每个玩家维护一个库存。
客户端的每个实例都将包含一个内存中的项目列表。 此列表将被备份。 - 支持聊天。
客户端应用程序还需要连接到聊天服务器并将用户登录到聚会的聊天室。
稍后将详细介绍客户的内部结构和设计。 同时,让我们完成设计阶段的最后一点准备:游戏文件。
游戏:JSON 文件
这就是有趣的地方,因为到目前为止,我已经介绍了基本的微服务定义。 他们中的一些人可能会说 REST,而其他人可能会使用套接字,但本质上,它们都是一样的:你定义它们,编码它们,它们提供服务。
对于这个特定的组件,我不打算编写任何代码,但我们需要设计它。 基本上,我们正在实现一种协议来定义我们的游戏、游戏中的场景以及游戏中的所有内容。
如果你仔细想想,文字冒险的核心基本上是一组相互连接的房间,在它们里面是你可以与之互动的“东西”,所有这些都与一个希望是体面的故事联系在一起。 现在,我们的引擎不会处理最后一部分; 那部分将取决于你。 但对于其他人来说,还有希望。
现在,回到一组相互连接的房间,对我来说这听起来像一个图表,如果我们还添加我之前提到的距离或移动速度的概念,我们就有了一个加权图表。 这只是一组节点,它们有一个权重(或只是一个数字——别担心它叫什么),代表它们之间的路径。 这是一个视觉效果(我喜欢边看边学,所以只看图像,好吗?):
这是一个加权图——就是这样。 我相信你已经弄清楚了,但为了完整起见,让我向你展示一旦我们的引擎准备好你将如何去做。
开始设置冒险后,您将创建地图(如下图左侧所示)。 然后您将其转换为加权图,如图像右侧所示。 我们的引擎将能够将其拾取并让您以正确的顺序通过它。
通过上面的加权图,我们可以确保球员不能从入口一直走到左翼。 他们必须通过这两者之间的节点,这样做会消耗时间,我们可以使用连接的权重来衡量。
现在,进入“有趣”的部分。 让我们看看 JSON 格式的图形是什么样子的。 在这里忍受我; 这个 JSON 将包含很多信息,但我会尽可能多地介绍它:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
我知道它看起来很多,但如果你把它归结为对游戏的简单描述,你有一个由六个房间组成的地牢,每个房间都相互连接,如上图所示。
你的任务是穿过它并探索它。 你会发现有两个不同的地方可以找到武器(在厨房或黑暗的房间里,通过打破椅子)。 您还将面临一扇锁着的门; 所以,一旦你找到钥匙(位于类似办公室的房间内),你就可以打开它并用你收集的任何武器与老板战斗。
您要么杀死它而获胜,要么被它杀死而失败。
现在让我们更详细地了解整个 JSON 结构及其三个部分。
图形
这将包含节点之间的关系。 基本上,这部分直接翻译成我们之前看过的图表。
本节的结构非常简单。 它是一个节点列表,其中每个节点都包含以下属性:
- 在游戏中唯一标识节点的 ID;
- 名称,基本上是人类可读的 ID 版本;
- 一组到其他节点的链接。 存在四个可能的键就证明了这一点:“北”、“南”、“东”和“西”。 我们最终可以通过添加这四个的组合来添加更多的方向。 每个链接都包含相关节点的 ID 和该关系的距离(或权重)。
游戏
本节将包含一般设置和条件。 特别是,在上面的示例中,此部分包含输赢条件。 换句话说,有了这两个条件,我们会让引擎知道游戏什么时候可以结束。
为了简单起见,我只添加了两个条件:
- 你要么通过杀死老板而获胜,
- 或因被杀而输。
房间
这是 163 行中大部分的来源,也是最复杂的部分。 在这里,我们将描述我们冒险中的所有房间以及其中的一切。
每个房间都有一把钥匙,使用我们之前定义的 ID。 每个房间都有描述、物品清单、出口(或门)清单和不可玩角色(NPC)清单。 在这些属性中,唯一应该是强制性的就是描述,因为引擎需要该描述才能让您知道所看到的内容。 他们中的其他人只有在有东西要展示的时候才会在那里。
让我们看看这些属性可以为我们的游戏做什么。
说明
这个项目并不像人们想象的那么简单,因为您对房间的看法可能会根据不同的情况而改变。 例如,如果您查看第一个房间的描述,您会注意到,默认情况下,您什么都看不到,当然,除非您随身携带点燃的手电筒。
因此,拿起物品并使用它们可能会触发影响游戏其他部分的全局条件。
这几项
这些代表了您可以在房间内找到的所有东西。 每个项目都与图形部分中的节点具有相同的 ID 和名称。
他们还将有一个“目的地”属性,该属性指示该物品一旦被拾起应该存放在哪里。 这是相关的,因为您将只能拥有一件物品,而您可以在库存中拥有任意数量的物品。
最后,其中一些项目可能会触发其他操作或状态更新,具体取决于玩家决定如何处理它们。 其中一个例子是入口处点燃的火把。 如果您抓住其中一个,您将触发游戏中的状态更新,进而使游戏向您显示下一个房间的不同描述。
物品也可以有“子物品”,一旦原始物品被破坏(例如通过“破坏”动作),它就会发挥作用。 一个项目可以分解为多个项目,这在“子项目”元素中定义。
本质上,这个元素只是一个新项目的数组,其中还包含一组可以触发其创建的操作。 这基本上打开了根据您对原始项目执行的操作创建不同子项目的可能性。
最后,一些物品将具有“损坏”属性。 因此,如果您使用物品击中 NPC,则该值将用于减去他们的生命。
出口
这只是一组属性,指示出口的方向和它的属性(描述,如果你想检查它,它的名称,在某些情况下,它的状态)。
出口是与项目分开的实体,因为引擎需要了解您是否可以根据它们的状态实际遍历它们。 除非您弄清楚如何将其状态更改为未锁定,否则锁定的出口不会让您通过它们。
NPC
最后,NPC 将成为另一个列表的一部分。 它们基本上是带有统计数据的项目,引擎将使用这些数据来了解每个项目的行为方式。 我们在示例中定义的是“hp”,它代表生命值,而“damage”,就像武器一样,是每次命中将从玩家生命值中减去的数字。
这就是我创建的地牢。 很多,是的,将来我可能会考虑创建一个关卡编辑器,以简化 JSON 文件的创建。 但就目前而言,这没有必要。
如果您还没有意识到,将我们的游戏定义在这样的文件中的主要好处是,我们将能够像您在超级任天堂时代那样切换 JSON 文件。 只需加载一个新文件并开始新的冒险。 简单!
结束的想法
感谢您到目前为止的阅读。 我希望你喜欢我为实现一个想法而经历的设计过程。 不过请记住,我正在编造这个,所以我们稍后可能会意识到我们今天定义的某些东西不会起作用,在这种情况下,我们将不得不回溯并修复它。
我敢肯定,有很多方法可以改进这里提出的想法并制造出一个地狱般的引擎。 但这需要更多的文字,而不是让每个人都觉得无聊,所以我们暂时搁置它。
本系列的其他部分
- 第 2 部分:游戏引擎服务器设计
- 第 3 部分:创建终端客户端
- 第 4 部分:将聊天添加到我们的游戏中