# 如何写一个 2048 小游戏?
# 一、2048 是什么
2048
是一款滑块类游戏,由意大利程序员 Gabriele Cirulli
编写并在 GitHub
上开源。
游戏在一个 4 x 4
的网格上通过上下左右键来进行移动,方块在移动时会被边缘和已经有的方块所阻碍,但如果两个相同的方块在移动时碰撞,它们就会合并成一个新的方块,合并后的方块的值是原来的两倍。
# 二、为什么写 2048 小游戏
其实 2048
在我大二的那个暑假我就用原生的 JavaScript
写完了,这次是同事浏览我的 GitHub
库。
对话如下:
「你还写过 2048 ?」
「是啊」
「可以玩吗?」
「不能。。。」
为了能够让互联网的用户都能玩上,于是我花了几天的业余时间,将原生的 JavaScript
移植成 Vue3
版本,并且加上了排行榜。
大学时候想加排行榜,当时不会 NodeJs
也不会 Java
,不想写 PHP
,于是排行榜没做成,现在算是弥补某种遗憾了。
# 三、开发思路与步骤
游戏设计的核心是数值,其实 2048
游戏就是一个 4x4
二维数组上的数值变化。
# 3.1 布局
布局采用了绝对定位,每个方块会有各自的一个位置。
<div id="grid-container">
<div>
<div v-for="(row, rowIndex) in chessBoard" :key="rowIndex">
<div
v-for="(cell, columnIndex) in row"
:key="rowIndex + columnIndex"
class="grid-cell"
:id="'grid-cell-' + rowIndex + '-' + columnIndex"></div>
</div>
<div v-for="(row, rowIndex) in chessBoard" :key="rowIndex">
<div v-for="(cell, columnIndex) in row" :key="columnIndex">
<span
class="number-cell"
:id="'number-cell-' + rowIndex + '-' + columnIndex"
v-show="shouldShowCell(rowIndex, columnIndex)"
v-text="cell"></span>
</div>
</div>
</div>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2 UI 设计
方块的值不同,背景颜色和字体颜色要改变,如果超过了 1000
要更改字体大小。
# 3.3 游戏逻辑
# 3.3.1 开始游戏
- 添加键盘事件
document.addEventListener('keyup', processKeyUp);
以便之后通过上下左右来移动滑块。
- 初始化网格
初始化网格位置,方块的值全部置为 0
,模板上通过 v-show
让方块值为 0
的不显示。
- 在网格上生成两个随机位置,值是
2
或者4
。
条件:
- 网格上还存在空间,没有空间就无法生成。
- 随机位置不能是已经有值的位置,我们是生成,不是更新。
- 对随机位置生成的方块添加背景颜色和字体颜色。
/**
* 判断棋盘中还有空间吗
*
* @returns true->还有空间, false->没有
*/
function isChessBoardExistSpace() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// 还有空间
if (chessBoard[i][j] === 0) {
return false;
}
}
}
return true;
}
/**
* 生成数字
*/
function showNumberWithAnimation(i, j, randNumber) {
let numberCell = document.getElementById('number-cell-' + i + '-' + j);
if (numberCell) {
numberCell.style.top = getPosTop(i, j) + 'px';
numberCell.style.left = getPosLeft(i, j) + 'px';
numberCell.style.width = '100px';
numberCell.style.height = '100px';
// 获取随机数值的背景颜色和字体颜色
numberCell.style.backgroundColor = getNumberBackgroundColor(randNumber);
numberCell.style.color = getNumberColor(randNumber);
numberCell.textContent = randNumber;
}
}
/**
* 随机生成数字
*
* @returns
*/
function generateOneNumber() {
// 棋盘中还有空间就生成数字
if (isChessBoardExistSpace()) {
// 没有空间返回 false
return false;
}
// 随机生成 0-4 的位置不包括 4
let randX = parseInt(Math.floor(Math.random() * 4));
let randY = parseInt(Math.floor(Math.random() * 4));
// 判断位置是否可用
while (true) {
if (chessBoard[randX][randY] === 0) {
// 位置可用跳出死循环,不可用继续找
break;
}
randX = parseInt(Math.floor(Math.random() * 4));
randY = parseInt(Math.floor(Math.random() * 4));
}
// 随机生成 2 或 4,它们的概率相同
var randNumber = Math.random() > 0.5 ? 2 : 4;
// 在随机位置显示随机数字 2 或 4
chessBoard[randX][randY] = randNumber;
showNumberWithAnimation(randX, randY, randNumber);
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 3.3.2 移动方块
- 点击上下左右按键
/**
* 点击上下左右按键
*/
function processKeyUp(e) {
var keyCode = e.keyCode;
// 点击上键
if (keyCode === 38) {
if (moveUp()) {
afterMove();
}
return;
}
// 点击左键
if (keyCode === 37) {
if (moveLeft()) {
afterMove();
}
return;
}
if (keyCode === 39) {
if (moveRight()) {
afterMove();
}
return;
}
if (keyCode === 40) {
if (moveDown()) {
afterMove();
}
return;
}
}
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
30
31
32
33
34
35
- 判断是否可以向上移动
比如点击了上键,所有的方块都要向上移动,这时要判断可不可以向上移动,其他方向也是类似。
点击上键,第一行不变,因此从第二行开始,对数值不为0
的方块都去判断可不可以移动到它的上一个方块,有两种可能性:
- 上面那个方块格子的值为
0
,表明为空,因此可以移动 - 上面那个方块格子的值与我当前的值相同,表示合并,因此可以移动。
/**
* 判断是否能向上移动
* 1. 上面那个格子的值为 0,表示是空的,因此可以移动过去
* 2. 上面那个格子的值与当前值相同,表示可以合并,因此也可以移动过去
*
* @return 可以返回 true, 不能返回 false
*/
function canMoveUp() {
for (let i = 1; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (chessBoard[i][j] !== 0) {
if (
chessBoard[i - 1][j] === 0 ||
chessBoard[i - 1][j] === chessBoard[i][j]
) {
// 可以向上移动
return true;
}
}
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 对方块移动进行处理
先说垂直移动时的障碍物判断,比如说从最下方上移到最上方,只要中间有一个障碍物,就不可以移动过去。
/**
* 判断垂直障碍物是否存在
* 在同一列的 startRow 和 endRow 中间只要有一个值为 0,说明垂直方向存在障碍物
*
* @return 存在 false, 不存在 true
*/
function noBlockVer(col, startRow, endRow) {
for (let i = startRow + 1; i < endRow; i++) {
// 存在障碍物
if (chessBoard[i][col] !== 0) {
return false;
}
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 方块移动的核心代码
这里就是整个 2048
最核心的代码了,如果上侧为空,并且中间不存在障碍物,那么直接移动过去。如果上侧的值与当前方块的值相同并且中间不存在障碍物,那么移动过去进行合并,更新分数。
这边都没什么问题,问题在于细节上的 bug
。
一种情况:
往上移动时,会出现一种 bug
情况:
实际上,我们想要的:
早上很早起来打算修复这个 bug
,发现按了葫芦起了瓢,改动了,其他列会受影响。
晚上回来的时候,改了一会没改成,有点生气,我连一个二维数组的算法都搞不定的话,干脆改行算了。
我认真思索了一下,变成 8
,是因为从 [2 2 4 0]
变成 [4 0 4 0]
,然后底下的那个 4
又往上移动合并成了 8
,因此要定义一个一维数组
canMoveTopRowIndex
表示每一列上的方块所可以移动到最上方的行索引,刚开始是 0
,表明可以移动到第一行。
一旦上方有发生合并,就不能是 0
了,这一列上的最上方的变为 1
,这时候 [4 0 4 0]
就只能变成 [4 4 0 0 ]
,也就是我们想要的效果了。
/**
* 往上移动
*/
function moveUp() {
if (!canMoveUp()) {
// 如果不能移动
return false;
}
// 每一列上的方块可以移动到最顶端的那个行索引
let canMoveTopRowIndex = [0, 0, 0, 0];
for (let rowIndex = 1; rowIndex < 4; rowIndex++) {
for (let columnIndex = 0; columnIndex < 4; columnIndex++) {
if (chessBoard[rowIndex][columnIndex] !== 0) {
for (let k = canMoveTopRowIndex[columnIndex]; k < rowIndex; k++) {
if (
chessBoard[k][columnIndex] === 0 &&
noBlockVer(columnIndex, k, rowIndex)
) {
// 上侧为空,不存在障碍物
showMoveAnimation(rowIndex, columnIndex, k, columnIndex);
// 移动过去
chessBoard[k][columnIndex] = chessBoard[rowIndex][columnIndex];
// 之前的消失
chessBoard[rowIndex][columnIndex] = 0;
continue;
} else if (
chessBoard[k][columnIndex] === chessBoard[rowIndex][columnIndex] &&
noBlockVer(columnIndex, k, rowIndex)
) {
showMoveAnimation(rowIndex, columnIndex, k, columnIndex);
chessBoard[k][columnIndex] = 2 * chessBoard[rowIndex][columnIndex];
chessBoard[rowIndex][columnIndex] = 0;
score.value = score.value + chessBoard[k][columnIndex];
canMoveTopRowIndex[columnIndex] = k + 1;
continue;
}
}
}
}
}
return true;
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 3.3.3 游戏结束的判断
游戏结束要同时满足以下两个条件
- 方格上没有空白空间了
- 方格上的所有点都不能移动了,上下左右四个方向都不能移动了。
空白空间的判断
/**
* 判断棋盘中还有空间吗
*/
function noSpace() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// 还有空间
if (chessBoard[i][j] === 0) {
return false;
}
}
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
判断是否可以移动
/**
* 判断是否可以移动
*
* @returns false 可以移动, true 无法移动
*/
function noMove() {
// 只要有一个方向可以移动,就能移动
if (canMoveDown() || canMoveLeft() || canMoveRight() || canMoveUp()) {
return false;
}
// 无法移动
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 四、游戏结果
排行榜
我只玩到了第十名,他们实在太卷了,最后感谢 SAKURA
和其他同学的大力捧场,谢谢大家的支持。
GitHub
仓库地址:https://github.com/stevenling/vue-2048 (opens new window)