从构思到7天的可行游戏-第2部分

在能够产生无穷无尽的游戏关卡之后,我需要考虑我的游戏应该是什么,以及我将要克服的挑战。

那么,从这一点到下面的最终应用程序的屏幕快照中可以看到什么呢?

我的第一个决定是,该项目的完成时间不应超过两天(或与我通常在一周的时间内花在Launch School上的时间差不多)。 当然,我会在该项目中使用Ruby ,因为这是我现在最熟悉的语言。

然后,我创建了一个绝对最小数量的功能列表,这些功能可以使游戏体验(至少是部分乐趣)成为可能:

  1. 可以穿越洞穴的英雄人物
  2. 跟随玩家并攻击他的怪物
  3. 一个与那些怪物打交道的战斗系统
  4. 至少令人愉悦的用户界面

这大致就是我要处理该项目的顺序。 考虑到这些功能中的每一个,我发现了需要克服的一些主要障碍:

  • 应用程序输出 -我需要一种快速而可靠的方式,通过终端窗口或某种可视UI在屏幕上绘制游戏。
  • 寻路算法 -敌人需要移动,因此需要确定玩家角色的路径。 我知道可能可以实现一些算法,但这只是我所了解的范围。
  • 代码组织 -这是我曾经处理过的最大的编程项目,因此我不知道如何防止代码变成混乱的意大利面条。 我需要一种方法来限制每个组件的职责,并以某种方式将所有这些组件编排为一个大游戏循环。

我使用Launch School受信任的PEDAC方法 ,有条不紊地开始构建每个功能。 我的第一个决定必须迅速做出:如何处理输入和输出?

图形保真度的代价

我最初的想法是利用标准终端以完全ASCII的形式显示我的游戏,就像过去的Rogue类游戏一样 。 我使用了Ruby的IO#raw模式来读取字符输入,但是更大的问题是输出渲染:我找到了一种使用ANSI转义代码将光标移到终端内并在需要的地方绘制内容的方法,但是表现很糟糕 在屏幕上绘制我的洞穴地图会导致光标快速闪烁。

有像ncurses这样的库,它们提供了用于正确渲染终端应用程序的API,但是我发现包装了这个基于C的库的Ruby gem要么过时,要么稀疏地记录了下来。

Launch School核心课程的后端开发部分中,我退回到了一个更简单的解决方案:我将在后端中使用Sinatra,并使浏览器能够处理用户输入和游戏输出。 这有几个优点:

这迫使我在前端和后端之间保持强烈的分隔,这反过来又可以帮助我组织代码。 我也摆脱了ASCII终端输出的束缚,可以同时使用HTML和CSS创建令人满意的GUI。 最后,它使处理用户输入就像在JavaScript中创建单个事件监听器一样容易。

Sinatra的这一决定产生了一个有趣的效果:我最终代码中只有四分之三是用Ruby编写的。 其余部分分为三种前端语言。 我并不是特别喜欢创建前端,因为我还没有上过HTML,CSS和JavaScript的Launch School课程。 过去,我在徒劳地学习编码的过程中不得不依靠自己获得的点点知识,这使工作流程不尽人意,在GoogleMDN文档Stack Overflow之间摇摆不定

寻路,或:不用担心算法

很快就可以实现一个通用的游戏循环和一个可以在空白地图中四处走动的角色。 现在是时候面对我认为最大的挑战:寻路。

我认为我需要一个解决所有问题的算法:首先,找到怪物到达玩家所需要的路径。 但是我需要更多信息:怪物离玩家有多远? 离玩家最远的牢房是什么(我想将关卡出口放置得尽可能远)? 怪物是否在可能的玩家战斗能力范围内?

理想情况下,我将拥有可以在所有这些情况下使用的算法。 我查看了维基百科关于寻路的内容,发现最常用的算法全都依赖于将问题表示为图形的想法。 在处理了基于图形的数据结构之后,我采用了一种简单的广度优先搜索算法 。 让我们看看它是如何工作的:

为了获得从怪物到玩家的路径,我们创建了一个树形图形,以怪物单元为根节点 。 每个节点都是一个简单的构造,它保存有关其网格坐标的信息,并可以返回其相邻像元的位置:

 类节点 
def初始化( 位置
@x,@y =位置
结束
 防御位置 
[@ x,@ y]
结束
  def邻居 
相邻= [
[-1,-1],[0,-1],[1,-1],
[-1,0],[1,0],
[-1,1],[0,1],[1,1]
]
neighbor.map {| dx,dy | [@ x + dx,@ y + dy]}
结束
结束

要为我们的图创建分支,我们查看根节点的邻居:

我们可以安全地忽略壁单元(灰色),因为玩家或怪物都不能输入壁单元。 上面的示例剩下三个邻居。

这三个邻居组成了我们树的第一个分支。 为了用代码表示这一点,我们使用两个变量(数组和哈希)来存储我们需要探索的单元格和我们已经访问过的单元格:

  @explore = [] 
@visited = 哈希 .new(nil)

我们的根节点是要探索的初始单元格,因此将其添加到数组中:

  @explore << @root 
@visited [@ root.position] = [0,[]]

在我们的哈希中,我们保存了到根节点的距离(对于根节点本身,该距离为0)和关于到达当前节点所采用的路径的信息(对于根节点,则为空数组)。

根节点的这三个邻居中的每一个都将添加到要探索的单元格阵列中。 在一个简单的循环中,我们取出该数组的第一个元素并对其进行访问。 这意味着,如果以前没有访问过该节点,则将信息保存到哈希中:距离从父节点的距离增加1,并将该节点的位置添加到距离父节点的步骤数组中。 最后,我们将该访问节点的所有邻居添加到节点数组中,以进行下一步的研究。

当该节点阵列中没有更多元素要探索时,算法停止。 我们返回我们的哈希,该哈希现在包含有关距离和到任何单元格的可能路径的信息。 Voilá,我们实施了广度优先搜索 ,现在可以解决所有上述问题:找出到特定位置的路径以及到关卡中所有其他像元的距离。

我实现了一种基本的敌人类型,并让其在玩家身上散开。 真是太棒了:我可以捉迷藏! 但是,当我在项目中逐级添加时,我需要一个解决我现在最大的问题的方法:添加更多功能后,如何编排所有内容?

阅读最后的第3部分,或查看项目的GitHub存储库 (我的寻路算法可以在lib / pathfinder.rb文件中找到)。