本文详解如何使用JavaScript结合中介者模式开发贪吃蛇游戏。通过拆分Game、Snake和Food类,实现结构清晰的游戏逻辑。内容涵盖DOM初始化、蛇身渲染与运动控制、碰撞死亡判定、食物生成及游戏状态管理(开始/暂停),提供完整的面向对象实现思路。
页面效果展示如下:

本贪吃蛇案例采用中介者模式进行开发。与以往将所有代码编写在单一HTML文件中的方式不同,本项目进行了类的拆分,每个类由独立的JS文件表示,其中Game类作为中介者协调各组件交互。

一、初始化结构
首先执行页面初始化,布局并非直接在HTML标签中硬编码,而是通过Game类动态初始化节点并挂载到DOM树中。
我们将游戏区域设置为20行20列的表格,使用this.row表示行数,this.col表示列数。

Games.prototype.init = function () {
this.dom = document.createElement('table');
var tr, td;
//遍历行和列上树
for (var i = 0; i < this.row; i++) {
// 遍历行,创建结点上树
tr = document.createElement('tr');
for (var j = 0; j < this.col; j++) {
// 遍历列,创建结点上树
td = document.createElement('td');
// 追加结点上树
tr.appendChild(td);
// 追加结点上树
this.dom.appendChild(tr);
}
}
// 表格上树
document.getElementById('app').appendChild(this.dom);
}
实现效果如下图所示:

二、渲染蛇的颜色
蛇的颜色渲染逻辑如下:Snake类调用Game类的setColor方法。由于表格本质上是颜色渲染的载体,且表格已在Game类初始化时创建,因此由Game类负责提供设置颜色的方法。
// 设置颜色的方法
Games.prototype.setColor = function (row, col, color) {
// 让表格的第几行第几列设置什么颜色
this.dom.getElementsByTagName('tr')[row].getElementsByTagName('td')[col].style.backgroundColor = color;
}
此时,由Snake类调用game的setColor方法来完成渲染。
Snake.prototype.render = function () {
// 蛇的渲染
game.setColor(this.body[0].row, this.body[0].col, 'pink');
// 蛇的身体
for (var i = 1; i < this.body.length; i++) {
game.setColor(this.body[i].row, this.body[i].col, 'skyblue');
}
}
这里存在一个问题:Snake的render方法由谁调用?如果在Snake自身的构造函数中直接调用,由于此时Game类的初始化四步尚未执行完毕,game实例仍为undefined,导致调用失败。
解决方法是使用定时器进行调用。由于定时器是异步执行的,不会阻塞Game类的初始化流程,从而解决了依赖时序问题。
this.timer = setInterval(function () {
// 定时器里面的核心就是游戏的渲染本质,清屏-更新-渲染
// 渲染蛇
game.snake.render();
}, 20);
实现效果如下图所示:

三、蛇的运动
蛇的运动本质上是body数组的更新过程,即从数组尾部删除一个元素,并在头部增加一个新元素,从而渲染出蛇的新状态。
Snake.prototype.update = function () {
// 当前的direction接收willdirection
this.direction = this.willDirection;
switch (this.direction) {
case 'R':
this.body.unshift({ 'row': this.body[0].row, 'col': this.body[0].col + 1 });
break;
case 'D':
this.body.unshift({ 'row': this.body[0].row + 1, 'col': this.body[0].col });
break;
case 'L':
this.body.unshift({ 'row': this.body[0].row, 'col': this.body[0].col - 1 });
break;
case 'T':
this.body.unshift({ 'row': this.body[0].row - 1, 'col': this.body[0].col });
break;
}
此时你会发现蛇的长度似乎没有变化,这是因为我们仅定义了如何渲染颜色,而未处理旧状态的清除。

之前渲染过的蛇身需要被擦除,否则会出现拖影或长度显示异常。
我们在Game类中添加一个擦除屏幕的方法,用于重置单元格样式。
// 清屏
Games.prototype.clear = function () {
for (var i = 0; i < this.row; i++) {
for (var j = 0; j < this.col; j++) {
this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].style.backgroundColor = '#fff';
this.dom.getElementsByTagName('tr')[i].getElementsByTagName('td')[j].innerHTML = '';
}
}
}
接下来,在定时器中执行游戏的三步走策略:清屏、更新蛇的位置、重新渲染。
// 清屏
game.clear();
// 蛇的运动/更新
// 蛇的更新速度 当蛇边长的时候,速度加快
var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
// 蛇的更新
game.f % during == 0 && game.snake.update();
// 渲染蛇
game.snake.render();
为了控制蛇在不同方向的运动,Game类设置bindEvent事件监听键盘操作,改变运动方向,并加入逻辑判断,例如当蛇头向下运动时禁止按上键。
Games.prototype.bindEvent = function () {
var self = this;
// 键盘事件
document.onkeydown = function (even) {
switch (even.keyCode) {
case 37:
// 先进行判断,如果当前的方向是向右移动,此时我们不能按左键
if (self.snake.direction == 'R') return;
self.snake.changeDirection('L');
break;
case 38:
// 先进行判断,如果当前的方向是向下移动,此时我们不能按上键
if (self.snake.direction == 'D') return;
self.snake.changeDirection('T');
break;
case 39:
// 先进行判断,如果当前的方向是向左移动,此时我们不能按右键
if (self.snake.direction == 'L') return;
self.snake.changeDirection('R');
break;
case 40:
// 先进行判断,如果当前的方向是向上移动,此时我们不能按下键
if (self.snake.direction == 'T') return;
self.snake.changeDirection('D');
break;
}
}
}
Snake类也需对应方向属性,我们在Snake初始化时设置this.direction='R'(向右)作为默认方向。
Snake.prototype.update = function () {
// 当前的direction接收willdirection
this.direction = this.willDirection;
switch (this.direction) {
case 'R':
this.body.unshift({ 'row': this.body[0].row, 'col': this.body[0].col + 1 });
break;
case 'D':
this.body.unshift({ 'row': this.body[0].row + 1, 'col': this.body[0].col });
break;
case 'L':
this.body.unshift({ 'row': this.body[0].row, 'col': this.body[0].col - 1 });
break;
case 'T':
this.body.unshift({ 'row': this.body[0].row - 1, 'col': this.body[0].col });
break;
}
四、蛇死亡的判定方式
蛇死亡包含两种判定情形:第一种是蛇头超出表格边界。
// 超出了表格边缘的部分
if (this.body[0].col > game.col - 1 || this.body[0].row > game.row - 1 || this.body[0].col < 0 || this.body[0].row < 0) {
alert('游戏结束,您当前的得分为' + game.score);
clearInterval(game.timer);
this.body.shift();
clearInterval(game.timer);
}
第二种是蛇头与蛇身任意部位发生重合。
// 自己撞到自己
for (var i = 1; i < this.body.length; i++) {
if (this.body[0].col == this.body[i].col && this.body[0].row == this.body[i].row) {
alert('游戏结束,您当前的得分为' + game.score);
this.body.shift();
clearInterval(game.timer);
}
}
五、食物的创建
创建一个Food类来生成食物,在Game类中实例化该对象,并在定时器中负责食物的渲染。
在随机生成食物的row和col坐标时,需先判断该位置是否已被蛇身占据,避免食物生成在蛇身上。
function Food(gameSnake) {
var self = this;
// 食物的位置
// do-while循环语句作用是先创建一个row和col然后判断这个row和col是否在蛇的身上
do {
this.row = parseInt(Math.random() * gameSnake.row);
this.col = parseInt(Math.random() * gameSnake.col);
} while ((function () {
// 遍历蛇的row和col 然后和Food新随机出来的row和col进行判断,是否重合
for (var i = 0; i < gameSnake.snake.body.length; i++) {
if (gameSnake.snake.body[i].row == self.row && gameSnake.snake.body[i].col == self.col) {
return true;
}
}
return false;
})())
console.log(this.row, this.col);
}
Food.prototype.render = function () {
game.setHTML(this.row, this.col, '♥');
}
六、蛇吃食物变长
蛇在运动时,body数组头部添加元素,尾部删除元素。当蛇头碰到食物后,只需让尾部不执行删除操作,即可实现蛇身变长。
if (this.body[0].row == game.food.row && this.body[0].col == game.food.col) {
// 创建新的食物
game.food = new Food(game);
// 让帧编号归0
// 加分数
game.score++;
game.f = 0;
} else {
this.body.pop();
}
七、蛇吃食物变快
我们引入帧编号机制,在碰到食物前帧编号一直自增。通过让蛇的更新频率随蛇的长度增长而加快,实现难度递增。
this.f = 0;
game.f++;
// 蛇的更新速度 当蛇边长的时候,速度加快
var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
// 蛇的更新
game.f % during == 0 && game.snake.update();
// 渲染蛇
八、开始游戏功能
在HTML中添加一个按钮,通过CSS定位将其放置在合适位置,并绑定点击事件。只有点击“开始”按钮后,才执行Game类中的核心代码逻辑。
<div id="app"></div>
<div class="startgame"><img src="images/btn1.gif" alt=""></div>
<div class="stopgame"><img src="images/btn4.png" alt=""></div>
<script>
var game = null;
var btnstart = document.querySelector('.startgame');
var btnstop = document.querySelector('.stopgame')
btnstart.addEventListener('click', function () {
btnstart.style.display = 'none';
game = new Games();
// console.log(table);
var table = document.querySelector('table');
table.addEventListener('click', function () {
clearInterval(game.timer);
btnstop.style.display = 'block';
})
九、暂停/继续游戏功能
为暂停按钮和表格分别绑定点击事件。点击表格时,暂停按钮显现并停止Game中的定时器;点击暂停按钮后,重新开启定时器并隐藏暂停按钮。
btnstop.addEventListener('click', function () {
btnstop.style.display = 'none';
game.timer = setInterval(function () {
// 定时器里面的核心就是游戏的渲染本质,清屏-更新-渲染
game.f++;
// document.getElementById('f').innerHTML = '帧编号:' + game.f;
// // 渲染分数
// document.getElementById('score').innerHTML = '分数:' + game.score;
// 清屏
game.clear();
// 蛇的运动/更新
// 蛇的更新速度 当蛇边长的时候,速度加快
var during = game.snake.body.length < 30 ? 30 - game.snake.body.length : 1;
// 蛇的更新
game.f % during == 0 && game.snake.update();
// 渲染蛇
game.snake.render();
// 渲染食物
game.food.render();
}, 10);
})
到此这篇关于JS实现贪吃蛇小游戏的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持本站。