• 还记得2048怎么玩吗?快来玩会儿(摸鱼)吧


    相信大家都玩过 2048 吧!什么,你还没玩过??那就快来跟我一起玩一下吧,顺便了解一下如何使用 canvas + js 开发一款我们自己的 2048 吧!话不多说,let’s go~

    2048 试玩

    首先我们还是先来玩一下 2048,并且了解一下到底 2048 的相关规则。可以看一下下面的动态图片,应该就能了解 2048 的大致玩法了,如图:

    上图中,我们可以通过控制键盘的 上下左右 键来移动对于的数字,当两个数字相同时,它们就会合成更大的数字,例如 22 可以合成 4,同理越大的数字合成的数字也越大,当整个区域没有可以合成的数字后,游戏就结束了,这就是 2048 的大致规则了,下面我们就一起来看一下该如何实现一个自定义的 2048 吧!

    基础架子

    因为我们整个游戏是基于 canvas 来编写的,但是还需要有一个基础的架子,我们使用 html + css 来完成,大致的 html 代码如下:

    2048

    Size:

    Start
    Reset
    Score: 0Best Score: 0
    > Start <
    Game Over!
    • 1
    • 2

    在上面的代码中,我们通过在页面添加 input 输入框,让玩家可以自定义 2048 的游戏区域,通过输入不同的数字,可以生成不同的游戏区域,当然有个限制,最小是 3 块,最大是 10 块,并且还添加了相关的游戏数据,例如当前的游戏分数,以及当游戏结束时,我们会通过 localstorage 将最高分数记录在本地,这样当下次再玩时,如果获得的分数没有之前的分数高,则不会进行分数的更新,反之就会将最新的分数存储在本地并展示在页面上。

    有了基本的 html 架子还不够,还需要添加相关的 css 代码才能让我们的游戏有一个基本的样子,这里只截取部分 css 代码,完整的代码会在最后放出,css 相关的代码如下:

    ...
    #canvas{background: rgba(var(--blue),1);margin-top: 30px;box-sizing: border-box;
    }
    h1{font-size: 58px;color: white;margin-top: 30px;
    }
    .scores{width: 50%;display: flex;flex-direction: column;justify-content: center;align-items: flex-end;
    }
    .edits{min-width: 500px;width: 100%;display: flex;flex-direction: row;justify-content: space-between;align-content: center;margin-top: 30px;font-size: 24px;
    }
    .set-size{width: 100%;display: flex;justify-content: flex-start;align-items: center;flex-direction: row;
    }
    .size-block{display: flex;flex-direction: column;width: 50%;justify-content: center;align-items: flex-start;
    }
    .btns, #bestScore{margin-top: 15px;
    }
    .size-block input{margin-left: 10px;max-width: 50px;
    }
    #size{background: transparent;border: 2px solid white;color: white;font-size: 18px;padding: 5px 8px 5px 5px;width: 60px;outline: none;box-sizing: border-box;text-align: center;
    }
    .lose, .start2{z-index: 999;position: absolute;margin-top: 15px;font-size: 35px;display: none;
    }
    .start2{display: block;min-width: 150px;margin-top: 200px;cursor: pointer;
    }
    #canvas-block{position: relative;display: flex;justify-content: center;align-items: center;
    }
    .btns{display: flex;flex-direction: row;justify-content: flex-start;align-items: center;cursor: pointer;
    }
    .reset{margin-left: 15px;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    通过 htmlcss 的加成,最终的展示如下图所示:

    因为我们还没有添加相关的 js 代码,因此整个页面上除了一些静态的文字以外,别的基本都看不到了。

    基础的架子已经搭建好了,那么我们就来实现这个游戏吧!

    js + canvas 实现

    我们使用 canvas 的时候本身就是需要配合 js 来进行开发的,因为 canvas 只是一个标签元素,就跟普通的 div 元素一样,只是它上面提供了很多方法给我们使用。这里我们使用 ES6 相关的知识点来进行开发,如果对 ES6 还不熟悉的童鞋,可以点击这里进行学习。

    因为使用的是 ES6 ,因此我们直接使用 面向对象 的写法来开发这个游戏。在 ES6 中,它给我们提供了一个 class 语法糖,让我们可以不需要在构造函数的原型上去添加方法,不过本质上也还是一样的,这里不做深究。

    首先我们有一个 ,这个 的名字你可以随意定,这里就叫做 Game,然后需要在这个 构造函数 中初始化相关的内容,让我们可以在后续使用,代码如下:

    class Game {constructor() {this.canvas = document.getElementById('canvas');this.ctx = this.canvas.getContext('2d');this.sizeInput = document.getElementById('size');this.startBtn = document.querySelector('.start');this.startBtn2 = document.querySelector('.start2');this.scoreLabel = document.getElementById('score');this.resetBtn = document.querySelector('.reset');this.lessHtml = document.querySelector('.lose');this.scoreValue = 0;this.bestScore = document.getElementById('bestScore');this.bestScoreValue = localStorage.getItem('score2048');this.size = 4;this.width = this.canvas.width / this.size - 6;this.cells = [];this.fontSize = 0;this.loss = false;}
    } 
    
    • 1
    • 2

    在这个 构造函数 中,我们将页面中需要用到的元素通过 document.getElementById 或者 document.querySelector 选取到,这样后续就可以直接使用。

    基本的信息有了后,我们就该初始化游戏的相关信息了,代码如下:

    ...
    
    init() {this.startBtn.addEventListener('click', () => {this.publicEvent();});this.resetBtn.addEventListener('click', () => {this.scoreValue = 0;this.canvas.style.opacity = '1';this.loss = false;this.lessHtml.style.display = 'none';this.bestScoreValue = localStorage.getItem('score2048');this.scoreLabel.innerHTML = `Score: ${+this.scoreValue}`;this.startGame();this.initScore();});
    }
    
    initState() {this.initStart();this.initScore();this.initEvent();
    }
    
    initStart() {this.canvas.style.display = 'none';this.startBtn2.addEventListener('click', () => {this.publicEvent();});
    }
    
    publicEvent() {if (this.sizeInput.value >= 3 && this.sizeInput.value <= 10) {this.size = this.sizeInput.value;this.width = this.canvas.width / this.size - 6;this.canvasClear();this.startGame();this.canvas.style.display = 'block';this.startBtn2.style.display = 'none';} else {alert('不在生成的区间内,无法开始游戏');return;}
    }
    
    ... 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在初始化的过程中,我们需要给页面上的相关元素添加点击事件,包括开始游戏、重置游戏等内容,然后我们需要在 Game 的 构造函数,也就是上述的 constructor 中执行 init 方法,这样当整个 实例化 的时候,就会自动将各种初始化信息及绑定事件完成。

    当初始信息和绑定事件都完成后,页面中的 canvas 上目前还是没有任何内容显示的,因此我们就需要通过 canvas 相关的 api 来生成对应的方块和数字了。

    这里我们还需要一个新的 ,用来帮我们生成每一个小方块里面的初始值和它的位置信息,这样我们才能在 canvas 上画出来,小方块的生成代码如下:

    class Cell {constructor(row, col, width) {this.value = 0;this.x = col * width + 5 * (col + 1);this.y = row * width + 5 * (row + 1);}
    } 
    
    • 1
    • 2

    这个 很简单,只有三个属性 ,其中 value 默认为 0,这么做是为了后面初始化的时候根据不同的 value 生成不同颜色的方块。其次是 xy,这两个值主要是这个小方块在 canvas 中的位置信息,通过从外部传入的行、列以及宽度来生成。

    接下来我们只需要根据前面设置的 初始值 或者 input 中输入的值来生成对应的游戏区域即可,代码如下:

    async createCells() {for (let i = 0; i < this.size; i++) {this.cells[i] = [];for (let j = 0; j < this.size; j++) {this.cells[i][j] = new Cell(i, j, this.width);}}
    } 
    
    • 1
    • 2

    上述代码中,通过两个循环来创建小方块的位置信息,最终的数据如下图所示:

    通过上图可以看到我们已经生成了一个 4 * 4 的格子,并且里面的每一个小方块的 x轴y轴 信息都已经有了,接下来我们就可以通过上面的信息来生成一个基本的游戏区域了,生成的游戏区域如下图所示:

    那么我们是如何生成上面这样的游戏区域的呢?让我们一起来看代码,如下:

    ...other code
    
    async drawAllCells() {for (let i = 0; i < this.size; i++) {for (let j = 0; j < this.size; j++) {this.drawCell(this.cells[i][j]);}}
    }
    
    ...other code 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    通过调用 drawAllCells 方法,在内部循环执行 drawCell 方法,然后得到上图。我们一起来看一下 drawCell 内部是如何实现的,代码如下:

    drawCell(cell) {this.ctx.beginPath();this.ctx.rect(cell.x, cell.y, this.width, this.width);this.ctx.fillStyle = "#384081";this.ctx.fill();
    } 
    
    • 1
    • 2

    通过上述代码可以看到,我们直到现在才真正使用到了 canvas 上面提供的相关方法。首先我们要调用画布上面的 beginPath 方法,这个 API 主要用于 “作画” 的开始,因此它是必须的,只要使用 canvas 绘制,就一定需要这个方法;然后我们使用了 rect 方法,它主要用于绘制一个方块,其中有四个参数,前两个参数分别是需要绘制方块的 x轴坐标y轴坐标,后两个参数就是这个方块的 宽度高度,这些信息在前面我们都准备好了,因此这里就可以直接拿来用;最后我们需要给方块添加颜色,也就是 fillStyle,并完成收尾,也就是填充这个方块 fill,这样我们就得到了如上图所示的游戏区域。

    当我们将这些准备工作完成后,接下来就要实现当玩家点击开始后,游戏区域生成对应的小方块和数字了,那么该如何做呢?在前面我们初始化的时候,已经给相关的按钮添加了点击事件,并且绑定了一个 startGame 方法,让我们看一下这个方法内部的实现,如下:

     async startGame() {await this.createCells();await this.drawAllCells();await this.pasteNewCell();await this.pasteNewCell();
    } 
    
    • 1
    • 2

    这个方法很简单,只是调用了 createCellsdrawAllCells 方法用于生成基本的游戏区域,然后后面连续调用了两次 pasteNewCell 方法,那 pasteNewCell 内部实现了什么呢?让我们一起来看一下代码,如下:

    async pasteNewCell() {let countFree = 0;for (let i = 0; i < this.size; i++) {for (let j = 0; j < this.size; j++) {if (!this.cells[i][j].value) {countFree++;}}}if (!countFree) {this.finishGame();return;}while (true) {let row = Math.floor(Math.random() * this.size);let col = Math.floor(Math.random() * this.size);if (!this.cells[row][col].value) {this.cells[row][col].value = 2 * Math.ceil(Math.random() * 2);this.drawAllCells();return;}}
    } 
    
    • 1
    • 2

    pasteNewCell 内部,我们通过不断的循环来生成新的游戏方法,其中最主要的就是判断当前小方块的值是否为 0,如果不为 0,就会画一个新的方块展示在游戏区域内,反之则不执行。

    通过两次执行 pasteNewCell 方法,是为了在初始化的时候生成两个数字,这样才能开始游戏。而通过生成的不同方法内的值来生成不同颜色的小方法,这是如何实现的呢?还记得前面的 drawCell 方法吗?这里其实还有一部分代码之前没有放出来,我们一起来看看,如下:

    drawCell(cell) {this.ctx.beginPath();this.ctx.rect(cell.x, cell.y, this.width, this.width);this.ctx.fillStyle = "#384081";this.ctx.fill();if (cell.value) {this.ctx.fillStyle = `${this.cellColor(cell.value)}`;this.ctx.fill();this.fontSize = this.width / 2;this.ctx.font = this.fontSize + 'px Viga';this.ctx.fillStyle = 'white';this.ctx.textAlign = "center";this.ctx.fillText(cell.value, cell.x + this.width / 2, cell.y + this.width / 1.5);}
    }
    
    cellColor(value) {const colorList = new Map([[0, 'rgb(135,200,116)'],[2, 'rgb(135,200,116)'],[4, 'rgb(95,149,212)'],[8, 'rgb(139,89,177)'],[16, 'rgb(229,195,81)'],[32, 'rgb(202,77,64)'],[64, 'rgb(108,129,112)'],[128, 'rgb(207,126,63)'],[256, 'rgb(82,125,124)'],[512, 'rgb(191,76,134)'],[1024, 'rgb(119,41,92)'],[2048, 'rgb(118,179,194)'],[4096, 'rgb(52,63,79)'],]);return colorList.get(value) || 'rgba(70,80,161,0.8)';
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上述代码与前面的代码相比,通过判断当前的小方块的值是否为真,也就是当前小方块的值是否不为 0,从而重新生成一个新的小方块。在 drawCell 内部,通过新生成的小方块的 value 值来调用 cellColor 方法,在 cellColor 方法内,我们使用 ES6 中的 Map 方法来匹配到当前不同 value 所对应的颜色值,这样比直接使用 if…elseswitch…case… 要更加清晰明了。

    最后我们还需要添加相关的键盘绑定的事件,在最开始初始化的时候我们已经给 document 绑定了相关的键盘事件,代码如下:

    initEvent() {document.addEventListener('keydown', (event) => {if (!this.loss && this.canvas.style.display === 'block') {switch (event.key) {case 'ArrowUp':this.moveUp();break;case 'ArrowDown':this.moveDown();break;case 'ArrowLeft':this.moveLeft();break;case 'ArrowRight':this.moveRight();break;}this.scoreLabel.innerHTML = `Score: ${+this.scoreValue}`;}});
    } 
    
    • 1
    • 2

    在上述的键盘事件中,通过监听玩家按下的键盘方法键,从而让我们的小方块进行相关的合并,这里截取部分代码,如下:

    ...other code
    
    moveUp() {let row;for (let j = 0; j < this.size; j++) {for (let i = 1; i < this.size; i++) {if (this.cells[i][j].value) {row = i;while (row > 1) {if (!this.cells[row - 1][j].value) {this.cells[row - 1][j].value = this.cells[row][j].value;this.cells[row][j].value = 0;row--;} else if (this.cells[row][j].value === this.cells[row - 1][j].value) {this.cells[row - 1][j].value *= 2;this.scoreValue += this.cells[row - 1][j].value;this.cells[row][j].value = 0;break;} else {break;}}}}}this.pasteNewCell();
    }
    
    moveDown() {let row;for (let j = 0; j < this.size; j++) {for (let i = this.size - 2; i >= 0; i--) {if (this.cells[i][j].value) {row = i;while (row + 1 < this.size) {if (!this.cells[row + 1][j].value) {this.cells[row + 1][j].value = this.cells[row][j].value;this.cells[row][j].value = 0;row++;} else if (this.cells[row][j].value === this.cells[row + 1][j].value) {this.cells[row + 1][j].value *= 2;this.scoreValue += this.cells[row + 1][j].value;this.cells[row][j].value = 0;break;} else {break;}}}}}this.pasteNewCell();
    }...other code 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在上面的代码中,通过获取当前的方块移动的方法,找到它左、右、上或者下方的方块,从而判断它们的值是否一致,如果一致就可以进行合并,反之则不能进行合并和移动。当玩家的游戏区域内已经没有任何可移动的小方块时,整个游戏就结束了,最终游戏结束的代码如下:

    finishGame() {const currentScore = localStorage.getItem('score2048');if (currentScore < this.scoreValue) {localStorage.setItem('score2048', this.scoreValue);}this.canvas.style.opacity = '0.3';this.loss = true;this.lessHtml.style.display = 'block';
    } 
    
    • 1
    • 2

    当游戏结束时,就像一开始说的一样,我们会获取当前的值以及存储在 localStorage 中的值进行对比,如果当前的值比之前的值要大,则会将新的值更新到 localStorage 中,最后在页面中展示一个 Game Over 告诉玩家游戏结束了。

    最终整个游戏的实现在这里可以查看,也可以直接通过键盘玩耍,如下:

    最后

    通过上面对 ES6 的基本使用以及 canvas 中基础 API 的介绍,相信大家已经学会了如何开发一个 2048 小游戏了,那么就赶快动手实现一个你自己的 2048 吧!

  • 相关阅读:
    向量数据库,为什么是大模型的最佳拍档?
    矩阵分析与应用+张贤达
    四旋翼无人机学习第13节--Padstack Editor的简单使用
    数据结构之智能指针类
    Spring Security OAuth2 入门
    计算机视觉与深度学习实战,Python为工具,小波变换的数字水印技术
    DNS 系列(三):如何免受 DNS 欺骗的侵害
    java面试强基(3)
    【pen200-lab】10.11.1.13
    各报文段格式集合
  • 原文地址:https://blog.csdn.net/web2022050903/article/details/126889618