开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第六天,点击查看活动详情
线上地址:http://codeape.site
源码:https://gitee.com/wooden-joint/my-game/blob/master/src/components/GoBang.vue
小游戏管理平台:https://juejin.cn/post/7149169293714784293
开发技术
vue2 + js + canvas
需求
五子棋大家都懂,五个子连起来就赢,可以是横竖或左斜右斜四个方向;由于技术和精力有限,这里只能左手和右手下棋了,大家见谅。
代码讲解
初始化数据
由于采用 canvas
,老规矩肯定要先获取 canvas
上下文;然后就可以开始画棋盘,然后再初始化棋盘每个点的位置,将其保存到一个二维数组中。
init() {
setTimeout(() => {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext("2d");
// 画棋盘
this.drawCheckerboard();
// 保存棋子的坐标
this.saveLocation();
}, 100);
setTimeout(() => {
this.getCanvasMargin();
}, 1050);
},
- 注意:由于后续需要鼠标点击
canvas
中的对应坐标,所以这里需要获取canvas
距离屏幕左上边界的距离,由于我是每个小游戏单独一个弹出层,所以每次移动弹出层都需要重新获取该位置信息。
// 获取canvas与浏览器 左边 / 顶部 的距离
getCanvasMargin() {
// 获取dom元素对象与浏览器 左边/顶部 的距离 5是border
this.canvasLeft = this.$refs.canvas.getBoundingClientRect().left + 5;
this.canvasTop = this.$refs.canvas.getBoundingClientRect().top + 5;
},
画棋盘
一个标准的围棋棋盘是 14 * 14 个格子的。所以我也按照该比例来画棋盘了。由于有14个格子,所以对应的需要行和列都需要有15根线。
我这里定义每个格子是 30px,所以棋盘的第一根线的起始位置应在 15px 处。
计算好横和竖线的起始位置和每根线的间距,然后就可以进行绘制了。
beginPath
创建一个路径
moveTo
起始位置
lineTo
终点位置
strokeStyle
绘制需要的颜色
stroke
根据已定义好的路径来进行绘制
然后再绘制5个点,找到点对应的行和列信息,然后在该位置上绘画出5个小圆即可。
beginPath
创建一个路径
arc
创建一个圆弧
fillStyle
填充需要的颜色
fill
往路径中填充
这样一个五子棋的棋盘就绘制完成了。
drawCheckerboard() {
let begin = 15; // 开始的位置
let lineLength = this.canvasWidth - 30; // 每条线的总长度
let gridWidth = this.gridWidth + 1; // 每个格子加右边/下边线的宽度
for (let i = 0; i < this.lineNum; i++) {
// 横线
this.ctx.beginPath();
this.ctx.moveTo(begin, begin + gridWidth * i);
this.ctx.lineTo(begin + lineLength, begin + gridWidth * i);
this.ctx.strokeStyle = "#763a0d";
this.ctx.stroke();
// 竖线
this.ctx.beginPath();
this.ctx.moveTo(begin + gridWidth * i, begin);
this.ctx.lineTo(begin + gridWidth * i, begin + lineLength);
this.ctx.strokeStyle = "#763a0d";
this.ctx.stroke();
}
// 画中间的五个点
let arr = [
{ x: 3, y: 3 },
{ x: 3, y: 11 },
{ x: 7, y: 7 },
{ x: 11, y: 3 },
{ x: 11, y: 11 },
];
for (let item of arr) {
this.ctx.beginPath();
this.ctx.arc(
begin + gridWidth * item.x, begin + gridWidth * item.y,
5, 0, 2 * Math.PI
);
this.ctx.fillStyle = "#763a0d";
this.ctx.fill();
}
},
初始化棋盘每个点的坐标
定义两个个二维数组,行和列作为二维数组的索引,并保存下对应的x和y坐标,并把该位置标志为未下棋。另一个数组记录每个格子的棋子信息(0:没棋 1:黑 2:白)。
- 注:一年多前的代码了,当时写得太捞了,分别保存了两个数组,现在回看,将第二个数组取代第一个数组的
isPlay
就好了。
saveLocation() {
let gridWidth = this.gridWidth + 1;
for (let i = 0; i < this.lineNum; i++) {
this.allChessArr.push([]);
this.chessArr.push([]);
for (let j = 0; j < this.lineNum; j++) {
// 保存所有的棋子位置信息
this.allChessArr[i][j] = {
x: 15 + gridWidth * j,
y: 15 + gridWidth * i,
isPlay: false,
};
// 将所有的棋子置为 0
this.chessArr[i][j] = 0;
}
}
},
点击下棋
根据 canvas
离浏览器左和上的距离,和鼠标点击的坐标,计算出在 canvas
中的坐标,然后遍历数组,只要有一个坐标是在点的上下左右15px的范围内,则表示该位置就是下棋位。
- 注:回看代码发现这里太蠢了,这里根本用不着两层for循环,只要用x, y坐标除以线和格子的宽度,再通过30px来取整就可以计算出行和列的索引,然后直接根据这个索引就知道下棋位了。
如果该位置没有棋的话,就可以进行下棋操作,给数组添加1或2(黑或白)。绘画一个棋子,然后通过 checkChess
判断游戏是否已经结束了,没结束就颜色取反。
getMouse() {
if (this.gameOver) return;
// canvas 中棋子的位置
let chessX = window.event.pageX - this.canvasLeft;
let chessY = window.event.pageY - this.canvasTop;
let x, y;
// 获取鼠标点击 下棋的位置
for (let i = 0; i < this.lineNum; i++) {
for (let j = 0; j < this.lineNum; j++) {
if (this.chessArr[i][j] == 0) {
x = this.allChessArr[i][j].x;
y = this.allChessArr[i][j].y;
if (
chessX >= x - 15 &&
chessX <= x + 15 &&
chessY >= y - 15 &&
chessY <= y + 15
) {
let isBlack = this.isBlack;
let chess = isBlack ? 1 : 2;
// 1:黑 2:白
this.chessArr[i][j] = chess;
this.drawChess(x, y, isBlack);
// 判断游戏是否结束
if (this.checkChess(i, j, chess)) {
this.gameOver = true;
return;
}
this.isBlack = !this.isBlack; // 下棋颜色取反
return;
}
}
}
}
},
绘画棋子
创建一个 Image
实例,然后当图片 onload
后,使用 drawImage
绘画棋子。
- 注:这里很捞,当时写的每次都加载一个图片,这里可以初始化的时候先加载好图片,然后保存下来。
drawChess(x, y, isBlack) {
let img = new Image();
isBlack
? (img.src = this.chessImg.black)
: (img.src = this.chessImg.white);
img.onload = () => {
// (image, x, y, 切片width, 切片height, 切片x, y, w, h)
this.ctx.drawImage(img, x - 15, y - 15, 28, 28);
};
},
checkChess 检查是否已经游戏结束
这里一大坨看上去很多,其实都是相似的逻辑的,就是分别对待 x,y和两条斜边的情况。
我这里的思想是先根据 x,y和两条斜边,定义好四个对象,然后根据当前的棋子,判断该对象的左右两边是否还能延伸,如果可以延伸,则该方向的总和加1,当该值大于等于5,则游戏结束。如果遍历完成,四个对象的总和都小于5,则游戏继续。
checkChess(i, j, chess) {
let x = { sum: 1, isL: true, isR: true },
y = { sum: 1, isL: true, isR: true },
z = { sum: 1, isL: true, isR: true },
z2 = { sum: 1, isL: true, isR: true };
for (let a = 1; a < 5; a++) {
// 处理行
x.isL && j + a < 15 && this.chessArr[i][j + a] == chess
? x.sum++
: (x.isL = false);
x.isR && j - a >= 0 && this.chessArr[i][j - a] == chess
? x.sum++
: (x.isR = false);
// 处理列
y.isL && i + a < 15 && this.chessArr[i + a][j] == chess
? y.sum++
: (y.isL = false);
y.isR && i - a >= 0 && this.chessArr[i - a][j] == chess
? y.sum++
: (y.isR = false);
// 处理斜边1 '\'
z.isL && i + a < 15 && j + a < 15 && this.chessArr[i + a][j + a] == chess
? z.sum++
: (z.isL = false);
z.isR && i - a >= 0 && j - a >= 0 && this.chessArr[i - a][j - a] == chess
? z.sum++
: (z.isR = false);
// 处理斜边2 '/'
z2.isL && i + a < 15 && j - a >= 0 && this.chessArr[i + a][j - a] == chess
? z2.sum++
: (z2.isL = false);
z2.isR && i - a >= 0 && j + a < 15 && this.chessArr[i - a][j + a] == chess
? z2.sum++
: (z2.isR = false);
}
// console.log("x: ", x.sum, "y: ", y.sum, "z: ", z.sum, "z2: ", z2.sum);
if (x.sum >= 5 || y.sum >= 5 || z.sum >= 5 || z2.sum >= 5) return true;
return false;
},
感悟
整个五子棋,小游戏下来对思维和js基本功还是有一定的锻炼的,整个里面最复杂的就是判断游戏是否已经结束的处理了。
现在回顾当时的代码,感觉很不堪回首,有很多地方的代码都是写得很差的,存在一定的优化空间,现在细看也能看到了自己的成长。
暂无评论内容