背景
象棋是一款益智类游戏,玩家可以通过操控棋子排兵布阵,最终获得游戏的胜利。在作者小时候,该游戏一直是风靡在村里的大街小巷,本着对童年的回味和对象棋的热爱,本主会尽自己的全力完成此款游戏的设计开发,欢迎点赞和收藏!
- 在线地址:https://www.aixt.vip (点我直达)
功能列表
- 游客登录
- 账号密码/绑定邮箱登录
- QQ授权登录
- 账号注册,支持绑定邮箱,绑定时需要发送验证码
- 忘记密码找回
- 版本历史记录展示
- 登录后展示个人信息(头像、昵称、积分、胜负和、排名)
- 加入房间
- 房间换桌
- 房间踢人
- 对局准备
- 棋盘、棋子展示
- 棋子移动动画
- 游戏音效
- 对局聊天、悔棋、认输、求和
- 换肤功能
- 对局观战信息展示
- 观战全员禁言
- 观战针对个人禁言
- 观战针对个人踢出
- 房间邀请功能
- 大厅用户可观战已建立的对局
- 大厅用户观战聊天功能
- 大厅用户观战交换观战视角(红方与黑方互相切换)
- 大厅用户观战可保持屏幕常亮
- 复盘功能
- 复盘分享功能
- 棋谱功能
- 数据断连(游戏断开再登录后,可恢复对局)
技术要点
- React
前端主要语言,配合umijs编写功能
- Ant-Mobile
UI框架,有很多现成的组件供使用
- umijs
脚本架,快速构建项目
- socket.io
消息通信
- node.js
javascript引擎,配合ts编写服务端功能
- ES6
学习ES6的语法,用于编写代码
- Redis
缓存工具,用于缓存对战信息
- MySQL
数据库,用户保存游戏产生的数据
- typescript
项目结构(重点文件/文件夹)
├── src
│ ├── app.js 应用配置(全局)
│ ├── config.js 项目配置(全局)
│ ├── global.less 样式配置(全局)
│ ├── assets 资源文件
│ │ ├── audio 音效
│ │ ├── font 字体
│ │ ├── images 图片
│ │ └── skins 皮肤
│ ├── button 自定义按钮
│ ├── circle 自定义弹窗
│ ├── components 自定义加载动画
│ ├── header 自定义标题
│ ├── layouts 父页面
│ ├── pages 子页面
│ │ ├── document.ejs 启动加载页
│ │ ├── board 棋盘界面
│ │ ├── user 登录/注册/忘记密码
│ │ ├── platform 游戏平台界面
│ │ ├── review 复盘界面
│ │ ├── rooms 房间界面
│ │ ├── sahre 分享页面
│ │ ├── version 版本界面
│ │ └── watch 观战界面
│ ├── service Socket封装
│ │ ├── event.js socket事件管理(API)
│ │ └── socket.js socket连接管理
│ └── utils 工具包
│ ├── board-canvas-utils.js 棋盘绘制工具
│ ├── board-utils.js 棋盘工具
│ ├── cache-key-utils.js 缓存Key管理
│ ├── check-win.js 检测胜利(绝杀/困毙)
│ ├── const-utils.js 常量管理
│ ├── cryptor-utils.js 数据加解密
│ ├── head-canvas-utils.js 头像绘制工具
│ ├── images-res.js 图片资源管理
│ ├── keep-fighting.js 长将检测
│ ├── log-utils.js 日志管理
│ ├── map-res.js 地图静态数据
│ ├── rule-check.js 规则检测
│ ├── rules 具体规则实现
│ │ ├── basic-rule.js 基本规则检测(车、马、象等)
│ │ └── boss-rule.js Boss规则检测(碰面、被将军)
│ ├── skin-utils.js 皮肤管理
│ ├── sounds-res.js 音效管理
│ └── storage-utils.js 缓存管理
├── package.json
功能细节
1. 通信
socket.io
有两个常用方法,发送emit
,监听on
,当服务器启动后,首先会通过on
监听已预设的API
,当socket
与服务端成功连接后,客户端可以通过emit
向相应的API
发送消息,此时服务端就能收到该消息并做一系列操作,拿聊天来说,客户端调用sendChatApi
发送消息到服务端后,服务端将消息进行敏感词处理,处理完成后将消息通过emit
发送到指定的房间,而客户端则会通过on
监听此房间的消息,从而实现消息转发。若有疑问,可查看socket.io
相关文档
2. 棋盘棋子绘制
游戏素材
都来源于网络,棋盘和棋子也就是一张贴图,在一个html中绘制一张贴图,一般用img
标签,如何将一张img
绘制到不同的位置,可以通过对其进行position: absolute
,再根据棋盘的间距和棋子的坐标,设置css的transform: translate(${x}rem,${y}rem)
棋盘绘制示例:
gameMap.map(chess => {
// gridSpan: 网格大小
const x = gridSpan * chess.y - (gridSpan / 2);
const y = gridSpan * chess.x - (gridSpan / 2);
return (
<img
key={chess.id}
alt={chess.id}
// 获取棋盘的图片
src={resMap.get(chess.id)}
style={{
// 浮动,并利用translate对棋子进行偏移(left/top是同样的效果)
position: 'absolute',
width: `${gridSpan}rem`,
height: `${gridSpan}rem`,
// 移动时的动画
transition: 'transform 0.5s, opacity 0.5s',
transform: `translate(${x}rem,${y}rem)`,
zIndex: 3,
}}
/>
);
})
3. 棋子规则
车走直、马走日、象走田,要实现这些棋子的规则,首先要将棋盘抽象理解成一个二维数组
,x, y 相交的点为棋子所在的点,假设(左上角)车
在0,0
的位置,那么当车
要走到马
的位置时,实际上就是坐标0,0 -> 0,1
,明白这个道理后,如果你要对车进行规则判定,就非常的简单,车只能直着走,那么必然x -> x1
,它两的值是相等的,如果x
不相等,则再判断一下y -> y1
是不是相等,如果两者都不相等,那必然不可能是横或竖着走了,另外还要考虑其它的一些情况,比如说推算到0,1
时这个位置有我方的马
,那后面的位置,车也是走不了的,如果遇到的是对方的棋,这个位置就能走(对方的棋子可以吃)
车
的走法判断逻辑(可以考虑一下马、象这种有蹩脚的是如何判定的)
const handleMoveC = (gameMap, from, to) => {
const board = listToArray(gameMap);
// 必须有一个点是在同一条线上
if (from.x === to.x) {
const tempChessList = [];
let start = Math.min(from.y, to.y);
let end = Math.max(from.y, to.y);
// 判断其中间有没有棋子
while (start <= end) {
// 排除掉自身位置且中间不能有其它棋子
if (from.y !== start && board[from.x][start]) {
tempChessList.push({ x: from.x, y: start });
}
++start;
}
// 循环结束后进行判断,空棋盘直接返回(要走的位置中间没有其它棋子)
if (tempChessList.length === 0) {
return true;
} else if (tempChessList.length === 1) {
// 如果有一个棋子,则要处理一下是否是要到达的位置
const chess = tempChessList.pop();
if (chess.x === to.x && chess.y === to.y) {
return true;
}
}
return false;
} else if (from.y === to.y) {
const tempChessList = [];
let start = Math.min(from.x, to.x);
let end = Math.max(from.x, to.x);
while (start <= end) {
if (from.x !== start && board[start][from.y]) {
tempChessList.push({ x: start, y: from.y });
}
++start;
}
// 循环结束后进行判断,空棋盘直接返回(要走的位置中间没有其它棋子)
if (tempChessList.length === 0) {
return true;
} else if (tempChessList.length === 1) {
// 如果有一个棋子,则要处理一下是否是要到达的位置
const chess = tempChessList.pop();
if (chess.x === to.x && chess.y === to.y) {
return true;
}
}
}
return false;
};
4. 如何判断游戏结束
- 当boss没有位置可走时(只有boss自己)
- 当boss没有位置可走时(有其它棋子,但被堵死,如:象被卡住象眼了)
- 被将军无路可走(包括没有其它棋子能抵挡boss被击杀)
- 被将军且boss走棋就会与对方boss碰面
- 双方都没有可进攻棋子判和
- 对局中有一方直接认输
- 对局中有一方发起求和且被求和方也同意和棋
5. 匹配机制
当用户进入房间后,后台有一张用户游离表
会记录该用户在整个游戏中的状态,包括但不限于当前页面
、当前房间号
、房间状态
,当用户首次进入房间,该房间无其他人时,房间状态
为等待中
,这个动作,服务端只需要将该用户游离数据表
的房间号
变更为当前分配的房间号并更改房间状态
为等待中
,当另一位玩家也进入房间时,此时服务器会从游离数据表
中根据房间状态
进行数据筛选,目前匹配规则是房间状态
为等待中
的会优先分配,当房间内人数达到2个时,房间状态会变更为多人等待
,此时该房间不会参与匹配。
- 目前已设计的房间状态
// 空房间
export const ROOM_STATUS_EMPTY = 'EMPTY';
// 有人在房间中等待(仅1个人)
export const ROOM_STATUS_WAIT = 'WAIT';
// 多个人在房间中等待
export const ROOM_STATUS_MULTIPLE_WAIT = 'MULTIPLE_WAIT';
// 匹配成功(双方已准备)
export const ROOM_STATUS_MATCH_SUCCESS = 'MATCH_SUCCESS';
// 对战中
export const ROOM_STATUS_BATTLE = 'BATTLE';
// 对战有一方超时了
export const ROOM_STATUS_TIMEOUT = 'TIMEOUT';
// 对战结束
export const ROOM_STATUS_BATTLE_OVER = 'BATTLE_OVER';
6. 棋子移动
如果你对如何使用emit
或on
已经有足够的了解,那么棋子移动则会变得异常简单,简单来说,棋子移动至少有两个对象的数据传送给服务端,分别是from
和to
,它们都包含x, y
属性,前者为棋子所在位置,后者为要到达的位置,但值得注意的是,对战时,对方看到的是我方的镜像数据,也就是说,棋子通过服务器发送给对方时,对方需要做棋子坐标翻转,下面是一个真实的棋子移动数据包
{
"fromChessBox": {
"color": "boxColorBlack",
"show": true,
"x": 4,
"y": 7
},
"battleId": "73782847853944809178100889398553",
"from": {
"isBlackColor": true,
"isAttach": true,
"prefix": "P",
"isBoss": false,
"x": 4,
"y": 7,
"id": "BRP"
},
"to": {
"x": 4,
"y": 4
},
"toChessBox": {
"color": "boxColorBlack",
"show": true,
"x": 4,
"y": 4
},
"stepExplain": "炮2平5",
"userId": "tour_47347731",
"roomId": 7
}
7.悔棋
如果在对局中想要悔棋,首先,如果是我方落子时,我方是无法悔棋的,其次,当非我方落子时,我方发起悔棋后,对方需要同意,我方才会做悔棋操作,悔棋在技术上的实现方案是,将每一次的快照都保存在本地,当调用悔棋方法时,将倒数第一条数据删除,并还原当前棋盘数据成倒数第二条。值得注意的是,当用户在房间或对局中,用户有可能将页面刷新,所以此处跟数据断连
功能也是紧密配合的,当用户做了上述操作时,数据断连
会将对局的数据从服务器中查询并发送给客户端
8.观战实现
当创建对局后,对局表会产生一条对战记录,该条记录的状态为对战中
,当其它用户进入观战列表时,列表会检索对战表的对战状态
,将对战中
的数据展示在列表中,另外还有一个重要的概念,当用户进入房间后,后台会将用户的 socket
加入到分配的房间中,而房间是可以存在多人的,消息通知是直接往这个房间推送的
,如果用户观战了某一场对局,那么该用户也会加入这个房间中,并且共享这个房间的数据
,此时观战的数据来源有两部分,第一部分则是这场对局已保存在库里的数据,另一部分则是对局玩家操作棋子后,将棋子操作信息发送到房间,观战用户则共享房间的数据,再通过逻辑在棋盘上呈现。
9.复盘和分享
a. 通过前面几点的介绍,您大致能了解到,对战是会对每一步保存快照的,而复盘则是对这些快照的数据进行读取展示。
b.分享使用的数据也是一样的,用户将复盘的数据分享时,后台会为盘对局创建一个唯一的分享码,通过分享链接前缀+分享码组成一个分享链接,用户通过分享的链接进入页面后,使用的也是快照数据。
10.邀请功能
为了提高匹配成功率,用户在房间时可以通过在线列表
,通过对玩家进行邀请,邀请请求发送到服务器后,服务器通过被邀请的用户账号查找其对应的socket
,并通过emit
发送邀请信息,同悔棋一样,受邀也需要对方同意,才能加入该房间,如若房间人数
已满,房间状态
已进行对战,此些情况都会被视为邀请已过期