JS实现贪吃蛇小游戏:基于中介者模式的结构化开发

作者:闭包旅人

时间:2026-10-24

浏览:312

本文详解如何使用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实现贪吃蛇小游戏的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章