【回归初心】react-chinese-chess 中国象棋(3.13已更新)

3,525 阅读8分钟

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"]
}

修改 startbuild 命令

"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 的方式实现棋盘绘制。

不使用图片的原因有以下两点:

    1. 方便定制化棋盘样式,可以通过修改主题参数轻松变换棋盘风格
    1. 有助于思考网格及定位

3.2. ChessMen 思考

思考一、是否与 ChessBoard 复用同一个 canvas , 还是在 ChessBoardcanvas 上叠加另一个 canvas?

为了避免在闪烁、移动棋子的时候重绘 ChessBoard, 决定采用叠加方案~

思考二、如何对棋子的位置、文字、红黑进行管理

如下图所示:

  • 1.使用二维数组管理棋子位置,有棋子根据 ${colorType + colorName + colorIndex} 拼接,如 24 代表 黑方车一,无棋子使用 0
  • 2.定义 COLOR_TYPE 管理棋子红黑
  • 3.定义 REDBLACK 对象管理红黑方文字

4.绘制

画之前先思考一个问题,因为设备的屏幕是不一样的,棋盘的格子可大可小,如果咱们按 50 的宽度画,并不能满足所有设备。

所以我们需要先计算网格的宽高,因为网格是正方形的,所以代码里直接称为宽度。

1.采用了屏幕的宽高减去了一个固定数值作为最大宽高。有点padding终究是更好看的。 2.比较横向和纵向除以单元格个数后的更小的那个数值,作为单元格宽度。

有了 cellWidth 后,让我们开始真正的 canvas 操作把!

4.1. 网格

大家可以想象初中画格子怎么画的,其实在 canvas 里面是一样的,并不要觉得电脑比较机械,是一个一个方块画的。

横着画10根线,纵着画9根线,你就会得到一个这样的网格结构

这里主要就是用到一些 for 循环的知识,然后不停的在循环里 moveTo, lineTo 就可以了。

细心的小伙伴可能观察到了楚河汉界的地方不应该有竖线的,这里我们根据纵线不是第一条,也不是最后一条的时候,简单处理一下就ok了。

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

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

这里组件代码内对 moveTo, lineTo 方法做了一次封装,最后 return this ,链式调用加上注释会让代码更清晰移动。了解 jquery 的小伙伴看到这里应该会会心一笑,因为实在是太熟悉了。

4.2. 边框

棋盘的网格外面还有一层边框,本来没有什么可说的,就是画四条线就可以,但是考虑到大家也可能有连续画线的需求,所以这里简单的说一下,我们可以将 ctxlineTomoveTo 封装到我们组件内的方法中,这样就可以不用 lineTo 某个点位后,又调一遍 moveTo 了。

4.3. 文字

使用 fillTextapi,增加楚河汉界。为了让字体好看一点,使用了 STKaiti

由于并不是所有设备都有这一字体,所以我们需要先先下载 STKaiti.ttf 源文件,再使用 font-spider 配合一个 html 文件,将 STKaiti.ttf12.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 点

  1. 相同颜色不应该移动,比如将红车移动到红帅上
  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.兼容性

canvasctx 设置 fillStyle 时,如果需要使用透明的背景,应该使用 rgba 模式,如果使用了 #xxxxxxx 8位色码的模式,会导致背景在很多设备上是黑色的。

8.写在最后

原创不易,希望掘友萌可以点点大拇指,鼓励一下!

github地址:中国象棋(React版)的 github

在线预览:中国象棋(React版)

感谢大家的阅读!