2025年第六章---匹配系统(中)

第六章---匹配系统(中)1 玩家位置同步 1 1 后端修改 玩家的位置也要在服务端确定 确定完之后将每个玩家的位置传到前端 添加一个玩家类 consumer utils Game java import java util List Data AllArgsConst NoArgsConstr

大家好,我是讯享网,很高兴认识大家。

1.玩家位置同步

1.1后端修改

玩家的位置也要在服务端确定,确定完之后将每个玩家的位置传到前端。

添加一个玩家类

consumer.utils.Game.java

import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class Player { 
    private Integer id; private Integer sx;//起始x坐标 private Integer sy;//起始y坐标 private List<Integer> steps;//保存每一步操作---决定了蛇当前的形状 } 

讯享网

在初始化Game的时候,实例化两个Player对象
在这里插入图片描述
讯享网
在这里插入图片描述
WebSocketServer.java中,为了方便管理,将与Game相关的信息,封装成一个JSON

在这里插入图片描述
这样后端就可以将两名玩家的信息(包括生成的地图)传送给前端

1.2前端修改

src\store\pk.js中添加玩家信息的变量和更新函数

讯享网export default ({ 
    state: { 
    status:"matching",//matching表示匹配界面 playing表示对战界面 socket:null,//存储前后端建立的connection opponent_username:"",//对手名 opponent_photo:"",//对手头像 gamemap:null, a_id:0, a_sx:0, a_sy:0, b_id:0, b_sx:0, b_sy:0, }, mutations: { 
    updateSocket(state, socket){ 
    state.socket = socket; }, updateOpponent(state, opponent){ 
    state.opponent_username = opponent.username; state.opponent_photo = opponent.photo; }, updateStatus(state, status){ 
    state.status = status; }, updateGame(state, game){ 
    state.a_id = game.a_id; state.a_sx = game.a_sx; state.a_sy = game.a_sy; state.b_id = game.b_id; state.b_sx = game.b_sx; state.b_sy = game.b_sy; state.gamemap = game.map; }, }, actions: { 
    }, modules: { 
    } }) 

src\views\pk\PkIndexView.vue中,在onmessage中,调用updateGame函数

<script> import PlayGround from '../../components/PlayGround.vue' import MatchGround from '../../components/MatchGround.vue' import { 
    onMounted } from 'vue' import { 
    onUnmounted } from 'vue' import { 
    useStore } from 'vuex' export default { 
    components: { 
    PlayGround, MatchGround }, setup() { 
    const store = useStore(); const socketUrl = `ws://127.0.0.1:3000/websocket/${ 
     store.state.user.token}`; let socket = null; onMounted(() => { 
    ....//省略 socket.onmessage = msg => { 
    const data = JSON.parse(msg.data); console.log(data); if (data.event === "start-matching") { 
    store.commit("updateOpponent", { 
    username: data.opponent_username, photo: data.opponent_photo }); //匹配成功后,延时2秒,进入对战页面 setTimeout(() => { 
    store.commit("updateStatus", "playing") }, 2000); store.commit("updateGame",data.game)//更新Game:包括玩家信息和地图 } } socket.onclose = () => { 
    console.log("disconnected!"); } }); onUnmounted(() => { 
    socket.close(); store.commit("updateStatus", "matching"); }) } } </script> 

运行项目,使用用户名sun和用户名hong的登录,两个浏览器控制台console.log(data.game)的输出内容一致,均为,同步成功

在这里插入图片描述

2.游戏同步:多线程

2.1分析过程

之前只是两个棋盘,在浏览器本地通过wsad和上下左右来控制移动。

现在三个棋盘,两个client和一个server,需要实现三个棋盘的同步

在这里插入图片描述
再来梳理一下之前的游戏流程

在这里插入图片描述
对于从等待用户orBot输入到判别系统这一过程是独立的,
在这里插入图片描述
但是一般代码的执行是单线程,也就是按照顺序执行,例如如果在当前线程执行操作,当等待用户输入的时候,线程就会卡死,需要我们这样一个线程中有多个游戏在运行,只有Game1结束之后才能跑Game2,这样在第二个对局中,玩家就会漫长的等待。

在这里插入图片描述

因此,Game不能作为一个单线程来处理,因此,需要另起一个新的线程来做。

也就是将Game变成一个支持多线程的类

在这里插入图片描述

2.2多线程

首先为WebSocketServer增加一个成员变量,用于记录链接中的Game实例

在这里插入图片描述

在确定两名匹配的玩家之后,更新两名玩家的WebSocketServer连接上的Game实例值。
在这里插入图片描述
然后回到Game.java,将Game变成一个支持多线程的类,只需将Game继承Thread类,就可以支持多线程

讯享网public class Game extends Thread 

然后重写多线程的入口函数run()

在开启一个新线程执行game.start()的时候,新线程中的入口函数,就是run()

初始化两个成员变量,用于表示两名玩家的下一步操作

private Integer nextStepA; private Integer nextStepB; public void setNextStepA(Integer nextStepA) { 
    this.nextStepA = nextStepA; } public void setNextStepB(Integer nextStepB) { 
    this.nextStepB = nextStepB; } 

未来会在WebSocketServer.java中,接收到输入的时候,调用这两个函数

也就是在蓝色的线程里面修改nextStepAnextStepB的值,而在红色的线程里面,会读取这两个线程的值

在这里插入图片描述
这就涉及到两个线程会同时读写一个变量,可能会产生读写冲突,需要枷锁

定义一个锁

讯享网private ReentrantLock lock = new ReentrantLock(); 

之后在setNextStepAsetNextStepB

对两个变量进行更新之前,先锁上,操作完之后,解锁(不管有没有报异常)

public void setNextStepA(Integer nextStepA) { 
    lock.lock(); try { 
    this.nextStepA = nextStepA; }finally { 
    lock.unlock(); } } public void setNextStepB(Integer nextStepB) { 
    lock.lock(); try { 
    this.nextStepB = nextStepB; }finally { 
    lock.unlock(); } } 

nextStep()函数中,负责等待两名玩家的输入,如果都在指定时间内输入了,就返回true

讯享网private boolean nextStep(){ 
   //等待两名玩家的下一步操作 //由于前端动画200ms才能画一个格子 //如果在此期间接收到的输入多于一步 只会留最后一步 多余的会被覆盖 //因此在每一个下一步都要先休息200ms try { 
    Thread.sleep(200); } catch (InterruptedException e) { 
    e.printStackTrace(); } //如果5秒内有玩家没有输入 就返回false for (int i = 0; i < 5; i++) { 
    try { 
    Thread.sleep(1000); lock.lock(); try { 
    if(nextStepA != null && nextStepB != null){ 
    playerA.getSteps().add(nextStepA); playerB.getSteps().add(nextStepB); return true; } }finally { 
    lock.unlock(); } } catch (InterruptedException e) { 
    e.printStackTrace(); } } return false; } 

如果其中一个超时没有输入,游戏就终止,并且分出胜负。

因此还需要定义一个游戏状态status和谁输了loser

private String status = "playing";//游戏状态 playing-->finished private String loser = "";//all:平; A:A输; B:B输了 

最后,在线程的入口run()中初始逻辑如下

讯享网@Override public void run() { 
    for (int i = 0; i < 1000; i++) { 
   //1000步之内游戏肯定结束 if(nextStep()){ 
    //如果获取两个玩家的下一步操作 }else { 
    status = "finished"; if(nextStepA == null && nextStepB == null){ 
    loser = "all"; } else if (nextStepA == null) { 
    loser = "A"; } else{ 
    loser = "B"; } } } } 

但是上面这段逻辑有个问题,如果两名玩家在五秒内没有给出操作,就会进入else判断,此时本应该是平均,也就是loser = "all",但如果下面这段代码执行时,用户给出了输入,结果就会不符合预期。

if(nextStepA == null && nextStepB == null){ 
    loser = "all"; } else if (nextStepA == null) { 
    loser = "A"; } else{ 
    loser = "B"; } 

所以,由于这里涉及到变量的读操作,为了在读的过程中被修改,因此也需要加锁。读完之后再解锁。

讯享网if(nextStep()){ 
    //如果获取两个玩家的下一步操作 System.out.println(); }else { 
    status = "finished"; lock.lock(); try { 
    if(nextStepA == null && nextStepB == null){ 
    loser = "all"; } else if (nextStepA == null) { 
    loser = "A"; } else{ 
    loser = "B"; } }finally { 
    lock.lock(); } } 

然后来看if (nextStep())判断,如果获取两个玩家的下一步操作

需要先进行judge(),来判断输入是否合法

并且,虽然A和B都知道自己的操作,但是看不到对方的操作,因此需要中心服务器以广播的形式来告知。

在这里插入图片描述

@Override public void run() { 
    for (int i = 0; i < 1000; i++) { 
   //1000步之内游戏肯定结束 if (nextStep()) { 
    //如果获取两个玩家的下一步操作 judge(); if(status.equals("playing")){ 
    sentMove(); }else { 
    sentResult(); break; } } else { 
    status = "finished"; lock.lock(); try { 
    if (nextStepA == null && nextStepB == null) { 
    loser = "all"; } else if (nextStepA == null) { 
    loser = "A"; } else { 
    loser = "B"; } } finally { 
    lock.lock(); } sentResult(); break; } } } 

而其中暂时不实现judge的逻辑,其他辅助函数的逻辑如下

讯享网private void sentAllmessage(String message){ 
   //工具函数:向两名玩家广播信息 WebSocketServer.userConnectionInfo.get(playerA.getId()).sendMessage(message); WebSocketServer.userConnectionInfo.get(playerB.getId()).sendMessage(message); } private void sentMove() { 
   //向两个Client广播玩家操作信息 lock.lock();//凡是对操作进行读写的操作 都要加锁 try{ 
    JSONObject resp = new JSONObject(); resp.put("event","move"); resp.put("a_direction",nextStepA); resp.put("b_direction",nextStepB); nextStepA = nextStepB = null;//清空操作 sentAllmessage(resp.toJSONString()); }finally { 
    lock.unlock(); } } private void sentResult() { 
   //向两个client公布结果信息 JSONObject resp = new JSONObject(); resp.put("event","result");//定义事件 resp.put("loser",loser); sentAllmessage(resp.toJSONString()); } 

这样后端基本逻辑完成,接下来是前端与后端的通信,前端要将用户的操作发送过来,以及接收并处理中心服务器的广播

2.3前后端通信

此前判断蛇的移动,在scripts\GameMap.js

 add_listening_events(){ 
    this.ctx.canvas.focus();//聚焦 const [snake0, snake1] = this.snakes; this.ctx.canvas.addEventListener("keydown",e=>{ 
    console.log(e.key); //wasd控制左下角球 上下左右控制右上角球 if(e.key === 'w') snake0.set_direction(0); else if (e.key === 'd') snake0.set_direction(1); else if (e.key === 's') snake0.set_direction(2); else if (e.key === 'a') snake0.set_direction(3); else if (e.key === 'ArrowUp') snake1.set_direction(0); else if (e.key === 'ArrowRight') snake1.set_direction(1); else if (e.key === 'ArrowDown') snake1.set_direction(2); else if (e.key === 'ArrowLeft') snake1.set_direction(3); }); } 

这里,由于一个client负责一个玩家,只处理wsad即可。

修改如下,将玩家的操作操作传送到后端

讯享网 add_listening_events(){ 
    this.ctx.canvas.focus();//聚焦 const [snake0, snake1] = this.snakes; this.ctx.canvas.addEventListener("keydown",e=>{ 
    console.log(e.key); //wasd控制移动 let d = -1; if(e.key === 'w') d = 0; else if (e.key === 'd') d = 1; else if (e.key === 's') d = 2; else if (e.key === 'a') d = 3; if(d >= 0){ 
   //有效输入 this.store.state.pk.socket.sent(JSON.stringify({ 
   //将JSON转换为字符串 event:"move", direction:d, })) } }); } 

后端接收并分配给专门的路由来进行处理

private void move(Integer direction) { 
    //判断是A玩家还是B玩家在操作 if(game.getPlayerA().getId().equals(user.getId())){ 
    game.setNextStepA(direction); }else if (game.getPlayerB().getId().equals(user.getId())) { 
    game.setNextStepB(direction); } else { 
    Exception e = new Exception("Error"); e.printStackTrace(); } } @OnMessage public void onMessage(String message, Session session) { 
   //当做路由 分配任务 // Server从Client接收消息时触发 System.out.println("Receive message!"); JSONObject data = JSONObject.parseObject(message);//将字符串解析成JSON String event = data.getString("event"); if("start-matching".equals(event)){ 
   //防止event为空的异常 startMatching(); } else if ("stop-matching".equals(event)) { 
    stopMatching(); } else if ("move".equals(event)) { 
    Integer direction = data.getInteger("direction"); System.out.println(direction); move(direction); } } 

此时,client端用户输入WSAD的时候,后端就能准确接收到信息。

在这里插入图片描述
同时,前端也要接收后端的广播来的信息,具体有两种event,分别是moveresult

2.4(1) event == move

在这里插入图片描述
对操作进行更新需要用到Snack.js中的set_direction方法

在这里插入图片描述
两个玩家控制的snack对象在保存在GameMap对象中。

在这里插入图片描述
为了取到,需要将GameMap对象,作为游戏对象,保存为全局变量

在这里插入图片描述
先在src\store\pk.js中将gameObject存入全局变量,并写好更新函数

在这里插入图片描述
这样就能获取到游戏对象,并且更新两个玩家控制的snack的方向
在这里插入图片描述
此时,两个玩家都能够控制蛇正常移动

在这里插入图片描述
但是每次输入之后都会感觉到一些延迟,是因为输入之后可能线程还处于睡眠状态

在这里插入图片描述
调整为:

在这里插入图片描述

2.5(2) event == result

之前判断玩家输赢(蛇的状态)的逻辑在前端

讯享网//如果下一步操作撞了 蛇瞬间去世 if(!this.gamemap.check_valid(this.next_cell)){ 
    this.status = "die"; } 

在这里插入图片描述
将这段代码去掉。现在要交由后端来播报结果。

判断输赢有两部分逻辑:撞墙和超时,超时的逻辑已经写好,现在写判断撞墙的逻辑

参考前端GameMap.js中的check_valid(cell)函数

在这里插入图片描述
后端逻辑如下:

1)首先需要将两名玩家所控制的蛇取到:

新建Cell类代表蛇的单元

@Data @AllArgsConstructor @NoArgsConstructor public class Cell { 
    private Integer x; private Integer y; } 

Player.java中,将蛇的身体返回

0、1、2、3位置表示表示上右下左

在这里插入图片描述
对于四种操作0(w), 1(d), 2(s), 3(a)分别在行和列方向上的偏移量

讯享网int[] dx = { 
   -1, 0, 1, 0};//行方向的偏移量 int[] dy = { 
   0, 1, 0, -1}; //列方向的偏移量 

所以Player.java的逻辑更新为

@Data @AllArgsConstructor @NoArgsConstructor public class Player { 
    private Integer id; private Integer sx;//起始x坐标 private Integer sy;//起始y坐标 private List<Integer> steps;//保存每一步操作---决定了蛇当前的形状 //检验当前回合 蛇的长度是否增加 private boolean check_tail_increasing(int step){ 
    if(step <= 10) return true; else return step % 3 == 1; } //返回蛇的身体 public List<Cell> getCells(){ 
    List<Cell> res = new ArrayList<>(); //对于四种操作0(w), 1(d), 2(s), 3(a) // 在行和列方向上的计算偏移量 int[] dx = { 
   -1, 0, 1, 0}; int[] dy = { 
   0, 1, 0, -1}; int x = sx; int y = sy; int step = 0;//回合数 res.add(new Cell(x,y));//添加起点 //不断根据steps计算出整个蛇身体 for (Integer d : steps) { 
    x += dx[d]; y += dy[d]; res.add(new Cell(x,y)); if(!check_tail_increasing(++step)){ 
    //如果蛇尾不增加 就删掉蛇尾 res.remove(0);//O(N) } } return res; } } 

2)判断两名玩家最后一步操作是否合法

  • 没有撞到障碍物
  • 没有撞到两条蛇的身体
    • 没有撞到自己:最后一步与之前n-1个Cell是否重合
    • 没有撞到别人:最后一步与之前n-1个Cell是否重合
      • 由于A和B不可能走到同一个格子 因此不用判断最后一个格子是否重合

只需要判断最后一步,也就是蛇的最后一个Cell是否符合上面三种原则即可。

讯享网private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) { 
    int n = cellsA.size(); Cell cell = cellsA.get(n - 1);//取到A的最后一步 //三种不合法操作: A撞墙、A撞A、A撞B //A撞墙 if(g[cell.getX()][cell.getY()] == 1) return false; //A撞A for (int i = 0; i < n - 1; i++) { 
    if(cellsA.get(i).getX().equals(cell.getX()) && cellsA.get(i).getY().equals(cell.getY())){ 
    return false; } } //A撞B for (int i = 0; i < n - 1; i++) { 
    if(cellsB.get(i).getX().equals(cell.getX()) && cellsB.get(i).getY().equals((cell.getY()))){ 
    return false; } } return true; } private void judge() { 
    List<Cell> cellsA = playerA.getCells(); List<Cell> cellsB = playerB.getCells(); //判断两名玩家最后一步操作是否合法 boolean validA = check_valid(cellsA, cellsB); boolean validB = check_valid(cellsB, cellsA); if(!validA || !validB){ 
    status = "finished"; if(validA){ 
    loser = "B"; } else if (validB) { 
    loser = "A"; } else { 
    loser = "all"; } } } 

此时就能正常的进行合法性判断。

在这里插入图片描述

2.6游戏结果展示

最后,还需要将游戏的结果在前端展示,并且,设置一个重启按钮,点击重启之后,重新开始一局。

pk.js中新增变量,方便用于展示谁赢谁输

在这里插入图片描述新增一个组件ResultBoard.vue用于展示结果

在这里插入图片描述

核心代码如下:

在这里插入图片描述
然后在对战页面PkIndexView.vue导入组件,使其在loser!=none时展示出来
在这里插入图片描述
并且在收到后端播报结果时,更新全局变量中的loser

在这里插入图片描述
最终的结果如下,成功的实现了结果展示和重来一局。

在这里插入图片描述
点击重启

在这里插入图片描述
此时,再匹配的用户,又可以开始新的一轮对战。

在这里插入图片描述
这样,游戏同步功能就全部完成。

3.对局记录

接下来来实现另外一功能,就是将对局记录保存在数据库中。

3.1创建record

1)创建record表用来记录每局对战的信息

表中的列:

  • id: int
    • 非空 自增 唯一 主键
  • a_id: int
  • a_sx: int
  • a_sy: int
  • b_id: int
  • b_sx: int
  • b_sy: int
  • a_steps: varchar(1000)
  • b_steps: varchar(1000)
  • map: varchar(1000)
  • loser: varchar(10)
  • createtime: datetime

2)创建Pojo

注意,数据库中如果用下划线,则在pojo中要使用驼峰命名法

@Data @AllArgsConstructor @NoArgsConstructor public class Record { 
    @TableId(type = IdType.AUTO) private Integer id; private Integer aId; private Integer aSx; private Integer aSy; private Integer bId; private Integer bSx; private Integer bSy; private String aSteps; private String bSteps; private String map; private String loser; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date createtime; } 

3)创建Mapper

讯享网@Mapper public interface RecordMapper extends BaseMapper<Record> { 
    } 

写入数据库
首先将RecordMapper实例注入到WebSocketServer

在这里插入图片描述

Game.java中,在每次向client播报结果之前,将记录保存到数据库

在这里插入图片描述
这样在每局游戏结束时,记录就被保存下来

在这里插入图片描述
后续就可以根据记录来复原游戏画面

小讯
上一篇 2025-02-23 23:11
下一篇 2025-04-08 23:26

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/25879.html