千家信息网

基于JS怎么实现消消乐游戏

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章主要讲解了"基于JS怎么实现消消乐游戏",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"基于JS怎么实现消消乐游戏"吧!游戏的准备工作首先我们思考
千家信息网最后更新 2026年11月02日基于JS怎么实现消消乐游戏

这篇文章主要讲解了"基于JS怎么实现消消乐游戏",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"基于JS怎么实现消消乐游戏"吧!

游戏的准备工作

首先我们思考游戏的机制: 游戏有一个"棋盘",是一个n*m的矩形。矩形中有若干个颜色(或者类型)的方块,相同类型的方块,在一个横行或者竖行,有3个或者3个以上时,便会消除。

在部分方块消除后,这些方块上方的方块便会下坠并补充这些消除方块的缺口,同时,上方又会生成新的方块来补充下坠方块的位置,在执行完上述步骤后,便完成了一个游戏过程的循环。

总结一下

一共有3个步骤,将生成一个游戏循环:消除,下坠,补充。在补充后,如果方块们无法自然消除,循环便会结束。这时候就需要玩家来交还两个相邻方块,来人为制造可以消除的情况,以重新进入循环。

如果玩家的交换并不能使得重新进入消除循环呢?那么这个交换将重新换回原样。

基本机制思考完毕,现在开始代码构建:

首先考虑到方块们会进行大量的动画过程(主要是四种:移动,消除,下坠,冒出),于是我们使用绝对定位来安排这些方块,并且在其行类样式当中添加属性:transition,用css来实现这些方块的动画。具体实现如下:

移动:通过left和top值的改变,控制方块的移动。

消除:通过修改transform,修改为scale(0),以实现消除的动画。

下坠:通过top值的改变,同移动。

冒出:通过修改transform,将本来为scale(0)的transform值修改为scale(1),以实现冒出的动画。

考虑到这些动画是一个接一个的执行,我们应该是需要使用异步来执行这些动画的,当然使用回调函数也能实现,但回调函数可能会很麻烦,所以我们使用Promise对象来解决这些麻烦。

废话太多了!现在开始写代码。

棋盘

首先是棋盘的实现,简单操作就定义了一个棋盘的整体结构出来。当然,给#app添加position:relative或者position:absolute是必不可少的。

    

接下来我们用面向对象的思想,构造棋盘的具体内容:

首先一个棋盘有它的宽度和高度(x和y),我们还同时还定义它的方块大小(size)。

matrix则为之后要用到的,存放不同type的矩阵,types则为所有的棋子种类。

除此之外,还有几个属性,这些之后再说。

class GameMap {        constructor(x, y, size) {                        this.x = x;                        this.y = y;                        this.size = size;                        this.matrix = [];                        this.useSwap = false;                        this.handleable = true;                        this.types = emojis.length;                }}

我们再来构造"棋子",棋子的属性很多,所以我们通过仅将options作为参数,并将options解构,来赋予棋子这些属性,这些属性分别是

class Cell {        constructor(options) {                const { position, status, type, left, top, right, bottom, instance } = options;                        this.type = type;                        this.position = position;                        this.status = status;                        this.top = top;                        this.bottom = bottom;                        this.left = left;                        this.right = right;                        this.instance = instance;                    }}

type 类型(颜色),number类型表示,相同的number即被视为同样的类型。

position 位置,用一个形如[m,n]的二维数组存储

status 状态,分为'common' 普通 'collapse' 崩塌 'emerge' 冒出,一共三种

top 棋子上方的棋子对象,值也是一个Cell实例,如果棋子上方没有棋子,那它就是undefined

left 棋子的左侧棋子对象

right 棋子的右侧棋子对象

bottom 棋子的下方棋子对象

instance 根据上述属性刻画出的真实的棋子的DOM对象,最终这些对象会在GameMap中展现出来

在这里我们使用emoji表情来展现这些棋子,我们定义全局变量emojis:

const emojis = ['????', '????', '????', '????', '????'];

渲染画面

有了棋盘和棋子,我们就能渲染棋盘了。

首先我们定义全局变量cells,用以存放棋盘中所有的棋子,所有的Cell类。

然后我们在GameMap中定义方法genMatrix(),初始化棋盘。根据棋盘宽度和高度(x和y)的配置,我们填好了一个x*y的点阵,不过现在这里面还没有内容。

     genMatrix() {                        const { x, y } = this;                        const row = new Array(x).fill(undefined);                        const matrix = new Array(y).fill(undefined).map(item => row);                        this.matrix = matrix;                        return this;                }

接下来的工作是用随机数填满点阵,我们定义方法genRandom()。

genRandom() {    const { x, y } = this;    this.matrix = this.matrix.map(row => row.map(item => Math.floor(Math.random() * this.types)));    return this;}

如图所示的一个点阵就此生成。我们再用这些点阵渲染出真实的画面。

定义方法init()

目前来看,init()的写法有些晦涩难懂,这也很正常,之后我们还会提到它。

                init() {                        cells = [];                        const { x, y } = this;                        for (let i = 0; i < y; i++) {                                for (let j = 0; j < x; j++) {                                        const type = this.matrix[i][j];                                        const random = Math.floor(Math.random() * this.types);                                        cells.push(new Cell({                                                type: (type == undefined) ? random : type,                                                position: [j, i],                                                status: (type == undefined) ? 'emerge' : 'common',                                                left: undefined,                                                top: undefined,                                                right: undefined,                                                bottom: undefined,                                                instance: undefined                                        }));                                }                        }                        cells.forEach(cell => {                                const [row, col] = cell.position;                                cell.left = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row - 1) && (_col == col);                                });                                cell.right = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row + 1) && (_col == col);                                });                                cell.top = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row) && (_col == col - 1);                                });                                cell.bottom = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row) && (_col == col + 1);                                });                                cell.genCell();                        });                        return this;                }

之前定义的全局变量cells,就用以存放所有的Cell类的实例。

对于一个新鲜生成的棋盘来说,所有的Cell的状态(status)都是common,其他情况我们之后会讲到。回到Cell类的构造过程,我们发现这一步完成了Cell实例的塑造,这些棋子将被进一步的加工为最后的游戏画面,而最后一步cell.genCell()则最终把这些抽象的类实体化。

genCell()方法是在Cell类中我们定义的方法,我们根据

genCell() {                const cell = document.createElement('div');                const size = gameMap.size;                const [x, y] = this.position;                cell.type = this.type;                cell.style.cssText =                        `                        width:${size}px;                        height:${size}px;                        left:${size * x}px;                        top:${size * y}px;                        box-sizing:border-box;                        border:5px solid transparent;                        transition:0.5s;                        position:absolute;                        transform:scale(${this.status == 'emerge' ? '0' : '1'});                        display:flex;                        justify-content:center;                        align-items:center                        `;cell[xss_clean] = `${emojis[this.type]}`;this.instance = cell;}

genCell根据init()之前对cell数据的定义和演算,生成了棋子,但目前,棋子尚未渲染到页面中,只是作为DOM对象暂存。

最后我们定义方法genCellMap()生成真实的游戏画面。

              genCellMap() {                        app[xss_clean] = '';                        cells.forEach(cell => {                                app.append(cell.instance);                        });                        return this;                }

遍历之前全局变量cells中的内容,找到每个Cell中的instance,再将这些instance挂载到#app上,一个游戏棋盘就跃然而出了。

这就是一个Cell实例的模样,其中的instance是一个实实在在的div。

游戏画面则是一个最简单的棋盘,没有做其他的美化修饰(其实是因为懒得写了)。因为考虑到emoji不像图片加载这么麻烦,同时,也不存在失真的情况,所以我们使用emoji来刻画这些棋子。

一个真实的instance的样子。

动画效果

之前我们已经明确了游戏的一个循环过程发送的三件事情消除,下坠,补充,所以说我们把这三件事情分别通过定义GameMap的三个方法来刻画,这三个方法分别为 :

genCollapse() genDownfall() genEmerge()

代码如下

genCollapse()

genCollapse() {                return new Promise((resolve, reject) => {                        this.handleable = false;                        this.markCollapseCells();                        setTimeout(() => {                                cells.forEach(cell => {                                        if (cell.status == 'collapse') {                                                cell.instance.style.transform = 'scale(0)';                                        }                                });                        }, 0);                        setTimeout(() => {                                resolve('ok');                        }, 500);                });        }

genCollapse的过程中还有一个步骤叫做markCollapseCells(),用以标记将会崩塌的棋子,该方法代码如下:

markCollapseCells() {        cells.forEach((cell) => {                const { left, right, top, bottom, type } = cell;                if (left?.type == type && right?.type == type) {                        left.status = "collapse";                        cell.status = "collapse";                        right.status = "collapse";                }                if (top?.type == type && bottom?.type == type) {                        top.status = "collapse";                        cell.status = "collapse";                        bottom.status = "collapse";                }        });        return this;}

遍历整个cells,如果一个棋子的左边右边和自己都为同一个类型,那他们仨的状态都会被标记为'collapse',同理,如果棋子的上面下面和自己为同一个类型,也会被标记。我们不害怕重复的情况,因为已经被标记的棋子,再被标记一次也无所谓。

标记完成后,我们便将这些被标记的Cell对象,他们的instance的style加入一项transform:scale(0),在transition的作用下,它们会逐步(实际上很快)萎缩为看不见的状态,实际上它们并没有因此消失。而在这一个逐步萎缩的过程,我们通过Promise的性质,来阻塞该方法的执行,以等待棋子萎缩完毕,再进入下一个过程:下坠。0.5s后抛出resolve,完成放行,顺利进入到下一个步骤。

genDownfall()

genDownfall() {        return new Promise((resolve, reject) => {                setTimeout(() => {                        cells.forEach(cell => {                                if (cell.status != 'collapse') {                                        let downfallRange = 0;                                        let bottom = cell.bottom;                                        while (bottom) {                                                if (bottom.status == 'collapse') {                                                        downfallRange += 1;                                                }                                                bottom = bottom.bottom;                                        }                                        cell.instance.style.top = (parseInt(cell.instance.style.top) + gameMap.size * downfallRange) + 'px';                                }                        });                }, 0);                setTimeout(() => {                        resolve('ok');                }, 500);        });}

genDownfall()的关键是我们需要得知,哪些棋子会下坠,这些棋子应该下坠多少距离。我们得知下坠距离后,再设置这些Cell中新的top值,同样是通过transition的效果,来制造下坠的动画。

首先明确什么棋子可能会下坠:

其实很简单,除开status为collapse的棋子都可能会下坠,但也不见得,比如最下方一排的棋子无论如何也不会下坠。

所以下一步就是计算它们下坠的距离:

这一步也不复杂,之前的Cell类中我们已经事先定义了棋子的bottom属性,我们只需要知道棋子下方有多少个状态为collapse的棋子,我们就知道该棋子会下坠多少距离了,距离=棋子的size*下方状态为collapse的棋子数量。

通过while大法,逐一查询该棋子下方棋子的状态,便能得到答案。

我们故伎重施,使用Promise的性质阻塞整个过程0.5s,再放行到下一步。

genEmerge()

genEmerge()的过程相比之下要复杂很多,因为此时棋盘已经被打乱,需要重塑整个棋盘,重塑完毕后,再将缺失的棋盘补充出来。补充出来的方法之前也提到了,就是scale(0)->scale(1),以得到一种勃勃生机,万物竞发的效果。

代码如下:

genEmerge() {        return new Promise((resolve, reject) => {                this.regenCellMap();                this.genCellMap();                setTimeout(() => {                        cells.forEach(cell => {                                if (cell.status == 'emerge') {                                        cell.instance.style.transform = 'scale(1)';                                }                        });                }, 0);                setTimeout(() => { resolve('ok'); }, 500);        });}

其中有一个步骤叫做regenCellMap(),该步骤代码如下

regenCellMap() {        const size = gameMap.size;        const findInstance = (x, y) => {                return cells.find(item => {                        const { offsetLeft, offsetTop } = item.instance;                        return (item.status != 'collapse' && (x == offsetLeft / size) && (y == offsetTop / size));                })?.instance;        };        this.genMatrix();        this.matrix = this.matrix.map((row, rowIndex) => row.map((item, itemIndex) => findInstance(itemIndex, rowIndex)?.type));        this.init();}

这其中关键的一步就是findInstance,我们要重新找到执行了downfall后的棋子它们的position是什么,并将它们的type和位移后的position一一对应,我们用这些重新一一对应的信息,重新构造matrix,以完成对整个棋盘的重新塑造。注意该方法的最后一步,init(),也就是说我们对棋盘重新进行了初始化,下面我们再来看init()的代码,或许你就能理解init()为什么这么写了。

重塑后的matrix长这样。

下坠的棋子填了消除棋子的坑,那上面的空隙也就出现了,数组的find方法,找不到内容,便会返回undefined,因此findInstance()在找不到棋子的时候,便会返回undefined,也因此将这一结果作用到了重塑的matrix上。

我们重新来看init()

             init() {                        cells = [];                        const { x, y } = this;                        for (let i = 0; i < y; i++) {                                for (let j = 0; j < x; j++) {                                        const type = this.matrix[i][j];                                        const random = Math.floor(Math.random() * this.types);                                        cells.push(new Cell({                                                type: (type == undefined) ? random : type,                                                position: [j, i],                                                status: (type == undefined) ? 'emerge' : 'common',                                                left: undefined,                                                top: undefined,                                                right: undefined,                                                bottom: undefined,                                                instance: undefined                                        }));                                }                        }                        cells.forEach(cell => {                                const [row, col] = cell.position;                                cell.left = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row - 1) && (_col == col);                                });                                cell.right = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row + 1) && (_col == col);                                });                                cell.top = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row) && (_col == col - 1);                                });                                cell.bottom = cells.find(_cell => {                                        const [_row, _col] = _cell.position;                                        return (_row == row) && (_col == col + 1);                                });                                cell.genCell();                        });                        return this;                }

注意这两句代码:

type: (type == undefined) ? random : type

status: (type == undefined) ? 'emerge' : 'common'

重塑后的棋盘,空隙的部位得以补充:首先它们的类型为随机生成,它们的状态也有别于common,而是emerge,在genEmerge()过程中它们将会出现。

整合效果

最复杂的工作目前已经完成,我们已经刻画出了整个游戏的核心循环,现在我们再用genLoop方法来整合这一系列过程,代码如下:

genLoop()

async genLoop() {        await gameMap.genCollapse();        let status = cells.some(cell => cell.status == 'collapse');        while (cells.some(cell => cell.status == 'collapse')) {                await gameMap.genDownfall();                await gameMap.genEmerge();                await gameMap.genCollapse();        }        gameMap.handleable = true;        return status;}

考虑到我们使用了Promise,所以我们将genLoop弄成异步函数。while循环将循环往复的执行这些过程,直到我们无法将任何的棋子状态标记为collapse。之前我们有个变量没提到,就是handleable,它决定了我们是否可以对棋盘进行互动。

genSwap()

最后我们考虑的是我们操纵棋盘的过程。实际上消消乐游戏中,需要我们交互的场景是很少的,大多数时间都是在观看动画,genSwap是我们交换两个棋子的过程。

genSwap(firstCell, secondCell) {        return new Promise((resolve, reject) => {                const { instance: c1, type: t1 } = firstCell;                const { instance: c2, type: t2 } = secondCell;                const { left: x1, top: y1 } = c1.style;                const { left: x2, top: y2 } = c2.style;                setTimeout(() => {                        c1.style.left = x2;                        c1.style.top = y2;                        c2.style.left = x1;                        c2.style.top = y1;                }, 0);                setTimeout(() => {                        firstCell.instance = c2;                        firstCell.type = t2;                        secondCell.instance = c1;                        secondCell.type = t1;                        resolve('ok');                }, 500);        });}

套路还是很明确的,先改变两枚棋子的left和top值,获得动画效果,0.5s后再重构Cell对象。

最后是游戏的执行代码:

const app = document.getElementById('app');const btn = document.getElementById('btn');const emojis = ['????', '????', '????', '????', '????'];let cells = [];let gameMap = new GameMap(6, 10, 50);gameMap.genMatrix().genRandom();gameMap.init().genCellMap();gameMap.genLoop();let cell1 = null;let cell2 = null;app.onclick = () => {        if (gameMap.handleable) {                const target = event.target[xss_clean];                const { left: x, top: y } = target.style;                const _cell = cells.find(item => item.instance == target);                if (!gameMap.useSwap) {                        target.className = 'active';                        cell1 = _cell;                } else {                        cell2 = _cell;                        cell1.instance.className = '';                        if (['left', 'top', 'bottom', 'right'].some(item => cell1[item] == cell2)) {                                (async () => {                                        await gameMap.genSwap(cell1, cell2);                                        let res = await gameMap.genLoop();                                        if (!res) {                                                await gameMap.genSwap(cell1, cell2);                                        }                                })();                        }                }                gameMap.useSwap = !gameMap.useSwap;        }};

实际上主要的工作是花在构造GameMap和Cell两个类上面,清楚的构造了两个类之后,剩下的工作就不那么复杂了。

这里的点击事件稍微复杂一点,因为实际上消消乐当中只有相邻的两个单元格才能交换,并且交换后还需与判断是否有单元格会因此崩塌,如果没有,这个交换将被重置。app.onclick当中便刻画了这个效果。

感谢各位的阅读,以上就是"基于JS怎么实现消消乐游戏"的内容了,经过本文的学习后,相信大家对基于JS怎么实现消消乐游戏这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

棋子 棋盘 方块 方法 过程 对象 代码 动画 状态 循环 标记 类型 就是 属性 生成 内容 效果 步骤 复杂 两个 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网络科技 中国互联网科技工作委员会毛军 科技互联网产品 上海互联网科技有限公司 北京互联网科技有限公司 互联网养老 桔豆科技 恒生科技互联网指数分析 开启互联网科技时代小说 互联网之后的科技是什么 福州新华互联网科技学校 知乎 金融科技与互联网金融相同和不同 互联网科技教育视频 互联网对科技的发展 辽宁互联网新科技 中国互联网十大科技股排名 工业互联网等科技实现智能化赋能 领航科技大厦互联网公司 全球科技与互联网公司市值变化 广西科技产业互联网是什么 唐山双旺合互联网科技产业园 安徽鑫晟辉互联网科技 苏州互联网科技贷款公司 腾讯科技 互联网女皇 海南购全球互联网科技 中国互联网有什么科技含量 移动互联网科技股 赶超互联网科技 微信多开 互联网数据科技专业 工业互联网科技成果转化 互联网的黑科技实验室
0