十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
摘要
听说过文字冒险游戏吗? 如果你的年龄足够大的话(就像我一样),那么你可能听说过、甚至玩过“back in the day”。在本文中,我将向你展示编写的整个过程。这不仅仅是一个文本冒险游戏,而是一个能让你和你的朋友们一起玩的,可以进行任何剧情的文本冒险游戏引擎。 没错,我们将通过在添加多人游戏功能来增加它的趣味性。
文字冒险是最早的 RPG 形式的游戏之一,回到还没有图形画面的时代,你只能通过阅读 CRT 显示器上黑色背景下的描述,并且依赖自己的想象力来推动游戏剧情的发展。
如果要怀旧的话,可能世界上第一个文字冒险游戏名叫 Colossal Cave Adventure(也许是叫 Adventure)。
上图是你实际看到的游戏画面,这与我们现在的顶级 AAA 冒险游戏相差甚远。 尽管如此,但是他们玩起来却很有趣,并会很容易的消磨你几百个小时的时间,因为只有你自己自己坐在显示器前,试图找到打穿它的途径。
可以理解的是,多年以来,文字冒险已经被更好的视觉效果所取代,特别是在过去几年里,游戏的协作性越强,你可以和朋友们一起玩。 这是原始的文字冒险游戏所缺少的,同时也是我想在本文中提到的功能。
我们的目标
可能你已经从标题中猜到了,本文的重点在于创建一个文字冒险引擎,并且让你和朋友们一起玩,使你能够与他们进行协作,就像在玩“龙与地下城”这个游戏一样。
在创建引擎时,聊天服务器和客户端的工作了相当大。 在本文中,我将向你展示设计思路、解释引擎背后的架构、客户端如何与服务器交互以及这个游戏的规则。
为了让你对我的目标又一个直观的感受,先上一张图:
这就是我们的目标。 一旦达成这个目标,将会得到截图而不是简单和肮脏的模型。 所以,需要了解这个过程。首先要介绍的就是整体设计;然后介绍我将用来编码的相关工具;最后我将向你展示一些核心代码(当然,还有指向完整代码库的链接)。
希望到最后,你能够自己创造一个新的文字冒险游戏,并与朋友一起乐在其中!
设计阶段
在设计阶段,我将描述这个游戏的整体蓝图。 我会尽力不让你觉得无聊,不过我认为在给你展示第一行代码之前,很有必要先搞清楚幕后的一些工作。
我想接下来介绍的这四个组件能够提供相当多的细节:
引擎
游戏引擎或游戏服务器将会是REST API,并提供所有必需的功能。
我选择REST API只是因为(对于这种类型的游戏)HTTP造成的延迟以及他的异步特性不会造成任何麻烦。 但是,我们必须为聊天服务器采用不同的路线。 在开始定义 API 之前,先需要定义引擎的功能。 所以,让我们来看看吧。
特性 | 描述 |
---|---|
加入游戏 | 玩家可以通过指定的游戏ID来加入游戏。 |
创建一个新游戏 | 玩家还可以创建新的游戏实例。 引擎应该返回一个ID,以便其他人可以使它来加入游戏。 |
返回场景 | 此功能应返回玩家所在的当前场景。 基本上,它将返回描述,包含所有相关信息(可能的操作、其中的对象等)。 |
与场景互动 | 这将是最复杂的一个,因为它将从客户端获取命令并执行该操作——例如移动,攻击,获取,查看,读取等等。 |
检查库存 | 虽然这是与游戏互动的一种方式,但它与场景并没有直接关系。 因此,检查每个玩家的库存将被视为不同的操作。 |
关于移动
我们需要一种用来测量游戏中距离的方法,因为在游戏中玩家可以采取的核心行动之一就是移动。 我们需要用这个数字作为时间的衡量标准,来简化游戏的玩法。 考虑到这一类型的游戏具有基于回合的动作,例如战斗,使用实际时钟对时间进行测量可能不是最好的。 所以我们将使用距离来测量时间(意味着距离为 8 比距离为 2 将需要更多的时间,从而允许我们做一些事情,例如为持续一定数量的“距离点”的玩家添加效果)。
考虑运动的另一个原因是不是一个人在玩这个游戏。 为简单起见,引擎不会让玩家随意组队(虽然这对未来可能是一个有趣的改进)。 该模块的初始版本只允许个人朝着大多数参与者决定的地方移动。因此,必须以协商一致的方式进行移动,这意味着每一步行动都将等待大多数人在行动之前提出请求。
战斗
战斗是这种游戏另一个非常重要的方面,我们不得不考虑将它添加到引擎中,否则我们最终会失去一些乐趣。
说实话,这并不需要重新发明轮子。基于回合制的组队对战已经存在了几十年,所以在这里只实现这个机制的一个简单版本。我们将把它与“龙与地下城”中的“主动性”这个概念混合起来,产生一个随机数使战斗更有活力。
换句话说,就是参与战斗的每个人的行动顺序将会被随机化,其中包括敌人。
最后(虽然我将在下面详细介绍这一点),你可以用设置的“攻击力”值的物品。这些是你在战斗中可以使用的道具;如果一个道具没有这个属性的话只能对敌人造成 0 点伤害。当你试图用这样的道具进行战斗时,我们可能会添加一条消息,这样你就能知道自己要做的事情是毫无意义的。
客户端 - 服务器交互
现在来看看客户端怎样基于前面定义的功能与服务器进行交互(目前还没考虑端点,不过马上就会讲到这个):
客户端和服务器之间的初始交互(从服务器的角度来看)是一个新游戏的开始,其步骤如下:
一旦满足了先决条件,玩家就可以开始游戏,通过聊天室分享他们的想法,并推动故事的发展。上图显示了所需的四个步骤。
以下步骤将作为游戏循环的一部分来运行,这意味着它们将会不断重复,一直到游戏结束。
作为额外步骤,虽然不是流程的一部分,但服务器将通知客户端与它们相关的状态的更新情况。
存在这个额外重复步骤的原因是玩家可以从其他玩家的动作中获得更新。回想从一个地方移动另一个地方的需求;正如我之前所说那样,一旦大多数玩家选择了方向,那么所有玩家都会移动(不需要所有球员的输入)。
不过 HTTP(前面已经提到服务器为REST API)不允许这种类型的行为。所以,我们的选择是:
根据我的经验,我倾向于选择选项 2。实际上,我会(在本文中)使用Redis来实现这种行为。
下图演示了服务之间的依赖关系。
客户端应用程序与游戏引擎之间的交互
聊天服务器
我将把这个模块的设计细节留给开发阶段(本文不涉及这一部分)。话虽如此,我们仍可以决定一些事情。
我们可以确定的一件事是服务器的限制集合,这将简化我们的工作。如果我们正确地玩牌,最终可能会有一个提供强大界面的服务,从而允许我们去进行扩展甚至修改实现,以提供更少的限制,而不会影响到游戏。
这就是聊天服务器。毕竟,它不会很复杂。在开始编码之前还有很多工作要做,但是对于本文来说已经足够了。
客户端
这是最后一个需要编码的模块,它将是最笨重的一个模块。根据经验来看,我更喜欢让客户端笨重,使服务器轻巧。这样为服务器开发新的客户端会更加容易。
这是我们最终应该采用的架构。
我们要实现的ClI客户端很简单,不会实现任何非常复杂的东西。实际上,必须要解决的最复杂的部分是 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": "littorch2", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch3", "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 } } ] } } }