开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第五天,点击查看活动详情
线上地址:mygame.codeape.site
小游戏管理平台:juejin.cn/post/714916…
开发技术
vue2 + js + canvas
需求
方向键上下左右可以控制数字往一个方向移动,当数字两两相同可以进行合并,例:2+2=4。当棋盘内无法继续移动时,则游戏结束。
代码讲解
初始化数据
获取 canvas 上下文,定义棋盘内盒子的宽高(4*4),紧接着为棋盘定义一个二维数组并赋值为0,然后是创建出两个随机数字,然后是绘画界面和绘画出这两个数字。最后监听键盘事件。
init() {
setTimeout(() => {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext("2d");
// 获取画布内盒子的大小和间距
this.boxWidth = (this.canvas.width * 0.8) / 4;
this.boxMargin = (this.canvas.width * 0.2) / 5;
// 初始化二维数组并赋值为 0
for (let i = 0; i < 4; i++) {
this.boxArr.push([]);
for (let j = 0; j < 4; j++) {
this.boxArr[i][j] = 0;
}
}
this.createRandom();
this.createRandom();
this.drawInterface();
this.drawText();
this.onKeyDown();
}, 100);
},
创建数字2
在4*4方格的随机位置创建一个数字2,给x和y一个0-3的随机整数即可,然后如果创建出来的位置没有值,则代表该位置可以创建数字2,否则递归调用即可。
createRandom() {
let x = Math.round(Math.random() * 3);
let y = Math.round(Math.random() * 3);
this.boxArr[x][y] == 0
? ((this.boxArr[x][y] = 2), (this.score += 2))
: this.createRandom();
},
画界面
在 canvas 画板中填充上一层背景色,紧接着遍历数组,计算出每个需要绘画的格子的位置和对于的颜色,紧接着级就可以绘画格子了。
drawInterface() {
this.ctx.beginPath();
this.ctx.fillStyle = "#576eb9";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
let color, x, y;
// 遍历数组 辨认数组的当前值 并赋上颜色
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
color = this.boxColor[this.boxArr[i][j]];
x = this.boxMargin + j * (this.boxWidth + this.boxMargin);
y = this.boxMargin + i * (this.boxWidth + this.boxMargin);
this.drawRect(x, y, color);
}
}
},
画格子
绘画格子,我这里画的不是一个简单的正方形格子,而是一个带圆角的格子;我这里用到了路径的画法来实现。首先是创建一个路径 beginPath,然后确定一个初始的绘制点 moveTo,最后就是四个角的绘制,通过 arcTo 来绘制4个弧度,然后这一个路径就完成了,即完成了一个带圆角的矩形。
drawRect(x, y, color) {
this.ctx.beginPath(); // 1.创建路径
this.ctx.fillStyle = color;
this.ctx.moveTo(x, y); // 2.移动绘制点
let w = this.boxWidth;
let m = this.boxMargin;
// 这是画一个矩形的四个圆角
// arcTo(起点x, y, 终点x, y, 半径) 分别是 右上 右下 左下 左上
this.ctx.arcTo(x + w, y, x + w, y + 1, m * 0.7);
this.ctx.arcTo(x + w, y + w, x + w - 1, y + w, m * 0.7);
this.ctx.arcTo(x, y + w, x, y + w - 1, m * 0.7);
this.ctx.arcTo(x, y, x + 1, y, m * 0.7);
this.ctx.fill();
},
画文字
遍历每个格子,根据里面的值来绘画对应的数字,计算出该文字应在方格中的位置,然后通过 fillText 方法来给画布的该位置绘制一个数字即可。
drawText() {
let x, y;
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.boxArr[i][j] > 0) {
this.ctx.beginPath();
this.ctx.textAlign = "center";
this.ctx.textBaseline = "middle";
this.ctx.fillStyle = "#3a5075";
this.ctx.font = "40px Arial";
x =
this.boxMargin +
j * (this.boxWidth + this.boxMargin) +
this.boxWidth / 2;
y =
this.boxMargin +
i * (this.boxWidth + this.boxMargin) +
this.boxWidth / 2;
// 在画布上对应的位置写上二维数组中的值
this.ctx.fillText(this.boxArr[i][j], x, y);
}
}
}
},
监听用户键盘事件
onKeyDown() {
document.onkeydown = (e) => {
switch (e.code) {
case "ArrowLeft":
this.accordingKey("left");
break;
case "ArrowUp":
this.accordingKey("up");
break;
case "ArrowRight":
this.accordingKey("right");
break;
case "ArrowDown":
this.accordingKey("down");
break;
}
};
},
代码中的复杂点
根据按键,处理方格中的数字
这里当时水平有限,我记得处理了很久😂。
首先是一个判断,防止一直重复按一个方向,我这里的最大次数限制为5;
然后就是循环4次,代表当前方向上的的4行格子,再遍历每一行的4列格子,根据上下左右的操作来区分出对数组索引的操作。
这里需要调用 checkSorted 判断是否已经排列好了,没排列好就调用 moveBoxArr 进行排列,如果排列好了,即可进行给当前的方格二维数组赋值。
紧接着就是调用 isGameOver 判断游戏是否结束,未结束就清空画板,再创建一个数字2,再进行界面和数字的绘画。
accordingKey(key) {
this.$store.commit("setCurrentKey", key);
// 处理按键,防止一直按一个键位
if (this.currentKey.key == key) {
this.currentKey.num++;
} else {
this.currentKey.key = key;
this.currentKey.num = 0;
}
if (this.currentKey.num > 5) {
this.$message({
showClose: true,
message: "该方向按了很多次了,试试别的方向吧",
duration: "1500",
offset: 10,
});
} else {
for (let i = 0; i < 4; i++) {
var arr = new Array();
let x, y;
key == "left" || key == "right" ? (x = i) : "";
key == "up" || key == "down" ? (y = i) : "";
// 由于是向左操作,将每行的数字都给一个数组,用于操作判断
for (let j = 0; j < 4; j++) {
key == "left" || key == "right" ? (y = j) : "";
key == "up" || key == "down" ? (x = j) : "";
arr[j] = this.boxArr[x][y];
}
// 如果点击的是右或下就 将数组反序排列
key == "right" || key == "down" ? arr.reverse() : "";
// 数字还没操作好就去操作
if (!this.checkSorted(arr)) arr = this.moveBoxArr(arr);
key == "right" || key == "down" ? arr.reverse() : "";
// 重新赋值
for (let j = 0; j < 4; j++) {
key == "left" || key == "right" ? (y = j) : "";
key == "up" || key == "down" ? (x = j) : "";
this.boxArr[x][y] = arr[j];
}
}
if (this.isGameOver() == 1) {
this.$message({
showClose: true,
message: "该方向已经无法移动了,试试别的方向吧",
duration: "1500",
// Message 距离窗口顶部的偏移量
offset: 10,
});
} else if (this.isGameOver() == 0) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.createRandom();
this.drawInterface();
this.drawText();
} else {
this.gameOver = true;
}
}
},
checkSorted 判断当前行或列是否已经排列好
我这里用了个笨方法,就是根据当前传入的数组来判断该列是否排列好,一列一列(or一行一行)的比较值,然后先处理0的情况,当该列(or行)存在0时,其余条件是否满足。
紧接着就是判断相邻的数字是否相等,相等则代表还未排列好。
这里当时好像借鉴了网上的一些代码,由于时间是一年多之前的了,我也忘了是哪找的了。😂
checkSorted() {
return (arr) => {
let flag = false;
// 先处理 0 的情况
if (
(arr[0] == 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0) ||
(arr[0] > 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0) ||
(arr[0] > 0 && arr[1] > 0 && arr[2] == 0 && arr[3] == 0) ||
(arr[0] > 0 && arr[1] > 0 && arr[2] > 0 && arr[3] == 0) ||
(arr[0] > 0 && arr[1] > 0 && arr[2] > 0 && arr[3] > 0)
) {
flag = true;
}
// 如果有一个相邻数字是相等的就是还没排列好
if (
(arr[0] == arr[1] && arr[0] != 0) ||
(arr[1] == arr[2] && arr[1] != 0) ||
(arr[2] == arr[3] && arr[2] != 0)
) {
flag = false;
}
return flag;
};
},
moveBoxArr 移动数字并合并
根据传入的数组,遍历当前的列(or行),如果该值为0,则需要和另一个值交换位置(即可实现值移动了的效果)。
如果相邻的两个数字相同,就让后一个的值加到前一个值(即可实现合并的效果)。
最后还需判断当前的移动是否已经排列好值了,如果没排列好就继续将当前数组传入递归调用,排列好了就返回数组。
moveBoxArr(arr) {
for (let i = 0; i < 3; i++) {
// 0和其他数字交换位置
if (arr[i] == 0) {
let temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
// 如果两个数字相同就相加
if (arr[i] != 0 && arr[i] == arr[i + 1]) {
arr[i] = arr[i] + arr[i + 1];
this.score += arr[i];
// 合出128就有奖励分
arr[i] == 128 ? (this.reward += arr[i]) : "";
arr[i + 1] = 0;
}
}
// 如果当前已经排好序就返回数组,否处继续排序
if (this.checkSorted(arr)) {
return arr;
} else {
return this.moveBoxArr(arr);
}
},
isGameOver 判断游戏是否已经结束
首先是判断当前页面是否已经铺满数字了,如果未铺满,则返回0,代表游戏还能继续。
isFullNum() {
for (let i = 0; i < 4; i++) {
if (this.boxArr[i].indexOf(0) != -1) {
return false;
}
}
return true;
},
如果页面已经铺满数字的话,还需判断是否存在相邻的数字还有相等的,就返回1,代表某个方向还能进行移动操作。游戏还未结束。
如果以上条件都不满足则返回2,代表游戏结束。
// 0:数字还没铺满。 1:铺满了但相邻数还有相等的。 2:无法移动了游戏结束。
isGameOver() {
// 页面铺满数字的前提
if (this.isFullNum()) {
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
// 如果相邻的数(每个数的右和下数字)还相等就表示游戏还没结束
if (
this.boxArr[i][j] == this.boxArr[i][j + 1] ||
this.boxArr[i][j] == this.boxArr[i + 1][j]
) {
return 1;
}
}
}
// 最后一行和最后一列的判断
for (let i = 0; i < 3; i++) {
if (
this.boxArr[3][i] == this.boxArr[3][i + 1] ||
this.boxArr[i][3] == this.boxArr[i + 1][3]
) {
return 1;
}
}
// 如果以上条件都满足了,游戏就结束了
return 2;
}
return 0;
},
感悟
2048 这一个 canvas 小游戏,我记得当时折磨了我很久,对数字的移动这块我想了很久,后来有个别地方也借鉴了一下网上的方法,由于已经过去一年多了,我也忘了是从哪看的了。最终整个实现下来,对js基本功的提升肯定是很大的,使我对于数组的处理,还有递归的处理也越来越熟悉了。