今天来用WebGL撸一个五子棋,效果图如下:
如果对WebGL基础流程和概念不熟悉的同学可以看我的这篇文章
主要分为以下几个步骤:
- 初始化棋盘样式
- 拾取屏幕坐标并渲染在画布上
- 绘制圆形棋子
- 把画布上的坐标转化为棋盘的格子坐标
- 保存棋盘棋子状态并判断输赢
前置工作
辅助函数
为了计算方便引入MV.js的vec2,vec4和flatten函数,作用分别为存储2个数据,4个数据和转换为32位浮点值的连续数组。
着色器
顶点着色器颜色使用Uniforms全局变量获取,由Varyings可变量传入片元着色器
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec4 vPosition;
uniform vec4 vColor;
varying vec4 fColor;
void
main() {
gl_Position = vPosition;
fColor = vColor;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 fColor;
void main()
{
gl_FragColor = fColor;
}
</script>
初始化棋盘样式
首先声明一些变量,把棋盘设置成20x20
let gl;
const canvasSize = Math.min(window.innerHeight, window.innerWidth);
// 棋盘分段数 20*20
const boardSegment = 20;
// 棋盘顶点数量,使用`gl.LINES`绘制棋盘格子的话。横纵各需21条线,每条线2个顶点共需21x4个顶点。
const boardVertexNumber = (boardSegment + 1) * 4;
// 棋盘顶点坐标
const boardVertex = [];
// 每个棋子顶点数量(分段数量),先设为1用一个点表示,之后渲染成圆形棋子的时候会分段成多个顶点
const chessSegment = 1;
// 最大棋子数
const maxChess = (boardSegment + 1)** 2;
// 最大顶点数,棋盘顶点+棋子顶点
const maxPoints = boardVertexNumber + maxChess * chessSegment;
// 顶点着色器Uniforms全局变量vColor的位置
let colorLoc;
获取棋盘顶点
裁剪坐标x和y轴取值范围都是[-1, 1],长度为2。分为20份,,共需21条线,每条线间隔2 / boardSegment = 0.1。画水平于x轴线时,x值固定,y值-1 + i * 0.2。画垂直线于x轴线时,y值固定,x值-1 + i * 0.2。
function initBoard() {
const rowLineNumber = boardSegment + 1;
const columnLineNumber = boardSegment + 1;
const rowHeight = 2 / boardSegment;
const columnWidth = 2 / boardSegment;
for (let i = 0; i < rowLineNumber + columnLineNumber; i++) {
if (i < rowLineNumber) {
boardVertex.push(
vec2(-1.0, -1 + i * rowHeight),
vec2(1.0, -1 + i * rowHeight),
);
} else {
boardVertex.push(
vec2(-1 + (i % rowLineNumber) * columnWidth, -1),
vec2(-1 + (i % rowLineNumber) * columnWidth, 1),
);
}
}
}
WebGL程序
下面开始WebGL部分
通常情况一次性绘制所有顶点的用法是bufferData()把数据直接传给buffer:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
但是五子棋需要每次点击棋盘动态的传入数据,这里要说一下bufferData的另一种用法,需要先告诉缓冲区需要多大的空间,然后使用bufferSubData每次向里面传值:
gl.bufferData(gl.ARRAY_BUFFER, verticesSize, gl.STATIC_DRAW)
// 向缓冲区传值
gl.bufferSubData(
gl.ARRAY_BUFFER,
offset,
new Float32Array(vertices),
);
bufferSubData第二个参数表示以字节为单位指定开始数据替换的偏移量,类似splice(offset, 0, new Float32Array(vertices))。
window.onload = function() {
const canvas = document.getElementById("gl-canvas");
canvas.width = canvasSize;
canvas.height = canvasSize;
gl = canvas.getContext("webgl");
if (!gl) return false;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(204 / 255, 161 / 255, 129 / 255, 1.0); //这里把画布颜色设置为类似木头的颜色
// 着色程序
const vertexShaderSource = document.getElementById("vertex-shader").text;
const fragmentShaderSource = document.getElementById("fragment-shader").text;
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
gl.useProgram(program);
// 顶点
const vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
8 * (maxPoints), // 缓冲大小,每个顶点都是2个32位浮点数,共8个字节
gl.STATIC_DRAW,
);
const vPosition = gl.getAttribLocation(program, "vPosition");
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vPosition);
// 获取顶点着色器Uniforms全局变量vColor的位置
colorLoc = gl.getUniformLocation(program, 'vColor')
// 初始化棋盘
initBoard(); // 获取棋盘顶点
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer); // 绑定当前缓冲区
gl.bufferSubData(gl.ARRAY_BUFFER, 0, flatten(boardVertex)); // 向缓冲区传值
render()
}
把清除画布,设置顶点颜色和绘制函数单独封装成render函数方便接下来复用。
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform4fv(colorLoc, vec4(0.0, 0.0, 0.0, 1.0)); // 棋盘网格为黑色
gl.drawArrays(gl.LINES, 0, boardVertexNumber); // 使用gl.LINES,每两个点绘制成一条不相连的线段
}
这样棋盘的样式就绘制好了
拾取屏幕坐标并渲染在WebGL上
屏幕坐标转换为裁剪坐标
首先我们需要一个函数把鼠标拾取的屏幕坐标转换为WebGL需要的裁剪坐标,也就是
x轴取值范围[0, canvas.width]=>[-1, 1]
y轴取值范围[0, canvas.height]=>[1, -1]
x / width 取值范围是[0, 1],(x / width) * 2 - 1范围则是[-1, 1],
y轴和x类似,只不过屏幕坐标y轴正方和裁剪坐标y轴正方向相反,要取负数 1 - (y / height) * 2。下面是转换函数:
/**
* 屏幕坐标转化为WebGL裁剪坐标
* @param {Number} x 屏幕坐标x坐标
* @param {Number} y 屏幕坐标y坐标
* @param {Number} width 画布的宽
* @param {Number} height 画布的高
*/
function vertex2Gl(x, y, width, height) {
return {
x: (x / width) * 2 - 1,
y: 1 - (y / height) * 2,
};
}
点击画布添加棋子
其次需要为canvas添加click事件
window.onload = function() {
//...
render()
+ canvas.addEventListener("click", evt => {
+ const { clientX: x, clientY: y } = evt;
+ const { width, height } = canvas;
+ addChess(vBuffer, x, y, width, height);
+ });
}
每次点击画布执行添加棋子addChess()函数,需要执行bufferSubData()函数向缓冲区传递数据,因为要设置offset偏移量。所以我们需要一个变量记录当前渲染过棋子的顶点总数
// ...
let colorLoc;
+ //棋子顶点数
+ let chessVertexCount = 0;
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// 获取裁裁剪坐标
const { x: x_gl, y: y_gl } = vertex2Gl(x, y, width, height);
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferSubData(
gl.ARRAY_BUFFER,
(boardVertexNumber + chessVertexCount) * 8, // offeset偏移量为 棋盘顶点加上已绘制棋子顶点的字节大小
flatten(vec2(x_gl, y_gl)),
);
// 每次加上一个棋子顶点数量
chessVertexCount += chessSegment;
render();
}
render函数也需要添加绘制棋子的逻辑,循环执行drawArrays()函数,绘制的开始位置为棋盘顶点数量➕已绘制棋子顶点数量,每次绘制棋子顶点个数的数量
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform4fv(colorLoc, vec4(0.0, 0.0, 0.0, 1.0)); // 棋盘网格为黑色
gl.drawArrays(gl.LINES, 0, boardVertexNumber); // 使用gl.LINES,每两个点绘制成一条不相连的线段
+ for (let i = 0; i < chessVertexCount; i += chessSegment) {
+ gl.drawArrays(gl.POINTS, boardVertexNumber + i, chessSegment);
+ }
}
这样点击画布就有棋子:
设置棋子样式
可以看到每个点都很小不容易看清:
我们可以使用顶点着色器gl_PointSize使点的尺寸变大些
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec4 vPosition;
uniform vec4 vColor;
varying vec4 fColor;
void
main() {
+ gl_PointSize = 20.0;
gl_Position = vPosition;
fColor = vColor;
}
</script>
这样就好多了
全是黑色也不行,需要一次黑一次白
这里用
i / chessSegment表示当前绘制的第几个节点,可能有疑问chessSegment为1为什么要除一下,因为现在棋子用点表示只用一个顶点,下文换成圆渲染一个棋子会有好多顶点
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform4fv(colorLoc, vec4(0.0, 0.0, 0.0, 1.0)); // 棋盘网格为黑色
gl.drawArrays(gl.LINES, 0, boardVertexNumber); // 使用gl.LINES,每两个点绘制成一条不相连的线段
for (let i = 0; i < chessVertexCount; i += chessSegment) {
+ const color = i / chessSegment % 2 === 0 ? vec4(0.0, 0.0, 0.0, 1.0) : vec4(1.0, 1.0, 1.0, 1.0)
+ gl.uniform4fv(colorLoc, color);
gl.drawArrays(gl.POINTS, boardVertexNumber + i, chessSegment);
}
}
这样就就可以了
利用坐标绘制圆形棋子
那么怎么绘制一个圆呢?筛选了一边绘制类型,发现gl.TRIANGLE_FAN比较容易实现,我们只需要求出构成圆若干等距的顶点就可以了
怎么求构成圆的顶点坐标呢?在一个圆心在原点的圆中,圆上任意一坐标都可以利用半径和三角函数来求出,如下图:
点P的坐标为
P(r * cos(θ), r * sin(θ))。因为棋子的坐标不是在原点,所以我们需要把顶点坐标加上圆心的坐标P(Ox + r * cos(θ), Oy + r * sin(θ))。
我们暂时用20点构成一个圆。当然,点越多,渲染出来越接近圆。
- const chessSegment = 1;
+ const chessSegment = 20;
下面可以可以用循环得出每个点的角度,利用三角函数求出棋子的坐标了:
/**
* 由棋子坐标得到棋子顶点坐标
* @param {Number} x 棋子x坐标
* @param {Number} y 棋子y坐标
*/
function vertexChess(x, y) {
// 棋子半径是半个网格的长度
const chessRadius = 2 / boardSegment / 2;
const chessVertex = [];
for (let i = 0; i < chessSegment; i++) {
const x_chess =
x + chessRadius * Math.cos(((Math.PI * 2) / chessSegment) * i);
const y_chess =
y + chessRadius * Math.sin(((Math.PI * 2) / chessSegment) * i);
chessVertex.push(vec2(x_chess, y_chess));
}
return chessVertex;
}
接下来需要修改一些代码:
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// 获取裁裁剪坐标
const { x: x_gl, y: y_gl } = vertex2Gl(x, y, width, height);
+ const chessVertex = vertexChess(x_gl, y_gl);
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferSubData(
gl.ARRAY_BUFFER,
(boardVertexNumber + chessVertexCount) * 8,
- flatten(vec2(x_gl, y_gl)),
+ flatten(chessVertex),
);
// 每次加上棋子顶点数量
chessVertexCount += chessSegment;
render();
}
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform4fv(colorLoc, vec4(0.0, 0.0, 0.0, 1.0)); // 棋盘网格为黑色
gl.drawArrays(gl.LINES, 0, boardVertexNumber); // 使用gl.LINES,每两个点绘制成一条不相连的线段
for (let i = 0; i < chessVertexCount; i += chessSegment) {
- gl.drawArrays(gl.POINTS, boardVertexNumber + i, chessSegment);
+ gl.drawArrays(gl.TRIANGLE_FAN, boardVertexNumber + i, chessSegment); // 改变绘制类型
}
}
//着色器
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec4 vPosition;
uniform vec4 vColor;
varying vec4 fColor;
void
main() {
- gl_PointSize = 20.0;
gl_Position = vPosition;
fColor = vColor;
}
</script>
这样每次点击屏幕,之前的点就变成圆点了
把WebGL的坐标转化为棋盘的格子坐标
目前我们棋子的坐标是鼠标的点击位置,这显然是不对的,棋子应该在网格两条线的交点。每个交点的坐标都是2/boardSegment(棋盘分段数)也就是0.1(2/20)的整数倍,可以把点击交点附近[-0.5, +0.5]的范围都转换为中间的交点。
由裁剪坐标得出棋子坐标的函数可以表示为:
/**
* webGL裁剪坐标转化为棋子坐标
* @param {Number} x webGL x坐标
* @param {Number} y webGL y坐标
*/
function vertex2Board(x, y) {
// 每个格子大小
const diameter = 2 / boardSegment;
return {
x: Math.round(x / diameter) * diameter,
y: Math.round(y / diameter) * diameter,
};
}
然后修改一些代码:
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// 获取裁裁剪坐标
const { x: x_gl, y: y_gl } = vertex2Gl(x, y, width, height);
+ const { x: x_board, y: y_board } = vertex2Board(x_gl, y_gl);
- const chessVertex = vertexChess(x_gl, y_gl);
+ const chessVertex = vertexChess(x_board, y_board); // 由棋子中心坐标求出棋子所有顶点坐标
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferSubData(
gl.ARRAY_BUFFER,
(boardVertexNumber + chessVertexCount) * 8,
flatten(vec2(x_gl, y_gl)),
flatten(chessVertex),
);
// 每次加上棋子顶点数量
chessVertexCount += chessSegment;
render();
}
这样每次点击棋盘,棋子的位置就始终位于交点了
保存棋盘棋子状态并判断输赢
初始化棋盘状态
想要知道输赢,需要把棋盘落子的状态用数据表示出来,我们可以用下面的数组表示下图中棋盘的状态:
[
[0, 0, 1, 2, 0, 0],
[0, 0, 1, 2, 0, 0],
[0, 0, 1, 2, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
]
图中是一个6x6的棋盘,
0表示无子,1表示黑子,2表示白子。我们只需每次增加棋子时把相应位置的数据加进去,然后校验一下,新增的棋子附近有没有5个相同颜色的棋子即可。
首先增加一个棋盘状态变量,并封装一个初始化棋盘状态数据的函数
//...
let chessVertexCount = 0;
// 棋盘落子状态
+ const boardState = [];
/**
* 初始化棋盘落子状态 0无棋子,1黑子,2白子
* [
* [0,0,0...],
* [0,0,0...],
* ...
* ]
*/
function initBoardState() {
for (let i = 0; i < boardSegment + 1; i++) {
boardState[i] = [];
for (let j = 0; j < boardSegment + 1; j++) {
boardState[i][j] = 0;
}
}
}
棋盘状态数据坐标函数
其次,还需要有一个棋盘坐标转化为棋盘状态数据中的坐标的函数。
举个🌰,在一个5x5棋盘中间棋子裁剪坐标是(0, 0),需要得出(2,2)表示第三行中的第三列
这和之前屏幕坐标转化为裁剪坐标很相似,只不过反过来了,现在是要把裁剪坐标转化为数组的行和列(和屏幕坐标一样最小值都在左上方)。看下图对比一下:
屏幕坐标转裁剪坐标是这样
function vertex2Gl(x, y, width, height) {
return {
x: (x / width) * 2 - 1,
y: 1 - (y / height) * 2,
};
}
只要反推棋盘状态坐标就出来了,但是这里width和height就变成boardSegment也就是棋盘的行数和列数。
{
x: ((x + 1) * width) / 2,
y: ((1 - y) * height) / 2,
}
因为浮点数计算精度的问题,结果会有偏差,数组的index肯定是整数,所以需要四舍五入
/**
* 由棋子坐标得到棋盘状态中的坐标
* @param {Number} x 棋子x坐标
* @param {Number} y 棋子y坐标
*/
function vertex2State(x, y) {
const width = boardSegment;
const height = boardSegment;
return {
x: Math.round(((x + 1) * width) / 2),
y: Math.round(((1 - y) * height) / 2),
};
}
添加棋子修改棋盘状态
最后我们来修改一些代码。
添加一个棋子颜色的标识符,用来判断当前棋子颜色。
let const boardState = [];
+ // 棋子颜色标识
let colorFlag = true;
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// 获取裁裁剪坐标
const { x: x_gl, y: y_gl } = vertex2Gl(x, y, width, height);
const { x: x_board, y: y_board } = vertex2Board(x_gl, y_gl);
+ const { x: x_state, y: y_state } = vertex2State(x_board, y_board);
// 该位置有棋子return
+ if (boardState[y_state][x_state]) return;
// 1为黑子,2为白子
+ boardState[y_state][x_state] = colorFlag ? 1 : 2;
const chessVertex = vertexChess(x_board, y_board); // 由棋子坐标求出棋子顶点坐标
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferSubData(
gl.ARRAY_BUFFER,
(boardVertexNumber + chessVertexCount) * 8,
flatten(vec2(x_gl, y_gl)),
flatten(chessVertex),
);
// 每次加上棋子顶点数量
chessVertexCount += chessSegment;
render();
+ colorFlag = !colorFlag; // 渲染完成之后修改棋子颜色
}
可以看到每次增加棋子,棋盘状态会相应发生变化:
棋盘状态变化后,判断输赢
终于到了最后一个步骤了,怎样才算赢呢?横向,纵向,两个斜角方向。四个方向只要有一个方向有五个同样的棋子就算赢了。
拿横向判断举个🌰:棋子A落下后,先向左方查找,指到遇到和A颜色不一样的棋子或者到达左边边界停止。然后向A右方查找同样是直到遇到和A颜色不一样的棋子或者到达右边边界停止,记录期间一共有几个相同的。其他方向同理,都是先从一个方向找,再向反方向找。下面的例子是一共有4个。
下面试着写下判断横向的函数
/**
*
* @param {Number} x 落下棋子在棋盘状态中的x坐标
* @param {Number} y 落下棋子在棋盘状态中的x坐标
* @param {Number} chessType 棋子类别 1或2
*/
function check(x, y, chessType){
// 相同个数的棋子
let connected = 1;
// 左方是index减少方向
let decreaseDirection = 1;
// 右侧是index增加方向
let increaseDirection = 1;
// 除去落下的棋子,只需要寻找四次就够了
for (let i = 0; i < 4; i++) {
// 首先向左侧寻找
// 没有达到左边边界 && 左侧棋子状态等于落下棋子的颜色
if (x - decreaseDirection >= 0 && boardState[y][x - decreaseDirection] === chessType) {
// 相同个数的棋子增加
connected ++ ;
// 继续向左侧的下一个位置查找
decreaseDirection ++ ;
// 没有达到右边边界 && 右侧棋子状态等于落下棋子的颜色
} else if (x + increaseDirection <= boardSegment && boardState[y][x + increaseDirection] === chessType) {
// 相同个数的棋子增加
connected ++ ;
// 继续向右侧侧的下一个位置查找
increaseDirection ++ ;
}
}
return connected >= 5;
}
下面试着用下,我们修改一些代码:
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// 获取裁裁剪坐标
const { x: x_gl, y: y_gl } = vertex2Gl(x, y, width, height);
const { x: x_board, y: y_board } = vertex2Board(x_gl, y_gl);
const { x: x_state, y: y_state } = vertex2State(x_board, y_board);
// 该位置有棋子return
if (boardState[y_state][x_state]) return;
// 1为黑子,2为白子
boardState[y_state][x_state] = colorFlag ? 1 : 2;
const chessVertex = vertexChess(x_board, y_board); // 由棋子坐标求出棋子顶点坐标
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferSubData(
gl.ARRAY_BUFFER,
(boardVertexNumber + chessVertexCount) * 8,
flatten(vec2(x_gl, y_gl)),
flatten(chessVertex),
);
// 每次加上棋子顶点数量
chessVertexCount += chessSegment;
render();
// 检查是否获胜
+ checkFinished(x_state, y_state, colorFlag);
colorFlag = !colorFlag; // 渲染完成之后修改棋子颜色
}
/**
*
* @param {Number} x 落下棋子在棋盘状态中的x坐标
* @param {Number} y 落下棋子在棋盘状态中的x坐标
* @param {Boolean} colorFlag 目前棋子颜色标识 true黑色 false白色
*/
function checkFinished(x, y, colorFlag) {
const chessType = colorFlag ? 1 : 2;
if(check(x, y, chessType)){
console.log('win')
}
}
看起来不错
接下来还有纵向和两个斜角方向,纵向和横向基本一致就不讲了。这里讲下
右上-左下的情况。两个方向x,y都有增加,减少各有一个。判断条件就稍微复杂些
// 右上-左下
// (x未到右边界 && y未到上边界) && 右上方向棋子颜色是落下棋子的颜色
if ((x + decreaseDirection <= boardSegment && y - decreaseDirection >= 0) &&
boardState[y - decreaseDirection][x + decreaseDirection] === chessType) {
connected += 1;
decreaseDirection++;
// (x未到左边界 && y未到下边界) && 左下方向棋子颜色是落下棋子的颜色
} else if ((x - increaseDirection >= 0 && y + increaseDirection <= boardSegment) &&
boardState[y + increaseDirection][x - increaseDirection] === chessType) {
connected += 1;
increaseDirection++;
}
下面列出所有类型的检查条件函数,四个方向的判断条件基本一致都是下一坐标在棋盘范围内&&棋子颜色相同:
/**
*
* @param {Number} x 落下棋子在棋盘状态中的x坐标
* @param {Number} y 落下棋子在棋盘状态中的x坐标
* @param {Number} chessType 棋子类别 1或2
* @param {String} checkType 检查类型 row,col,diagonal_left(左上-右下),diagonal_right(右上-左下)
*/
function check(x, y, chessType, checkType) {
let connected = 1;
// index减少方向的个数 分别是 左,上,左上,右上
let decreaseDirection = 1;
// index增加方向的个数 分别是 右,下,右下,左下
let increaseDirection = 1;
for (let i = 0; i < 4; i++) {
// 每个检查类型增加方向和减少方向的 坐标和落子状态检验
switch (checkType) {
case 'row':
if (x - decreaseDirection >= 0 && boardState[y][x - decreaseDirection] === chessType) {
connected += 1;
decreaseDirection++;
} else if (x + increaseDirection <= boardSegment && boardState[y][x + increaseDirection] === chessType) {
connected += 1;
increaseDirection++;
}
break;
case 'col':
if (y - decreaseDirection >= 0 && boardState[y - decreaseDirection][x] === chessType) {
connected += 1;
decreaseDirection++;
} else if (y + increaseDirection <= boardSegment && boardState[y + increaseDirection][x] === chessType) {
connected += 1;
increaseDirection++;
}
break;
case 'diagonal_left':
if ((x - decreaseDirection >= 0 && y - decreaseDirection >= 0)
&& boardState[y - decreaseDirection][x - decreaseDirection] === chessType) {
connected += 1;
decreaseDirection++;
} else if ((x + increaseDirection <= boardSegment && y + increaseDirection <= boardSegment) &&
boardState[y + increaseDirection][x + increaseDirection] === chessType) {
connected += 1;
increaseDirection++;
}
break;
case 'diagonal_right':
if ((x + decreaseDirection <= boardSegment && y - decreaseDirection >= 0) &&
boardState[y - decreaseDirection][x + decreaseDirection] === chessType) {
connected += 1;
decreaseDirection++;
} else if ((x - increaseDirection >= 0 && y + increaseDirection <= boardSegment) &&
boardState[y + increaseDirection][x - increaseDirection] === chessType) {
connected += 1;
increaseDirection++;
}
break;
default:
break;
}
}
return connected >= 5;
}
再来修改下checkFinished()函数,使用一个标签显示当前获胜方:
/**
*
* @param {Number} x 落下棋子在棋盘状态中的x坐标
* @param {Number} y 落下棋子在棋盘状态中的x坐标
* @param {Boolean} colorFlag 目前棋子颜色标识 true黑色 false白色
*/
function checkFinished(x, y, colorFlag) {
const chessType = colorFlag ? 1 : 2;
if (check(x, y, chessType, "row")) {
winHandle(colorFlag);
return;
} else if (check(x, y, chessType, "col")) {
winHandle(colorFlag);
return;
} else if (check(x, y, chessType, "diagonal_left")) {
winHandle(colorFlag);
return;
} else if (check(x, y, chessType, "diagonal_right")) {
winHandle(colorFlag);
return;
}
}
function winHandle(colorFlag) {
winner = colorFlag ? "黑子" : "白子";
winnerEl.innerText = winner + "胜"
}
// 获胜方
+ let winner;
+ const winnerEl = document.getElementById('winner')
//...
// 有获胜方则不允许落子
canvas.addEventListener("click", evt => {
+ if (winner) return;
const { clientX: x, clientY: y } = evt;
const { width, height } = canvas;
addChess(vBuffer, x, y, width, height);
});
来看下效果:
体验优化
至此功能基本做完,但是还是有个体验需要优化,就是每次落子没有标记,容易忘记上次在哪落的子。所以我们需要在每次落子加一个标记。这就用到了我们之前vertex2State()由棋子坐标得到棋盘状态中的坐标函数,这个函数也是棋子坐标转换成屏幕坐标函数,他们的原理都一样。为了避免混淆,再声明一个新的函数:
/**
* 由棋子坐标得到屏幕坐标
* @param {Number} x 棋子x坐标
* @param {Number} y 棋子y坐标
*/
function vertex2Screen(x, y, width, height) {
return {
x: (x + 1) / 2 * width,
y: (1 - y) / 2 * height
};
}
最后在每次render之后重置标记位置
// 最后棋子标记
+ const lastEl = document.getElementById('last');
// 添加棋子
function addChess(vBuffer, x, y, width, height) {
// ...
render();
// 获取棋子屏幕坐标,标记最后一个棋子的位置
+ const { x: x_screen, y: y_screen } = vertex2Screen(x_board, y_board,width, height);
+ lastEl.style.display = `block`; //默认隐藏
+ lastEl.style.left = `${x_screen}px`;
+ lastEl.style.top = `${y_screen}px`;
// 检查是否获胜
checkFinished(x_state, y_state, colorFlag);
colorFlag = !colorFlag;
}
大功告成👏👏👏👏👏