网站建设问题清单,网站文章只被收录网站首页,医联媒体网站建设,有产品怎么找销售渠道笔记内容转载自 AcWing 的 SpringBoot 框架课讲义#xff0c;课程链接#xff1a;AcWing SpringBoot 框架课。 CONTENTS 1. 同步玩家位置1.1 游戏信息的记录1.2 实现多线程同步移动 2. 同步碰撞检测3. 实现游戏结束界面4. 持久化游戏状态4.1 创建数据库表4.2 保存游戏对局信息…笔记内容转载自 AcWing 的 SpringBoot 框架课讲义课程链接AcWing SpringBoot 框架课。 CONTENTS 1. 同步玩家位置1.1 游戏信息的记录1.2 实现多线程同步移动 2. 同步碰撞检测3. 实现游戏结束界面4. 持久化游戏状态4.1 创建数据库表4.2 保存游戏对局信息 1. 同步玩家位置
1.1 游戏信息的记录
两名玩家初始位置需要由服务器确定且之后的每次移动都需要在服务器上判断。我们需要在 Game 类中添加 Player 类用来记录玩家的信息在 consumer.utils 包下创建 Player 类
package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.List;Data
NoArgsConstructor
AllArgsConstructor
public class Player {private Integer id;private Integer sx;private Integer sy;private ListInteger steps; // 记录历史走过的每一步方向
}然后就可以在 Game 中创建玩家
package com.kob.backend.consumer.utils;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;public class Game {...private final Player playerA, playerB;public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {...playerA new Player(idA, rows - 2, 1, new ArrayList()); // 默认A在左下角B在右上角playerB new Player(idB, 1, cols - 2, new ArrayList());}public Player getPlayerA() {return playerA;}public Player getPlayerB() {return playerB;}...
}在 WebSocketServer 中创建 Game 时传入两名玩家的 ID并且我们将与游戏内容相关的信息全部包装到一个 JSONObject 类中
package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;Component
ServerEndpoint(/websocket/{token}) // 注意不要以/结尾
public class WebSocketServer {...private void startMatching() {System.out.println(Start matching!);matchPool.add(this.user);while (matchPool.size() 2) { // 临时调试用的未来要替换成微服务IteratorUser it matchPool.iterator();User a it.next(), b it.next();matchPool.remove(a);matchPool.remove(b);Game game new Game(13, 14, 20, a.getId(), b.getId());game.createMap();JSONObject respGame new JSONObject();respGame.put(a_id, game.getPlayerA().getId());respGame.put(a_sx, game.getPlayerA().getSx());respGame.put(a_sy, game.getPlayerA().getSy());respGame.put(b_id, game.getPlayerB().getId());respGame.put(b_sx, game.getPlayerB().getSx());respGame.put(b_sy, game.getPlayerB().getSy());respGame.put(map, game.getG());JSONObject respA new JSONObject(); // 发送给A的信息respA.put(event, match_success);respA.put(opponent_username, b.getUsername());respA.put(opponent_photo, b.getPhoto());respA.put(game, respGame);users.get(a.getId()).sendMessage(respA.toJSONString()); // A不一定是当前链接因此要在users中获取JSONObject respB new JSONObject(); // 发送给B的信息respB.put(event, match_success);respB.put(opponent_username, a.getUsername());respB.put(opponent_photo, a.getPhoto());respB.put(game, respGame);users.get(b.getId()).sendMessage(respB.toJSONString());}}...
}前端也需要进行相应的修改在 store/pk.js 中创建两名玩家的信息
export default {state: {...a_id: 0,a_sx: 0,a_sy: 0,b_id: 0,b_sx: 0,b_sy: 0,gameObject: null, // 整个GameMap对象},getters: {},mutations: {...updateGame(state, game) {state.game_map game.map;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;},updateGameObject(state, gameObject) {state.gameObject gameObject;},},actions: {},modules: {},
};在 GameMap.vue 中需要先将 GameMap 对象存下来之后会在 PKIndexView 中用到
...script
import { ref, onMounted } from vue;
import { GameMap } from /assets/scripts/GameMap;
import { useStore } from vuex;export default {setup() {const store useStore();let parent ref(null);let canvas ref(null);onMounted(() {store.commit(updateGameObject,new GameMap(canvas.value.getContext(2d), parent.value, store));});return {parent,canvas,};},
};
/script...PKIndexView 中要传入从后端获取到的 game 数据
...script
...export default {...setup() {...onMounted(() {...socket.onmessage (msg) { // 接收到后端消息时会执行...if (data.event match_success) { // 匹配成功...store.commit(updateGame, data.game); // 更新游戏内容...}};...});...},
};
/scriptstyle scoped/style1.2 实现多线程同步移动
我们需要实现两名玩家的客户端以及服务器端的移动同步假如 Client1 发出了移动指令那么就会将这个消息发送给服务器同理另一个客户端 Client2 发出移动指令时也会将消息发送给服务器服务器在接收到两名玩家的消息后再将消息同步给两名玩家。
我们在 WebSocketServer 中会维护一个游戏 Game这个 Game 也有自己的执行流程它会先创建地图 creatMap接着会一步一步执行即 nextStep每一步会等待两名玩家的操作这个操作可以是键盘输入也可以是由 Bot 代码执行的微服务返回回来的结果。获取输入后会将结果发送给一个评判系统 judge来判断两名玩家下一步是不是合法的如果有一方不合法就游戏结束。
在等待用户输入时会有一个时间限制比如5秒如果有一方还没有输入则表示输了同样也是游戏结束。否则如果两方输入的下一步都是合法的则继续循环 nextStep。这个 nextStep 流程是比较独立的而且每个游戏对局都有这个独立的过程如果 Game 是单线程的那么在等待用户输入时这个线程就会卡死如果有多个游戏对局的话那么只能先卡死在某个对局中其他对局的玩家体验就会很差因此 Game 不能作为一个单线程来处理每次在等待用户输入时都需要另起一个新的线程这就涉及到了多线程的通信以及加锁的问题。
我们将 Game 类继承自 Thread 类即可转为多线程然后需要实现 Thread 的入口函数使用快捷键 Alt Insert选择重写方法需要重写的是 run() 方法这是新线程的入口函数
package com.kob.backend.consumer.utils;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;public class Game extends Thread {private final Integer rows;private final Integer cols;private final Integer inner_walls_count;private final boolean[][] g;private static final int[] dx { -1, 0, 1, 0 }, dy { 0, 1, 0, -1 };private final Player playerA, playerB;private Integer nextStepA null; // 下一步操作0、1、2、3分别表示四个方向null表示还没有获取到private Integer nextStepB null;private ReentrantLock lock new ReentrantLock(); // 需要给nextStep变量上锁防止读写冲突private String status playing; // 整局游戏的状态结束后为finishedprivate String loser ; // 输的一方是谁all表示平局public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {this.rows rows;this.cols cols;this.inner_walls_count inner_walls_count;this.g new boolean[rows][cols];playerA new Player(idA, rows - 2, 1, new ArrayList()); // 默认A在左下角B在右上角playerB new Player(idB, 1, cols - 2, new ArrayList());}public Player getPlayerA() {return playerA;}public Player getPlayerB() {return playerB;}public void setNextStepA(Integer nextStepA) { // 未来会在另一个线程中调用lock.lock(); // 操作nextStep变量前先上锁try {this.nextStepA nextStepA;} finally {lock.unlock(); // 操作完后无论是否有异常都解锁}}public void setNextStepB(Integer nextStepB) {lock.lock();try {this.nextStepB nextStepB;} finally {lock.unlock();}}public boolean[][] getG() {return g;}private boolean check_connectivity(int sx, int sy, int tx, int ty) {if (sx tx sy ty) return true;g[sx][sy] true;for (int i 0; i 4; i) {int nx sx dx[i], ny sy dy[i];if (!g[nx][ny] check_connectivity(nx, ny, tx, ty)) {g[sx][sy] false; // 注意在这里我们用的g就是原始数组因此修改后要记得还原return true;}}g[sx][sy] false; // 记得还原return false;}private boolean drawMap() {// 初始化障碍物标记数组for (int i 0; i this.rows; i) {Arrays.fill(g[i], false);}// 给地图四周加上障碍物for (int r 0; r this.rows; r) {g[r][0] g[r][this.cols - 1] true;}for (int c 0; c this.cols; c) {g[0][c] g[this.rows - 1][c] true;}// 添加地图内部的随机障碍物需要有对称性因此枚举一半即可另一半对称生成Random random new Random();for (int i 0; i this.inner_walls_count / 2; i) {for (int j 0; j 10000; j) {int r random.nextInt(this.rows); // 返回0~this.rows-1的随机整数int c random.nextInt(this.cols);if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;if (r this.rows - 2 c 1 || r 1 c this.cols - 2) continue;g[r][c] g[this.rows - 1 - r][this.cols - 1 - c] true;break;}}return check_connectivity(this.rows - 2, 1, 1, this.cols - 2);}public void createMap() {for (int i 0; i 10000; i) {if (drawMap()) {break;}}}private boolean nextStep() { // 等待两名玩家的下一步操作在该方法中也会操作nextStep变量try {Thread.sleep(500); // 前端的蛇每秒走2格因此走一格需要500ms每次后端执行下一步时需要先sleep否则快速的多次输入将会覆盖掉之前输入的信息} catch (InterruptedException e) {e.printStackTrace();}for (int i 0; i 50; i) {try {Thread.sleep(100); // 每回合循环50次每次睡眠100ms即一回合等待用户输入的时间为5slock.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;}private void judge() { // 判断两名玩家下一步操作是否合法}private void sendAllMessage(String message) { // 向两个Client发送消息WebSocketServer.users.get(playerA.getId()).sendMessage(message);WebSocketServer.users.get(playerB.getId()).sendMessage(message);}private void sendMove() { // 向两个Client发送移动消息lock.lock();try {JSONObject resp new JSONObject();resp.put(event, move);resp.put(a_direction, nextStepA);resp.put(b_direction, nextStepB);sendAllMessage(resp.toJSONString());nextStepA nextStepB null;} finally {lock.unlock();}}private void sendResult() { // 向两个Client公布结果JSONObject resp new JSONObject();resp.put(event, result);resp.put(loser, loser);sendAllMessage(resp.toJSONString());}Overridepublic void run() {for (int i 0; i 1000; i) { // 游戏最多走的步数不会超过1000if (nextStep()) { // 是否获取了两条蛇的下一步操作judge();if (playing.equals(status)) { // 如果游戏还在进行中则需要将两名玩家的操作广播给两个ClientsendMove();} else {sendResult();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.unlock();}sendResult(); // 这一步结束后需要给两个Client发送消息break;}}}
}然后前端 GameMap.js 中在移动时需要向后端通信现在两名玩家的键盘输入操作就只需要 W/S/A/D 了
import { AcGameObject } from ./AcGameObject;
import { Wall } from ./Wall;
import { Snake } from ./Snake;export class GameMap extends AcGameObject {...add_listening_events() {this.ctx.canvas.focus(); // 使Canvas聚焦this.ctx.canvas.addEventListener(keydown, e {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 ! -1) {this.store.state.pk.socket.send(JSON.stringify({event: move,direction: d,}));}});}...
}WebSocketServer 对于每局游戏对局都会创建一个 Game 类通过 start() 方法可以新开一个线程运行 Game 中的 run() 方法由于我们需要在 Game 中使用 WebSocketServer 的 users还需要将 users 修改为 public然后需要接收前端的移动请求
package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;Component
ServerEndpoint(/websocket/{token}) // 注意不要以/结尾
public class WebSocketServer {// ConcurrentHashMap是一个线程安全的哈希表用于将用户ID映射到WS实例public static final ConcurrentHashMapInteger, WebSocketServer users new ConcurrentHashMap();// CopyOnWriteArraySet也是线程安全的private static final CopyOnWriteArraySetUser matchPool new CopyOnWriteArraySet(); // 匹配池private User user;private Session session null;private Game game null;private static UserMapper userMapper;Autowiredpublic void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper userMapper;}OnOpenpublic void onOpen(Session session, PathParam(token) String token) throws IOException {this.session session;System.out.println(Connected!);Integer userId JwtAuthentication.getUserId(token);this.user userMapper.selectById(userId);if (user ! null) {users.put(userId, this);} else {this.session.close();}}OnClosepublic void onClose() {System.out.println(Disconnected!);if (this.user ! null) {users.remove(this.user.getId());matchPool.remove(this.user);}}OnMessagepublic void onMessage(String message, Session session) { // 一般会把onMessage()当作路由System.out.println(Receive message!);JSONObject data JSONObject.parseObject(message);String event data.getString(event); // 取出event的内容if (start_match.equals(event)) { // 开始匹配this.startMatching();} else if (stop_match.equals(event)) { // 取消匹配this.stopMatching();} else if (move.equals(event)) { // 移动move(data.getInteger(direction));}}OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}public void sendMessage(String message) { // 从后端向当前链接发送消息synchronized (this.session) { // 由于是异步通信需要加一个锁try {this.session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}}private void startMatching() {System.out.println(Start matching!);matchPool.add(this.user);while (matchPool.size() 2) { // 临时调试用的未来要替换成微服务IteratorUser it matchPool.iterator();User a it.next(), b it.next();matchPool.remove(a);matchPool.remove(b);game new Game(13, 14, 20, a.getId(), b.getId());game.createMap();users.get(a.getId()).game game;users.get(b.getId()).game game;game.start(); // 开一个新的线程JSONObject respGame new JSONObject();respGame.put(a_id, game.getPlayerA().getId());respGame.put(a_sx, game.getPlayerA().getSx());respGame.put(a_sy, game.getPlayerA().getSy());respGame.put(b_id, game.getPlayerB().getId());respGame.put(b_sx, game.getPlayerB().getSx());respGame.put(b_sy, game.getPlayerB().getSy());respGame.put(map, game.getG());JSONObject respA new JSONObject(); // 发送给A的信息respA.put(event, match_success);respA.put(opponent_username, b.getUsername());respA.put(opponent_photo, b.getPhoto());respA.put(game, respGame);users.get(a.getId()).sendMessage(respA.toJSONString()); // A不一定是当前链接因此要在users中获取JSONObject respB new JSONObject(); // 发送给B的信息respB.put(event, match_success);respB.put(opponent_username, a.getUsername());respB.put(opponent_photo, a.getPhoto());respB.put(game, respGame);users.get(b.getId()).sendMessage(respB.toJSONString());}}private void stopMatching() {System.out.println(Stop matching!);matchPool.remove(this.user);}private void move(Integer direction) {if (game.getPlayerA().getId().equals(user.getId())) {game.setNextStepA(direction);} else if (game.getPlayerB().getId().equals(user.getId())) {game.setNextStepB(direction);}}
}最后在 PKIndexView 中处理接收到后端发来的移动消息以及游戏结束消息
templatePlayGround v-if$store.state.pk.status playing /MatchGround v-else /
/templatescript
import PlayGround from /components/PlayGround.vue;
import MatchGround from /components/MatchGround.vue;
import { onMounted, onUnmounted } from vue;
import { useStore } from vuex;export default {components: {PlayGround,MatchGround,},setup() {const store useStore();let socket null;let socket_url ws://localhost:3000/websocket/${store.state.user.jwt_token}/;onMounted(() {socket new WebSocket(socket_url);store.commit(updateOpponent, {username: 我的对手,photo: https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png,});socket.onopen () { // 链接成功建立后会执行console.log(Connected!);store.commit(updateSocket, socket);};socket.onmessage (msg) { // 接收到后端消息时会执行const data JSON.parse(msg.data); // Spring传过来的数据是放在消息的data中console.log(data);if (data.event match_success) { // 匹配成功store.commit(updateOpponent, { // 更新对手信息username: data.opponent_username,photo: data.opponent_photo,});store.commit(updateGame, data.game); // 更新游戏内容setTimeout(() { // 3秒后再进入游戏地图界面store.commit(updateStatus, playing);}, 3000);} else if (data.event move) { // 两名玩家的移动const gameObject store.state.pk.gameObject;const [snake0, snake1] gameObject.snakes;snake0.set_direction(data.a_direction);snake1.set_direction(data.b_direction);} else if (data.event result) { // 游戏结束const gameObject store.state.pk.gameObject;const [snake0, snake1] gameObject.snakes;if (data.loser all || data.loser A) {snake0.status die;}if (data.loser all || data.loser B) {snake1.status die;}}};socket.onclose () { // 关闭链接后会执行console.log(Disconnected!);store.commit(updateStatus, matching); // 进入游戏地图后玩家点击其他页面应该是默认退出游戏};});onUnmounted(() {socket.close(); // 如果不断开链接每次切换页面都会创建新链接就会导致有很多冗余链接});},
};
/scriptstyle scoped/style2. 同步碰撞检测
现在还需要将碰撞检测放到后端进行判断先将 Snake.js 中的碰撞检测判断代码删掉并将死后变白的逻辑放到 render() 函数中
...export class Snake extends AcGameObject {...next_step() { // 将蛇的状态变为走下一步...// if (!this.gamemap.check_next_valid(this.next_cell)) { // 下一步不合法// this.status die;// }}...render() {...ctx.fillStyle this.color;if (this.status die) {ctx.fillStyle white;}...}
}接下来需要实现后端中的 judge() 方法在判断的时候需要知道当前蛇的身体有哪些先在 comsumer.utils 包下创建 Cell 类表示身体的每一格
package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;Data
AllArgsConstructor
NoArgsConstructor
public class Cell {int x;int y;
}然后在 Player 类中创建一个方法能够根据玩家历史走过的路径找出当前这条蛇身体的每一格
package com.kob.backend.consumer.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.List;Data
NoArgsConstructor
AllArgsConstructor
public class Player {private Integer id;private Integer sx;private Integer sy;private ListInteger steps; // 记录历史走过的每一步方向private boolean check_tail_increasing(int step) { // 检测当前回合蛇的长度是否增加if (step 7) return true; // 前7回合每一回合长度都增加return step % 3 1; // 之后每3回合增加一次长度}public ListCell getCells() { // 返回蛇的身体每次都根据蛇历史走的方向将其每一格找出来ListCell cells new ArrayList();int[] dx { -1, 0, 1, 0 }, dy { 0, 1, 0, -1 };int x sx, y sy;int step 0;cells.add(new Cell(x, y));for (int d: steps) {x dx[d];y dy[d];cells.add(new Cell(x, y));if (!check_tail_increasing(step)) {cells.remove(0); // 删掉蛇尾即第一个起始的位置}}return cells;}
}最后即可在 Game 类中实现 judge() 方法
...public class Game extends Thread {...private boolean check_valid(ListCell cellsA, ListCell cellsB) { // 判断A是否合法int n cellsA.size();Cell headCellA cellsA.get(n - 1); // A的头也就是最后一个Cellif (g[headCellA.x][headCellA.y]) {return false;}for (int i 0; i n - 1; i) { // 判断除了头以外的其他身体部分if (cellsA.get(i).x headCellA.x cellsA.get(i).y headCellA.y) {return false;}if (cellsB.get(i).x headCellA.x cellsB.get(i).y headCellA.y) {return false;}}return true;}private void judge() { // 判断两名玩家下一步操作是否合法ListCell cellsA playerA.getCells();ListCell cellsB playerB.getCells();boolean validA check_valid(cellsA, cellsB);boolean validB check_valid(cellsB, cellsA);if (!validA || !validB) {status finished;if (!validA !validB) {loser all;} else if (!validA) {loser A;} else {loser B;}}}...
}3. 实现游戏结束界面
首先我们需要将输的玩家记录到前端的全局变量中在 store/pk.js 中添加 loser 变量
export default {state: {...loser: none, // none表示没人输all表示平局A/B表示A/B赢},getters: {},mutations: {...updateLoser(state, loser) {state.loser loser;},},actions: {},modules: {},
};然后在 PKIndexView 组件的游戏结束处理语句块中添加更新 loser 的语句
store.commit(updateLoser, data.loser);游戏结束后需要给用户给用户展示谁赢谁输的界面并提供一个重开按钮在 components 目录下创建 ResultBoard.vue
templatediv classcard text-bg-secondary text-centerdiv classcard-header stylefont-size: 26px;游戏结束/divdiv classcard-body stylebackground-color: rgba(255, 255, 255, 0.4);div classresult_board_text v-if$store.state.pk.loser allDraw/divdiv classresult_board_text v-else-if$store.state.pk.loser A $store.state.pk.a_id.toString() $store.state.user.idLose/divdiv classresult_board_text v-else-if$store.state.pk.loser B $store.state.pk.b_id.toString() $store.state.user.idLose/divdiv classresult_board_text v-elseWin/divdiv classresult_board_btnbutton clickreturnHome typebutton classbtn btn-info btn-lg返回主页/button/div/div/div
/templatescript
import { useStore } from vuex;export default {setup() {const store useStore();const returnHome () { // 需要复原一些全局变量store.commit(updateStatus, matching);store.commit(updateLoser, none);store.commit(updateOpponent, {username: 我的对手,photo: https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png,});};return {returnHome,};},
};
/scriptstyle scoped
.card {width: 30vw;position: absolute;top: 25vh;left: 35vw;
}.result_board_text {color: white;font-size: 50px;font-weight: bold;font-style: italic;padding: 5vh 0;
}.result_board_btn {padding: 3vh 0;
}
/style4. 持久化游戏状态
4.1 创建数据库表
最后我们还需要将游戏过程存到数据库中方便用户之后回看游戏录像在数据库中创建 record 表用来记录每局对战的信息
id: int主键、自增、非空a_id: inta_sx: inta_sy: intb_id: intb_sx: intb_sy: inta_steps: varchar(1000)b_steps: varchar(1000)map: varchar(1000)loser: varchar(10)createtime: datetime
创建该数据库表的 SQL 语句如下
CREATE TABLE kob.record (id int NOT NULL AUTO_INCREMENT,a_id int NULL,a_sx int NULL,a_sy int NULL,b_id int NULL,b_sx int NULL,b_sy int NULL,a_steps varchar(1000) NULL,b_steps varchar(1000) NULL,map varchar(1000) NULL,loser varchar(10) NULL,createtime datetime NULL,PRIMARY KEY (id)
);在 pojo 包下创建 Record 类如下
package com.kob.backend.pojo;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;Data
NoArgsConstructor
AllArgsConstructor
public class Record {TableId(value id, 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;
}在 mapper 包下创建 RecordMapper 类如下
package com.kob.backend.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kob.backend.pojo.Record;
import org.apache.ibatis.annotations.Mapper;Mapper
public interface RecordMapper extends BaseMapperRecord {
}4.2 保存游戏对局信息
可以在向前端发送游戏结果消息之前将对局信息存下来首先需要在 WebSocketServer 中将 RecordMapper 创建出来
...Component
ServerEndpoint(/websocket/{token}) // 注意不要以/结尾
public class WebSocketServer {...public static RecordMapper recordMapper; // 要在Game中调用Autowiredpublic void setRecordMapper(RecordMapper recordMapper) {WebSocketServer.recordMapper recordMapper;}...
}然后在 Player 中创建辅助函数用来返回 steps 的字符串形式
...Data
NoArgsConstructor
AllArgsConstructor
public class Player {...private ListInteger steps; // 记录历史走过的每一步方向...public String getStringSteps() { // 将steps转换成字符串StringBuilder res new StringBuilder();for (int d: steps) {res.append(d);}return res.toString();}
}最后就可以在 Game 中将游戏记录保存至数据库中
...public class Game extends Thread {...private String getStringMap() { // 将g转换成01字符串StringBuilder res new StringBuilder();for (int i 0; i rows; i) {for (int j 0; j cols; j) {res.append(g[i][j] ? 1 : 0);}}return res.toString();}private void saveRecord() { // 将对局信息存到数据库中Record record new Record(null,playerA.getId(),playerA.getSx(),playerA.getSy(),playerB.getId(),playerB.getSx(),playerB.getSy(),playerA.getStringSteps(),playerB.getStringSteps(),getStringMap(),loser,new Date());WebSocketServer.recordMapper.insert(record);}private void sendResult() { // 向两个Client公布结果JSONObject resp new JSONObject();resp.put(event, result);resp.put(loser, loser);saveRecord(); // 在发送结束消息给前端之前先将游戏记录存下来sendAllMessage(resp.toJSONString());}...
}