基于面向对象编码风格的贪吃蛇 HTML5 小游戏的开发教程。这里实现的贪吃蛇可以穿墙。
代码
源码地址:https://github.com/F-star/html5-game
示例链接:https://f-star.github.io/html5-game/snake/
模块设计
之所以使用了面向对象的风格去开发这个游戏,主要是为了让代码更加可扩展。相比面向过程自上而下的符合人类直觉的写法,面向对象则是把一个流程进行了拆分,分成很多小的模块,然后将它们组合成一个大模块然后运行,是自下而上的写法。
当然很残念的是,我使用的是 javaScript 的原生语法,并没有类型检测功能,也没有提供原生的封装特性(即可以设置私有变量)。但还好这个项目不是很大,可以靠程序员(指我自己)的自觉来保证不直接使用成员属性。
代码中涉及到有几个大类,分别是 Snake、Apple、Layer。Snake 类主要负责计算贪吃蛇的位置和绘制方式。Apple 其实就是贪吃蛇要吃的东西,其中比较重要的方法是计算在画布上且不在贪吃蛇上的位置。Layer 类就是负责操作 Snake 和 Apple 对象,在 canvas 画布上绘制出每一帧,之所以叫做 Layer(图层),主要是考虑以后可能还要进行分层,将不经常重绘的图案放到其他一些 canvas 上,比如背景层。当然这个小游戏最终还是只有一个图层,不过以后如果要扩展,就会比较方便。
此外还有一些小类:Vector 和 DataViewer。Vector 就是一个简单实现的向量类,目前只有一个求点积的方法。DataViewer 负责简单地将一些游戏数据可视化,比如游戏的 FPS(每秒帧数)和得分(吃掉的苹果数量)。
Snake 类
首先我们需要一个 snake 类。主要是贪吃蛇的一些相关方法,如计算贪吃蛇的下一帧的位置,贪吃蛇的绘制等方法。
|
|
首先我们要确定 Snake 类要用到的属性,这里简单进行说明:
grid_w:格子宽度,贪吃蛇其实是由一个个正方形的块(grip)组成的。points:构成贪吃蛇的所有点的位置。row:行数col:列数dir:贪吃蛇当前方向(只有上下左右方向)的单位向量对象,其实是一个有 x 和 y 属性且它们的平方和为 1 的对象,属于高中数学的知识点。next_dir:贪吃蛇的期望方向,同样是个向量。speed:贪吃蛇的速度,每秒贪吃蛇移动多少个单位。

Apple 类
贪吃蛇吃的是苹果(也有其他说法说是豆子)。苹果会在游戏初始化的时候和被贪吃蛇吃掉的时候,计算新的位置并绘制出来。这个位置要求在画布内,且不在贪吃蛇占据的位置上。
这个没什么好说的,不过这个计算新位置的算法实现起来比较麻烦。(另外我这里的实现有一点 bug,有空修复后我会更新这篇文章)
Layer 类
这个类用于控制 Snake 对象和 Apple 对象之间的交互以及绘制。比如贪吃蛇移动后如果吃到苹果后的逻辑,贪吃蛇咬到自己的逻辑等。
核心逻辑
核心代码是写在 Layer 类的 update() 方法中。
|
|
这里我把里面的贪吃蛇正常运动的代码提取出来(去掉了一下判断逻辑),以便读者更好地聚焦贪吃蛇的行为逻辑:
|
|
下面我们接着详细叙述一下 upate() 方法做了什么事情。
首先我们要计算贪吃蛇的头部的下一帧的位置。要计算头部的新位置,首先要知道贪吃蛇的期望方向。不过在讨论这个之前,我希望读者能够明白为什么这里有个期望方向(next_dir)的属性。读者可能会觉得当键盘按下时,直接就修改贪吃蛇的 dir,不是更简单吗?
为什么要引入 next_dir(期望方向) 属性,而不是直接修改贪吃蛇的 dir(当前方向) ?
之所以引入 next_dir 是有原因的。首先贪吃蛇的方向的修改是有限制的:下一个方向不能和当前贪吃蛇方向反向。假设我们使用了直接修改 dir 的方案。假设贪吃蛇的方向为右,我们按下左方向键时,修改失败。但如果我们快速地依次按下上方向键和左方向键,贪吃蛇的方向就会改为和当前方向相反的左方向了(只要这两个操作可以在下一帧之前执行)。中间的方向变更为向上方向其实被忽略掉了的(下一帧没有进行这个操作),这样会解除贪吃蛇不能与当前方向反向的限制。这显然不是我们想要的效果。
于是我们引入了 next_dir 这一属性,缓存了玩家希望贪吃蛇下一帧改变的方向,直到下一帧执行的时候,才进行对修改方向,并将 next_dir 重置为 null。
|
|
此外,之所以方向都用向量来表示,主要是可以通过运算得知两个向量是否方向。当然你也可以使用字符串来记录(如’left', ‘right’),不过这样判断两个方向是否反向,条件判断会多一些。此外,如果方向使用的是单位向量,计算下一个贪吃蛇头部位置会方便很多(向量相加)。但使用字符串的表达,可读性更好一些。
每个新的一帧我们会先更新贪吃蛇的方向,然后计算得到贪吃蛇的头部的新位置。
然后我们要更新贪吃蛇的位置:如果头部的新位置不与苹果的位置重合,往 snake 的 points 数组最前面添加这个头部位置,并删除最后一个数组元素,并给苹果一个新的随机位置;如果和苹果的位置重合,points 数组只需要添加头部位置,不删除数组的最后一个元素,这样贪吃蛇的长度就加一了。
更新完贪吃蛇的位置后,判断贪吃蛇的头部是否和其他点重合,重合说明咬到自己了,游戏结束;如果没有咬到自己,就渲染贪吃蛇和苹果。
结尾
在贪吃蛇开发的过程中,遇到和解决了很多问题,也学到了很多的东西。
在开发一个比较复杂的系统的时候,我们不可能一开始就想好所有的功能模块,因为其中的细节实在是太多。而是应该先做一个最小的原型。