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

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

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

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

2048 试玩

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

123.gif

上图中,我们可以通过控制键盘的 上下左右 键来移动对于的数字,当两个数字相同时,它们就会合成更大的数字,例如 22 可以合成 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'>&gt; Start &lt;</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;
}
复制代码

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

image.png

因为我们还没有添加相关的 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 生成不同颜色的方块。其次是 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);
        }
    }
}
复制代码

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

image.png

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

image.png

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

...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();
}
复制代码

这个方法很简单,只是调用了 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;
        }
    }
}
复制代码

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...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}`;
        }
    });
}
复制代码

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

...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 吧!

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

分类:
前端