用户表设计

image-20230405193030457

注册流程

Jwt八股

前端向后端发送请求

Controller层接收到请求并调用具体业务逻辑

image-20230405183555856

Service层处理完请求返回给前端

image-20230405184011568

前端接收到后端发来的数据后进行相关操作

image-20230405184115184

登录流程

前端有两个函数,login函数用来根据用户名和密码生成token,getinfo函数用来根据token得到用户的id、用户名和头像。

image-20230405185031461

user.js

login函数

image-20230405185355911

getinfo函数

image-20230405185430914

获取token

image-20230405185821770

根据token获得相关信息

image-20230405185936194

登出函数

直接纯前端实现。删除local storage里的jwt_token,然后把用户相关的变量清空即可。

bot相关API

一共有三个功能,创建、修改和删除。具体流程和登录注册差不多不再赘述。

需要注意的是,需要在编写一个辅助函数refresh_bots并绑定后端apigetBotsInfo。每次修改完bot都要执行该辅助函数。

还有一件事,每次调用后端API的时候,请求头都要加上jwtToken来验证。

Bot表设计

image-20230405193353218

获取天梯排行榜

前端:点击页码,根据页码去后端获取当前页码相对应的user信息,然后利用函数update_pages()更新页码信息。

image-20230406101239797

后端则和之前不同,直接返回一个JSON对象列表,列表里存放两类数据:用户信息「头像、名字、天梯分」以及用户总数。

另外,关于分页,Mybatis-Plus自带的插件IPage会自动处理好。配合条件构造器queryWrapper即可根据前端传来的信息筛选出相对应的页面以及该页面所携带的用户信息。

image-20230406102453388

获取录像列表

Record表设计

Record表设计

前端逻辑和获取天梯排行榜一样,不再赘述。

后端也差不多。JSON对象列表里存放以下数据「两个用户的头像、两个用户的名字、对局信息(回放用)、对局结果、对局时间」以及对局总数。

然后配合分页插件和条件构造器筛选出相对应的页面,将相关信息返回给前端。

image-20230406104432718

查看录像

纯前端实现。后端之前已经把对局信息喂给前端并存储到store里,所以其实相当于两个机器人在对战。

这时就可以复用PK界面的函数GameMap.js以及其他辅助函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const stringTo2D = map => {
let g = [];
for (let i = 0, k = 0; i < 13; i++) {
let line = [];
for (let j = 0; j < 14; j++, k++) {
if (map[k] === '0') line.push(0);
else line.push(1);
}
g.push(line);
}
return g;
}
const open_record_content = recordId => {
for (const record of records.value) {
if (record.record.id === recordId) {
store.commit("updateIsRecord", true);
store.commit("updateGame", {
map: stringTo2D(record.record.map),
a_id: record.record.aid,
a_sx: record.record.asx,
a_sy: record.record.asy,
b_id: record.record.bid,
b_sx: record.record.bsx,
b_sy: record.record.bsy,
});
store.commit("updateSteps", {
a_steps: record.record.asteps,
b_steps: record.record.bsteps,
});
store.commit("updateRecordLoser", record.record.loser);
router.push({
name: 'record_content',
params: {
recordId,
}
});
break;
}
}
};
  • updateIsRecord用于标记某场对局是不是录像,对于录像和真实的对战会有不同的逻辑
  • updateGame里面会更新游戏地图信息,真实对战游戏地图信息是随机生成的,显然这里与后端数据一致
  • updateSteps则是给出两条蛇的路径,根据这个信息可以生成蛇的移动

一些前端逻辑的调用

image-20230406110936989

image-20230406111109556

可以看到这里判断出该局游戏是「录像」,于是直接取store里的数据来渲染游戏界面。

前端开始匹配

WebSocket相关知识

前端发送开始匹配请求,后端将用户塞入匹配池,维护一个以用户id为键的websocket池。

一旦用户进入PK界面,就和后端建立一个websocket连接。

image-20230406144624690

若点击了「开始匹配」按钮,前端便向后端发送一个websocket请求:

image-20230406145514000

后端handler接收到调用相对应的函数

image-20230406150358181

把这个用户塞入匹配池。构造SpringCloud服务之间通信用的是RestTemplate

RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。

image-20230406150758707

MatchingPool的Controller捕捉到该请求,调用addPlayer方法

image-20230406151641474

image-20230406154120458

多线程添加player

image-20230406154513412

前端取消匹配

若点击了「取消匹配」按钮,前端向后端发送一个websocket请求:

image-20230406155101475

后端收到请求,调用相关函数

image-20230406155201806

image-20230406155230996

匹配池的handler收到后端发来的请求

image-20230406155455170

调用removePlayer函数删除player

image-20230406155657012

匹配池匹配成功

匹配池线程一直运转,根据相应的算法不断匹配用户

image-20230406162807726

若发现两名用户符合匹配条件,那么就利用restTemplate向后端发送匹配成功的结果并在匹配池中删除这两名玩家。

image-20230406163802777

![image-20230406163830680](/Users/jinhui/Library/Application Support/typora-user-images/image-20230406163830680.png)

后端接收到匹配池发来的信息,调用接口生成游戏

image-20230406163937235

image-20230406164012815

ws服务器开始处理生成游戏的逻辑,首先是生成地图。注意这里的start()是指开启一个新线程。

image-20230406165525613

然后利用websocket的全双工通信向前端发送消息。

image-20230406170202530

前端收到匹配成功的信息,跳转到游戏界面

image-20230406170343705

蛇移动「键盘输入」

处理输入

前端监听函数检测到了键盘输入的事件,向后端发送websocket请求。

image-20230406184246465

ws服务器先判断出是哪名用户,然后调用该用户的相关函数

image-20230406184548124

image-20230406184725507

设置游戏类里面玩家的nextStep属性

image-20230406184905959

后端逻辑判断

image-20230406185049210

nextStep()函数用于确认玩家是否都进行了操作。如果函数返回结果为false,那么说明游戏结束,直接返回对战结果。

image-20230406185341271

judge()函数则是确保双方的下一步操作都合法。如果出现非法操作,那么说明游戏结束。

image-20230406185631900

如果双方的操作都是合法的,那么后端将把本次移动的信息返回给两位玩家。

image-20230406190412133

前端收到ws服务器传来的消息,开始渲染蛇。

image-20230406190738652

蛇移动「代码执行」

如何判断是否为代码执行?

前面的都一样。观察nextStep()函数的这两行代码。

image-20230406191318545

这里就是发送Bot的代码给botRunning微服务。

image-20230406191647600

可以看到该函数会判断当前Player是手动输入还是代码执行。手动输入是默认值-1,代码执行的话botId这个参数会另外设置过。

这些东西在开始匹配的时候就已经考虑到了,看下面的前端代码。

image-20230406191915011

在botRunning微服务里

通过restTemplate发送到SpringCloud微服务后,微服务的Controller捕捉到该请求,并调用相关函数,把bot加到bot池里。

image-20230406192428944

image-20230406192518504

在bot池里和手动输入的差不多,做一个生产者-消费者模型。把bot存到一个队列里。然后按照FIFO原则,挨个执行bot。

image-20230406192735404

image-20230406192937526

bot也得异步执行,得用start()函数新开一个线程。

image-20230406193123338

然后根据工具,运行代码,跑出结果,利用restTemplate向后端发送结果。

image-20230406193254648

在后端

接收到了微服务里发来的信息,并调用相关函数。

image-20230406193908449

然后后端就会根据收到的结果调用setNextStep()操作,之后的步骤就和人工操作一样了。

image-20230406194111814

总结:这段过程差不多是整个项目中最绕的了。

  • 前端带着bot相关信息发送给ws服务器 -> ws服务器发现是bot,将bot发送给微服务 -> 微服务用一个生产者-消费者模型不断执行bot结果 -> 微服务将结果发送给后端 -> 后端得到了下一步操作的结果,然后进行操作的合法性判断

发送对局结果

上述游戏不会一直进行下去,若至少有一名玩家停止操作或进行了非法操作,后端就会宣告游戏结束。

image-20230406195109342

这里主要干了两件事。一是告诉前端游戏已结束,二是将本局的对局结果存储在数据库,以便能够回放对局。

image-20230406195429077

image-20230406195552399

前端收到结果后停止游戏的渲染。

image-20230406195825829