From ae4d8ef42d605298572231e5b15ba072d97e63d7 Mon Sep 17 00:00:00 2001 From: flykhan Date: Fri, 3 Mar 2023 16:51:58 +0800 Subject: [PATCH] =?UTF-8?q?=E8=9B=87=E7=9A=84=E6=8E=A7=E5=88=B6=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=A2=9E=E9=95=BF=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=A2=B0=E6=92=9E=E6=A3=80=E6=B5=8B=E6=94=B9=E4=B8=BA=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=AE=9E=E7=8E=B0=20+=20=E7=BB=93=E6=9E=9C=E8=AE=A1?= =?UTF-8?q?=E5=88=86=E6=9D=BF=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kob/backend/consumer/WebSocketServer.java | 29 ++- .../com/kob/backend/consumer/utils/Cell.java | 12 + .../com/kob/backend/consumer/utils/Game.java | 225 +++++++++++++++++- .../kob/backend/consumer/utils/Player.java | 51 ++++ web/src/assets/scripts/GameMap.js | 29 ++- web/src/assets/scripts/Snake.js | 5 +- web/src/components/GameMap.vue | 6 +- web/src/components/ResultBoard.vue | 78 ++++++ web/src/store/pk.js | 23 ++ web/src/views/pk/PkIndexView.vue | 40 +++- 10 files changed, 475 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/com/kob/backend/consumer/utils/Cell.java create mode 100644 backend/src/main/java/com/kob/backend/consumer/utils/Player.java create mode 100644 web/src/components/ResultBoard.vue diff --git a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java index a87f4cd..db6e841 100644 --- a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java +++ b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java @@ -27,7 +27,7 @@ public class WebSocketServer { 使用线程安全的 Hash 表 ConcurrentHashMap<>(), 将 userId 映射到 webSocketServer */ - private static final ConcurrentHashMap users = new ConcurrentHashMap<>(); + public static final ConcurrentHashMap users = new ConcurrentHashMap<>(); // 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool private static final CopyOnWriteArraySet matchPool = new CopyOnWriteArraySet<>(); // 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 @@ -36,6 +36,7 @@ public class WebSocketServer { private Session session = null; // 用户信息:定义一个成员变量 private User user; + private Game game = null; // 注入方法 @Autowired @@ -99,14 +100,24 @@ public class WebSocketServer { matchPool.remove(b); // 匹配成功时,创建联机地图 - Game game = new Game(13,14,20); + Game game = new Game(13,14,20,a.getId(),b.getId()); game.createMap(); // 初始化地图 + users.get(a.getId()).game = game; // 将 game 赋给 a 玩家 + users.get(b.getId()).game = game; + + game.start(); // 开启新线程,执行函数 JSONObject respGameData = new JSONObject(); respGameData.put("game_map",game.getG()); respGameData.put("rows",game.getRows()); respGameData.put("cols",game.getCols()); respGameData.put("inner_walls_count",game.getInnerWallsCount()); + respGameData.put("a_id",game.getPlayerA().getId()); + respGameData.put("a_sx",game.getPlayerA().getSx()); + respGameData.put("a_sy",game.getPlayerA().getSy()); + respGameData.put("b_id",game.getPlayerB().getId()); + respGameData.put("b_sx",game.getPlayerB().getSx()); + respGameData.put("b_sy",game.getPlayerB().getSy()); // 将 a 配对成功的消息传回客户端 JSONObject respA = new JSONObject(); @@ -133,6 +144,18 @@ public class WebSocketServer { matchPool.remove(this.user); } + // direction 传入 move(移动) 方向参数 + private void move(int direction){ + // 判断角色:如果是 A 角色,则将获取到的方向设置为 A 的 nextStep 方向 + // user.getId() 是获取当前链接的用户 id + if(game.getPlayerA().getId().equals(user.getId())){ + game.setNextStepA(direction); + } + else if(game.getPlayerB().getId().equals(user.getId())){ + game.setNextStepB(direction); + } + } + // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 @OnMessage public void onMessage(String message, Session session) { @@ -148,6 +171,8 @@ public class WebSocketServer { startMatching(); } else if ("stop-matching".equals(event)) { stopMatching(); + } else if ("move".equals(event)) { + move(data.getInteger("direction")); } } diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java b/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java new file mode 100644 index 0000000..f74e2a3 --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java @@ -0,0 +1,12 @@ +package com.kob.backend.consumer.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Cell { + int x, y; +} diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Game.java b/backend/src/main/java/com/kob/backend/consumer/utils/Game.java index 071e3ca..5146692 100644 --- a/backend/src/main/java/com/kob/backend/consumer/utils/Game.java +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Game.java @@ -1,9 +1,17 @@ package com.kob.backend.consumer.utils; -import java.util.Random; +import com.alibaba.fastjson2.JSONObject; +import com.kob.backend.consumer.WebSocketServer; -// 用来管理整个游戏流程 -public class Game { +import javax.swing.event.InternalFrameEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.locks.ReentrantLock; + +// 用来管理整个游戏流程:这里需要多线程 +public class Game extends Thread { // 游戏地图:行数,列数,内部障碍物数量 private final Integer rows; private final Integer cols; @@ -12,25 +20,68 @@ public class Game { private final int[][] g; // 定义"上右下左"四个方向的 dx, dy偏移量 private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; + private final Player playerA, playerB; + // 玩家下一步操作状态定义 + private Integer nextStepA = null; + private Integer nextStepB = null; + // 加锁:解决读写冲突用 + private ReentrantLock lock = new ReentrantLock(); + // 整个游戏的当前状态: playing(正在进行) --> finished(游戏结束) + private String status = "playing"; + // 定义失败者: all(平局), A(A输), B(B输) + private String loser = ""; - // 初始化构造函数 - public Game(Integer rows, Integer cols, Integer inner_walls_count) { + + // 初始化(有参)构造函数 + 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 int[rows][cols]; + playerA = new Player(idA, rows - 2, 1, new ArrayList<>()); + playerB = new Player(idB, 1, cols - 2, new ArrayList<>()); } - public int getRows(){ + public Player getPlayerA() { + return playerA; + } + + public Player getPlayerB() { + return playerB; + } + + public int getRows() { return rows; } - public int getCols(){ + + public int getCols() { return cols; } - public int getInnerWallsCount(){ + + public int getInnerWallsCount() { return inner_walls_count; } + public void setNextStepA(Integer nextStepA) { + // 为了防止两方读写冲突,需要加线程锁之后操作 nextStep 值 + lock.lock(); + try { + this.nextStepA = nextStepA; + } finally { + // 操作完成后解锁 + lock.unlock(); + } + } + + public void setNextStepB(Integer nextStepB) { + lock.lock(); + try { + this.nextStepB = nextStepB; + } finally { + lock.unlock(); + } + } + // 返回生成的地图 public int[][] getG() { return g; @@ -117,4 +168,162 @@ public class Game { break; } } + + // 获取两名玩家的下一步操作 + private boolean nextStep() { + try { + // 返回下一步操作之前,先睡眠 200 毫秒,用于等待前端渲染完成,避免渲染速度跟不上后端运算处理速度 + // 前端定义了每 1 秒钟 5 个格子,则每 200 毫秒走一格(每一步至少需要 200 毫秒的时间) + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // 循环 50 秒钟,每次 100 毫秒,判断用户输入(50 * 100 = 5000 ms后如果没有接收到输入,则判断输赢) + for (int i = 0; i < 50; i++) { + try { + // 两次接收输入的等待间隔 + Thread.sleep(100); + lock.lock(); + try { + // 判断两名玩家是否都已经读取到输入:如果读入都非空,则表示读取结束,返回 true + if (nextStepA != null && nextStepB != null) { + // 将下一步操作存下来 + playerA.getSteps().add(nextStepA); + playerB.getSteps().add(nextStepB); + + // 成功获取到两名玩家的下一步操作,返回 true + return true; + } + } finally { + lock.unlock(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + return false; + } + + // 碰撞合法性检测辅助函数:需要判断 snakeA 和 snakeB 是否重合 + private boolean checkValid(List cellsA, List cellsB) { + int n = cellsA.size(); // cellsA 的当前长度 + Cell cell = cellsA.get(n - 1); // 取出 cellsA 的蛇头位置(数组最后一位)(第0个元素是蛇尾巴) + if (g[cell.x][cell.y] == 1) return false; // 判断 A 的最后一位(蛇头)的坐标是否是障碍物(值为1),如果是障碍物则返回false(判断已碰撞) + + // 判断 A蛇头位置是否和A除了蛇头以外的其他身体部分坐标有重合 + for (int i = 0; i < n - 1; i++) { + if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) + return false; + } + for (Cell cellB : cellsB) { // 判断 A蛇头位置是否和B蛇身体各部分坐标有重合:有重合返回不合法(表示已经碰撞) + if (cellB.x == cell.x && cellB.y == cell.y) + return false; + } + + // 以上判断条件如果都合法,则返回 true合法(表示没有碰撞) + return true; + } + + // 判断两名玩家下一步操作是否合法 + private void judge() { + // 取出 A,B 两蛇 + List cellsA = playerA.getCells(); + List cellsB = playerB.getCells(); + + boolean validA = checkValid(cellsA, cellsB); + boolean validB = checkValid(cellsB, cellsA); + + // 如果 A 和 B 有人这一步不合法(已经产生碰撞),将游戏状态改为 "finished",表示游戏结束 + if (!validA || !validB) { + status = "finished"; + + // 游戏结束时,判断输赢 + if (!validA && !validB) { + loser = "all"; + } else if (!validA && validB) { + loser = "A"; + } else if (validA && !validB) { + loser = "B"; + } + } + } + + // 向两个 Client 广播信息 + private void sendAllMessage(String message) { + WebSocketServer.users.get(playerA.getId()).sendMessage(message); + WebSocketServer.users.get(playerB.getId()).sendMessage(message); + } + + // 向两个 Client 传递移动信息 + private void sendMove() { + //由于这里需要读入 nextStepA, nextStepB, 这里需要加一下线程锁 + lock.lock(); + try { + JSONObject resp = new JSONObject(); + resp.put("event", "move"); // event(事件类型): move(移动信息) + resp.put("a_direction", nextStepA); // a_direction( playerA 移动的方向) + resp.put("b_direction", nextStepB); // b_direction( playerB 移动的方向) + sendAllMessage(resp.toJSONString()); + // 本次获取完下一步操作的同时需要进行再下一步操作输入,在这之前,需要将当前的 nextStep 清空 + nextStepA = nextStepB = null; + } finally { + lock.unlock(); + } + } + + // 向两个 Client 公布结果 + private void sendResult() { + lock.lock(); + try { + JSONObject resp = new JSONObject(); + resp.put("event", "result"); // event(事件类型): result(结果) + resp.put("loser", loser); // 失败者 + // 将最后碰撞时的蛇的眼睛指向传给前端 + resp.put("a_eyes_finally_direction", nextStepA); + resp.put("b_eyes_finally_direction", nextStepB); + sendAllMessage(resp.toJSONString()); + } finally { + lock.unlock(); + } + } + + // 重写线程函数 + @Override + public void run() { + // 循环 1000 步: 13格x14格x(最大)3步/格 < 1000,保证 1000 次一定可以走完 + for (int i = 0; i < 1000; i++) { + if (nextStep()) { // 是否获取到两条蛇的下一步操作 + // 判断下一步操作是否合法 + judge(); + + // 如果游戏状态还是 playing,则向两个玩家广播移动信息 + if ("playing".equals(status)) { + sendMove(); + } else if ("finished".equals(status)) { + // 如果游戏状态已经更改为了 finished,则将结果返回给 Client 并 break 中断执行后续操作 + sendResult(); + break; + } + + } else { + // 未获取到某蛇的下一步操作,则将游戏状态改为 finished + status = "finished"; + // 判断失败者:这里需要加锁,因为涉及到对 nextStep 的读操作 + lock.lock(); + try { + if (nextStepA == null && nextStepB == null) { + loser = "all"; + } else if (nextStepA == null) { + loser = "A"; + } else { + loser = "B"; + } + } finally { + lock.unlock(); + } + } + } + } } diff --git a/backend/src/main/java/com/kob/backend/consumer/utils/Player.java b/backend/src/main/java/com/kob/backend/consumer/utils/Player.java new file mode 100644 index 0000000..ac2186c --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Player.java @@ -0,0 +1,51 @@ +package com.kob.backend.consumer.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/// 玩家信息 +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Player { + private Integer id; + private Integer sx; + private Integer sy; + // 玩家走过的路径中每一步的方向 + private List steps; + + // 检验当前回合蛇的长度(蛇尾)是否需要增加 + public boolean checkTailIncreasing(int step) { + // 前 10 回合每次都增加一节蛇尾,后面每 3 回合增加一节蛇尾 + if (step <= 10) return true; + if (step % 3 == 1) return true; + + return false; + } + + // 蛇身 Cell 列表:记录蛇身各部分坐标 + public List getCells() { + List res = new ArrayList<>(); + + // 四方向偏移量 + int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; + int x = sx, y = sy; // 起始坐标 + int step = 0; // 定义当前回合数: 开始时为 0 回合 + res.add(new Cell(x, y)); // 现将蛇头添加进 List + for (int d : steps) { // 使用下一步方向求出下一个结点的坐标 + x += dx[d]; + y += dy[d]; + res.add(new Cell(x, y)); // 添加新的身体结点 + step++; // 回合数 +1 + // 判断蛇尾要不要增加:如果本回合蛇尾不增加,则将本回合新生成的蛇尾(列表的第0个元素)删掉 + if (!checkTailIncreasing(step)) { + res.remove(0); + } + } + return res; + } +} diff --git a/web/src/assets/scripts/GameMap.js b/web/src/assets/scripts/GameMap.js index 055b47f..b2eacaf 100644 --- a/web/src/assets/scripts/GameMap.js +++ b/web/src/assets/scripts/GameMap.js @@ -33,7 +33,7 @@ export class GameMap extends AcGameObject { // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制 this.walls = []; - // 创建蛇对象数组 + // 创建蛇对象数组(可以通过 store.state.pk.game_object.snakes 全局变量调取) this.snakes = [ // 注意这里的对象生成方式和传参方式 new Snake({ id: 0, color: "#4876ec", r: this.rows - 2, c: 1 }, this), @@ -167,20 +167,35 @@ export class GameMap extends AcGameObject { this.ctx.canvas.focus(); // 先取出两条蛇对象 - const [snake0, snake1] = this.snakes; + // const [snake0, snake1] = this.snakes; // 获取用户信息:绑定 keydown 事件 this.ctx.canvas.addEventListener("keydown", (e) => { + // 取出移动的方向,需要将方向传给后端 + let d = -1; // -1 表示没有方向, 0-上, 1-右, 2-下, 3-左 + // 定义 snake0 的键盘绑定事件 - 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); - // 定义 snake1 的键盘绑定事件 + 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){ + // 使用 socket.send() 传递信息给后端,使用 JSON.stringify() 将一个 JSON 封装成一字符串 + this.store.state.pk.socket.send(JSON.stringify({ + event: "move", // 事件类型: move + direction : d, // 方向: d + })) + } + + /* + // 定义 snake1 的键盘绑定事件(前端调试时启用) 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); + */ }); } diff --git a/web/src/assets/scripts/Snake.js b/web/src/assets/scripts/Snake.js index adf7021..64bd1cd 100644 --- a/web/src/assets/scripts/Snake.js +++ b/web/src/assets/scripts/Snake.js @@ -104,11 +104,12 @@ export class Snake extends AcGameObject { // 这里需要使用 JSON 方法进行深度复制,以产生新的对象避免数据出错 this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); } - + /* //(改为后端实现) // 如果下一步操作的目标位置碰撞检测不合法,则蛇直接去世 if (!this.gamemap.check_valid(this.next_cell)) { this.status = "die"; - } + } + */ } update_move() { diff --git a/web/src/components/GameMap.vue b/web/src/components/GameMap.vue index 7a61ef5..8161de0 100644 --- a/web/src/components/GameMap.vue +++ b/web/src/components/GameMap.vue @@ -24,8 +24,10 @@ export default { // 定义挂载函数,挂载完成后执行 onMounted(() => { // new GameMap(canvas.value.getContext("2d"), parent.value); - // 改为后端(服务器)获取生成地图 - new GameMap(canvas.value.getContext("2d"), parent.value, store); + // 改为后端(服务器)获取生成地图:并将游戏信息存入 store + store.commit("updateGameObject", + new GameMap(canvas.value.getContext("2d"), parent.value, store), + ); }); return { diff --git a/web/src/components/ResultBoard.vue b/web/src/components/ResultBoard.vue new file mode 100644 index 0000000..d134817 --- /dev/null +++ b/web/src/components/ResultBoard.vue @@ -0,0 +1,78 @@ +// 计分板 + + + + + \ No newline at end of file diff --git a/web/src/store/pk.js b/web/src/store/pk.js index b6e94e1..f9bbf2f 100644 --- a/web/src/store/pk.js +++ b/web/src/store/pk.js @@ -7,10 +7,20 @@ export default { // 对手信息 opponent_username: "", opponent_photo: "", + // 地图信息(后端传入) game_map: null, rows: 0, cols: 0, inner_walls_count: 0, + a_id: 0, + a_sx: 0, + a_sy: 0, + b_id: 0, + b_sx: 0, + b_sy: 0, + // 游戏全局 + game_object: null, + loser:"none", // all, A, B 三种结果 }, getters: {}, mutations: { @@ -33,7 +43,20 @@ export default { state.rows = game_data.rows; state.cols = game_data.cols; state.inner_walls_count = game_data.inner_walls_count; + state.a_id = game_data.a_id; + state.a_sx = game_data.a_sx; + state.a_sy = game_data.a_sy; + state.b_id = game_data.b_id; + state.b_sx = game_data.b_sx; + state.b_sy = game_data.b_sy; }, + updateGameObject(state, game_object){ + // 在 GameMap.vue 中使用 + state.game_object = game_object; + }, + updateLoser(state,loser){ + state.loser = loser; + } }, actions: {}, module: {}, diff --git a/web/src/views/pk/PkIndexView.vue b/web/src/views/pk/PkIndexView.vue index f93fd19..487f954 100644 --- a/web/src/views/pk/PkIndexView.vue +++ b/web/src/views/pk/PkIndexView.vue @@ -1,11 +1,14 @@