我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
相信大家都玩过 2048 吧!什么,你还没玩过??那就快来跟我一起玩一下吧,顺便了解一下如何使用 canvas
+ js
开发一款我们自己的 2048 吧!话不多说,let's go~
2048 试玩
首先我们还是先来玩一下 2048,并且了解一下到底 2048 的相关规则。可以看一下下面的动态图片,应该就能了解 2048 的大致玩法了,如图:
上图中,我们可以通过控制键盘的 上下左右 键来移动对于的数字,当两个数字相同时,它们就会合成更大的数字,例如 2 和 2 可以合成 4,同理越大的数字合成的数字也越大,当整个区域没有可以合成的数字后,游戏就结束了,这就是 2048 的大致规则了,下面我们就一起来看一下该如何实现一个自定义的 2048 吧!
基础架子
因为我们整个游戏是基于 canvas
来编写的,但是还需要有一个基础的架子,我们使用 html
+ css
来完成,大致的 html
代码如下:
<div class='container'>
<div class='fluid'>
<h1>2048</h1>
<div class='edits'>
<div class='size-block'>
<div class='set-size'>
<p id='size-title'>Size: </p>
<input id='size' type='number' value='4'>
</div>
<div class='btns'>
<div class='btn start'>Start</div>
<div class='btn reset'>Reset</div>
</div>
</div>
<div class='scores'>
<span id='score'>Score: 0</span>
<span id='bestScore'>Best Score: 0</span>
</div>
</div>
<div id='canvas-block'>
<div class='start2'>> Start <</div>
<div class='lose'>Game Over!</div>
<canvas id='canvas' width='500' height='500'></canvas>
</div>
</div>
</div>
复制代码
在上面的代码中,我们通过在页面添加 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;
}
复制代码
通过 html
和 css
的加成,最终的展示如下图所示:
因为我们还没有添加相关的 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;
}
}
复制代码
在这个 构造函数 中,我们将页面中需要用到的元素通过 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;
}
}
...
复制代码
在初始化的过程中,我们需要给页面上的相关元素添加点击事件,包括开始游戏、重置游戏等内容,然后我们需要在 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);
}
}
复制代码
这个 类 很简单,只有三个属性 ,其中 value
默认为 0,这么做是为了后面初始化的时候根据不同的 value 生成不同颜色的方块。其次是 x 和 y,这两个值主要是这个小方块在 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);
}
}
}
复制代码
上述代码中,通过两个循环来创建小方块的位置信息,最终的数据如下图所示:
通过上图可以看到我们已经生成了一个 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
复制代码
通过调用 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();
}
复制代码
通过上述代码可以看到,我们直到现在才真正使用到了 canvas
上面提供的相关方法。首先我们要调用画布上面的 beginPath 方法,这个 API 主要用于 “作画” 的开始,因此它是必须的,只要使用 canvas
绘制,就一定需要这个方法;然后我们使用了 rect
方法,它主要用于绘制一个方块,其中有四个参数,前两个参数分别是需要绘制方块的 x轴坐标 和 y轴坐标,后两个参数就是这个方块的 宽度 和 高度,这些信息在前面我们都准备好了,因此这里就可以直接拿来用;最后我们需要给方块添加颜色,也就是 fillStyle,并完成收尾,也就是填充这个方块 fill,这样我们就得到了如上图所示的游戏区域。
当我们将这些准备工作完成后,接下来就要实现当玩家点击开始后,游戏区域生成对应的小方块和数字了,那么该如何做呢?在前面我们初始化的时候,已经给相关的按钮添加了点击事件,并且绑定了一个 startGame 方法,让我们看一下这个方法内部的实现,如下:
async startGame() {
await this.createCells();
await this.drawAllCells();
await this.pasteNewCell();
await this.pasteNewCell();
}
复制代码
这个方法很简单,只是调用了 createCells 和 drawAllCells 方法用于生成基本的游戏区域,然后后面连续调用了两次 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;
}
}
}
复制代码
在 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)';
}
复制代码
上述代码与前面的代码相比,通过判断当前的小方块的值是否为真,也就是当前小方块的值是否不为 0,从而重新生成一个新的小方块。在 drawCell 内部,通过新生成的小方块的 value 值来调用 cellColor 方法,在 cellColor 方法内,我们使用 ES6 中的 Map
方法来匹配到当前不同 value 所对应的颜色值,这样比直接使用 if...else 或 switch...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}`;
}
});
}
复制代码
在上述的键盘事件中,通过监听玩家按下的键盘方法键,从而让我们的小方块进行相关的合并,这里截取部分代码,如下:
...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
复制代码
在上面的代码中,通过获取当前的方块移动的方法,找到它左、右、上或者下方的方块,从而判断它们的值是否一致,如果一致就可以进行合并,反之则不能进行合并和移动。当玩家的游戏区域内已经没有任何可移动的小方块时,整个游戏就结束了,最终游戏结束的代码如下:
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';
}
复制代码
当游戏结束时,就像一开始说的一样,我们会获取当前的值以及存储在 localStorage
中的值进行对比,如果当前的值比之前的值要大,则会将新的值更新到 localStorage
中,最后在页面中展示一个 Game Over 告诉玩家游戏结束了。
最终整个游戏的实现在这里可以查看,也可以直接通过键盘玩耍,如下:
最后
通过上面对 ES6 的基本使用以及 canvas
中基础 API 的介绍,相信大家已经学会了如何开发一个 2048 小游戏了,那么就赶快动手实现一个你自己的 2048 吧!
最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家