# 如何写一个 2048 小游戏? # 一、2048 是什么 2048 是一款滑块类游戏,由意大利程序员 Gabriele Cirulli 编写并在 GitHub 上开源。
游戏在一个 4 x 4 的网格上通过上下左右键来进行移动,方块在移动时会被边缘和已经有的方块所阻碍,但如果两个相同的方块在移动时碰撞,它们就会合并成一个新的方块,合并后的方块的值是原来的两倍。
# 二、为什么写 2048 小游戏 其实 2048 在我大二的那个暑假我就用原生的 JavaScript 写完了,这次是同事浏览我的 GitHub 库。
对话如下:
「你还写过 2048 ?」
「是啊」
「可以玩吗?」
「不能。。。」
为了能够让互联网的用户都能玩上,于是我花了几天的业余时间,将原生的 JavaScript 移植成 Vue3 版本,并且加上了排行榜。
大学时候想加排行榜,当时不会 NodeJs 也不会 Java,不想写 PHP,于是排行榜没做成,现在算是弥补某种遗憾了。
# 三、开发思路与步骤 游戏设计的核心是数值,其实 2048 游戏就是一个 4x4 二维数组上的数值变化。
# 3.1 布局 布局采用了绝对定位,每个方块会有各自的一个位置。
v-for="(cell, columnIndex) in row"
:key="rowIndex + columnIndex"
class="grid-cell"
:id="'grid-cell-' + rowIndex + '-' + columnIndex">
class="number-cell" :id="'number-cell-' + rowIndex + '-' + columnIndex" v-show="shouldShowCell(rowIndex, columnIndex)" v-text="cell">
123456789101112131415161718192021# 3.2 UI 设计 方块的值不同,背景颜色和字体颜色要改变,如果超过了 1000 要更改字体大小。
# 3.3 游戏逻辑 # 3.3.1 开始游戏 添加键盘事件 document.addEventListener('keyup', processKeyUp);
1以便之后通过上下左右来移动滑块。
初始化网格 初始化网格位置,方块的值全部置为 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);
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869# 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;
}
}
1234567891011121314151617181920212223242526272829303132333435判断是否可以向上移动 比如点击了上键,所有的方块都要向上移动,这时要判断可不可以向上移动,其他方向也是类似。
点击上键,第一行不变,因此从第二行开始,对数值不为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;
}
1234567891011121314151617181920212223对方块移动进行处理 先说垂直移动时的障碍物判断,比如说从最下方上移到最上方,只要中间有一个障碍物,就不可以移动过去。
/**
* 判断垂直障碍物是否存在
* 在同一列的 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;
}
123456789101112131415方块移动的核心代码 这里就是整个 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;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344# 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;
}
1234567891011121314判断是否可以移动
/**
* 判断是否可以移动
*
* @returns false 可以移动, true 无法移动
*/
function noMove() {
// 只要有一个方向可以移动,就能移动
if (canMoveDown() || canMoveLeft() || canMoveRight() || canMoveUp()) {
return false;
}
// 无法移动
return true;
}
1234567891011121314# 四、游戏结果
排行榜
我只玩到了第十名,他们实在太卷了,最后感谢 SAKURA和其他同学的大力捧场,谢谢大家的支持。
GitHub 仓库地址:https://github.com/stevenling/vue-2048 (opens new window)
游戏体验地址:http://2048.yunhu.wiki/ (opens new window)