
2020.03.13更新
1. 增加 ChessMen 棋子行走逻辑
2. 调整 ChessMen 棋子闪烁、停止闪烁逻辑
2. 增加自定义字体展示说明(font-spider及技巧)
3. 增加兼容性章节:canvas背景色兼容性
2020.03.10更新:
1. 更换头图为:爱心象棋摆位
2. 调整 webpack.config.js 文件为外链
3. 增加 parcel 构建方式说明
4. 调整组件化行文
5. 增加 ChessMen 棋子绘制、擦除方法介绍
6. 增加 ChessMen 棋子闪烁、停止闪烁、移动逻辑介绍
1.起因
接触编程已经快 10 年了,从最初的喜欢编码的感觉,到现在在业务中沉沦,未免有点可惜,这篇文章将从兴趣的角度出发,和大家一块聊聊怎么去实现一个中国象棋。
为什么选择中国象棋这个主题呢,首先这是博主第一个学会的棋类项目,小时候爸爸教的双炮将军至今也不会忘却。其次相对纯业务来讲,这个主题也比较有趣,不仅可以锻炼编程技巧,也可以娱乐自己,一举两得。话不多说,让我们开始吧!
2.初始化
因为博主是 react
技术栈的,所以下面用到的技术和 react
有点关联,但是主要还是 canvas
的使用。
项目初始化没有选择成熟的 create-react-app
。
第一版构建工具:webpack
增加 webpack.config.js
,点击查看配置
不熟悉 webpack
的小伙伴别害怕,这些配置博主也不是一次写好的,根据需求去找 webpack
文档,并手敲配置,千万不要复制粘贴,写完后会有一种新的体会。
第二版构建工具:parcel
感谢@圈圈的圈的建议。
尝试后发现parcel
在零配置上做的确实不错。
增加 .babelrc
文件
{
"plugins": ["@babel/plugin-proposal-class-properties"],
"presets": ["@babel/preset-react"]
}
修改 start
、build
命令
"scripts": {
"start": "parcel ./index.html -p 6888",
"build": "rm -rf dist && parcel build --public-url . ./index.html"
},
3.组件化
基于 react
的组件化的思路。
在 component
目录下建立组件
ChessBoard
棋盘ChessMen
棋子
3.1. ChessBoard
思考
对棋盘图片进行观察。横向有9个格子,纵向有10个格子。

分析一个棋盘必备的主题参数后(格子线宽,格子线色,背景色,边框线宽,边框颜色,边框间距,文字颜色),决定使用 canvas
的方式实现棋盘绘制。
不使用图片的原因有以下两点:
-
- 方便定制化棋盘样式,可以通过修改主题参数轻松变换棋盘风格
-
- 有助于思考网格及定位
3.2. ChessMen
思考
思考一、是否与 ChessBoard
复用同一个 canvas
, 还是在 ChessBoard
的 canvas
上叠加另一个 canvas
?
为了避免在闪烁、移动棋子的时候重绘 ChessBoard
, 决定采用叠加方案~
思考二、如何对棋子的位置、文字、红黑进行管理
如下图所示:
- 1.使用二维数组管理棋子位置,有棋子根据
${colorType + colorName + colorIndex}
拼接,如24
代表黑方车一
,无棋子使用0
- 2.定义
COLOR_TYPE
管理棋子红黑 - 3.定义
RED
、BLACK
对象管理红黑方文字

4.绘制
画之前先思考一个问题,因为设备的屏幕是不一样的,棋盘的格子可大可小,如果咱们按 50 的宽度画,并不能满足所有设备。
所以我们需要先计算网格的宽高,因为网格是正方形的,所以代码里直接称为宽度。

1.采用了屏幕的宽高减去了一个固定数值作为最大宽高。有点padding终究是更好看的。 2.比较横向和纵向除以单元格个数后的更小的那个数值,作为单元格宽度。
有了 cellWidth
后,让我们开始真正的 canvas
操作把!
4.1. 网格
大家可以想象初中画格子怎么画的,其实在 canvas
里面是一样的,并不要觉得电脑比较机械,是一个一个方块画的。
横着画10根线,纵着画9根线,你就会得到一个这样的网格结构

这里主要就是用到一些 for
循环的知识,然后不停的在循环里 moveTo
, lineTo
就可以了。
细心的小伙伴可能观察到了楚河汉界的地方不应该有竖线的,这里我们根据纵线不是第一条,也不是最后一条的时候,简单处理一下就ok了。

处理好后,我们会得到一个这样的棋盘

只需要我们画一下帅和将在的王座的xx就可以了。

这里组件代码内对 moveTo
, lineTo
方法做了一次封装,最后 return this
,链式调用加上注释会让代码更清晰移动。了解 jquery
的小伙伴看到这里应该会会心一笑,因为实在是太熟悉了。
4.2. 边框
棋盘的网格外面还有一层边框,本来没有什么可说的,就是画四条线就可以,但是考虑到大家也可能有连续画线的需求,所以这里简单的说一下,我们可以将 ctx
的 lineTo
和 moveTo
封装到我们组件内的方法中,这样就可以不用 lineTo
某个点位后,又调一遍 moveTo
了。

4.3. 文字
使用 fillText
的 api
,增加楚河汉界。为了让字体好看一点,使用了 STKaiti
。

由于并不是所有设备都有这一字体,所以我们需要先先下载 STKaiti.ttf
源文件,再使用 font-spider
配合一个 html
文件,将 STKaiti.ttf
从 12.1MB
减少为 20kb
。
此处要注意一个问题,要想在 canvas
中使用自定义字体,必须先用html标签加载这段文字,否则首次绘制该字体会无效~。
博主还在这踩了一个坑,就是把 html
中加载的文字放在了 div #app
中,这样有可能还没解析完 dom
的时候,就执行了渲染,导致字体未先触发解析。

4.4. 棋子
棋子的绘制主要需要通过以下几个方法:
ctx.arc
: 绘制圆形
ctx.lineWidth
: 设置线宽,让棋子看起来有内心圆,外心圆
ctx.fillText
: 填充文字
ctx.shadowOffsetX&Y shadowBlur shadowColor
设置阴影,让棋子更立体

4.5. 擦除棋子
网上搜索了现成的方案,还未分析代码原理。

5.逻辑
5.1. 点位判断
canvas
中获取鼠标的点击位置,我们可以使用 event.layerX
, event.layerY
。
获取到位置后,需要和坐标进行关联,采用如下逻辑:
用 canvas
点击位置 layerX|Y
除以格子宽度 cellWidth
,再减去棋盘边距。

5.2. 闪烁、停止闪烁
在点击的位置坐标基础上,做如下判断:
闪烁:
- 定时1秒,擦出棋子,延时500ms绘制棋子
- 并记录最后一次点击的坐标位置以及颜色
- 必须是轮次方才可以触发闪烁
停止闪烁:
- 如果本次点击棋子与上次一致,清除1s的擦除、绘制定时器。
- 如果本次点击棋子与上次不一致且为同方棋子,先清除1s的擦除、绘制定时器,再执行闪烁逻辑。

5.3 行棋规则-isShouldMove
首先基于以下 2 点
- 相同颜色不应该移动,比如将红车移动到红帅上
- 位置不变不应该移动,因为移动会触发轮次变换
下面来看不同类型棋子的移动规则
i
表示目标纵坐标,j
表示目标横坐标,this.lastI
表示移动前纵坐标,this.lastJ
表示移动前横坐标
5.3.0. 兵|卒

不论是否过河:只要是纵向向前走一格都可以
已过河:只要是横向不管方向,只要走一格均可
5.3.1. 帅|将

固定 9
个点位,且每次仅在纵向一格或仅在横向上移动一格。
5.3.2. 仕|士

固定 5
个点位,且同时在纵向和横向上均移动一格。
5.3.3. 相|象

同时在纵向和横向上均移动二格,且移动方向上的中间点无棋子。与此同时(相|象)不可过河。
5.3.4. 車

仅拿纵向举例,判断行进路线上仅有一个棋子(車本身),或者有2个棋子(車本身和另一个棋子,且该棋子在本次位移的终点)
5.3.5. 馬

( 同时在纵向移动二格和横向上均移动一格。
|| 同时在横向移动二格和纵向上均移动一格 )
且移动二格的方向上的中间点无棋子
5.3.6. 炮

仅拿纵向举例,判断行进路线上仅有一个棋子(炮本身),或者有3个棋子(炮本身、中间棋子、目标棋子,且目标棋子在本次位移的终点)
6.自适应
基于以下两点,对自适应进行补充。
1.网格宽度首先是通过计算的,这个可以很自然的自适应。
2.字体的位置和大小是根据 `cellWidth` 乘出来的,所以应该问题不大。
使用 window
监听 resize
事件 + 防抖 + 重新绘制。

2010.03.08
因为现在棋盘上的元素还比较简单,如果以后把棋子加上后,可能会存在性能问题,而且 resize
后要把棋子都绘制一遍,就需要对棋子进行状态管理。
2010.03.10
初步测试棋子重绘不会有性能问题,但不知道耗电量相关问题,暂停中~。
7.兼容性
canvas
的 ctx
设置 fillStyle
时,如果需要使用透明的背景,应该使用 rgba
模式,如果使用了 #xxxxxxx
8位色码的模式,会导致背景在很多设备上是黑色的。
8.写在最后
原创不易,希望掘友萌可以点点大拇指,鼓励一下!
github地址:中国象棋(React版)的 github
在线预览:中国象棋(React版)
感谢大家的阅读!