From e6c49ad600d31f7999c0c2b583dabb3c10ef5a35 Mon Sep 17 00:00:00 2001 From: flykhan Date: Mon, 27 Feb 2023 23:01:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E6=94=B9=E4=B8=BA=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E7=94=9F=E6=88=90+=E5=89=8D=E7=AB=AF=E7=BB=98?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kob/backend/consumer/WebSocketServer.java | 70 +++++------ .../com/kob/backend/consumer/utils/Game.java | 110 ++++++++++++++++++ web/src/assets/scripts/GameMap.js | 48 ++++++-- web/src/components/GameMap.vue | 6 +- web/src/store/pk.js | 5 + web/src/views/pk/PkIndexView.vue | 3 + 6 files changed, 198 insertions(+), 44 deletions(-) create mode 100644 backend/src/main/java/com/kob/backend/consumer/utils/Game.java 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 0e1566f..3f2cfe9 100644 --- a/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java +++ b/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java @@ -3,6 +3,7 @@ package com.kob.backend.consumer; // WebSocket用于前后端通信 import com.alibaba.fastjson2.JSONObject; +import com.kob.backend.consumer.utils.Game; import com.kob.backend.consumer.utils.JwtAuthenticationUtil; import com.kob.backend.mapper.UserMapper; import com.kob.backend.pojo.User; @@ -20,12 +21,10 @@ import java.util.concurrent.CopyOnWriteArraySet; @Component @ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾 public class WebSocketServer { - // 后端向前端发送信息,首先需要创建一个 session + // 后端向前端发送信息,首先需要创建一个 session private Session session = null; - - // 用户信息:定义一个成员变量 + // 用户信息:定义一个成员变量 private User user; - /* 存储所有链接:对所有 websocket 可见的全局变量,存储为 static 静态变量 因为每个 websocket 实例都在一个独立的线程里,所以该公共变量应该是线程安全的 @@ -33,43 +32,40 @@ public class WebSocketServer { 将 userId 映射到 webSocketServer */ private static final ConcurrentHashMap users = new ConcurrentHashMap<>(); - -// 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool + // 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool private static final CopyOnWriteArraySet matchPool = new CopyOnWriteArraySet<>(); - - - //在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 + // 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 private static UserMapper userMapper; - // 注入方法 + // 注入方法 @Autowired public void setUserMapper(UserMapper userMapper) { -// 静态变量 userMapper 访问需要使用类名 WebSocketServer 访问 + // 静态变量 userMapper 访问需要使用类名 WebSocketServer 访问 WebSocketServer.userMapper = userMapper; } - // @OnOpen 创建链接时自动触发这个函数 + // @OnOpen 创建链接时自动触发这个函数 @OnOpen public void onOpen(Session session, @PathParam("token") String token) throws IOException { // 建立链接时需要将 session 存下来 this.session = session; -// 成功建立连接时,输出 connected! + // 成功建立连接时,输出 connected! System.out.println("backend connected!"); -// 将 userId 取出来,通过 userId 将 user 找出来 -// int userId = Integer.parseInt(token); + // 将 userId 取出来,通过 userId 将 user 找出来 + // int userId = Integer.parseInt(token); int userId = JwtAuthenticationUtil.getUserId(token); this.user = userMapper.selectById(userId); -// 如果 user 存在, 表示用户登录成功, 用户信息是存在的 + // 如果 user 存在, 表示用户登录成功, 用户信息是存在的 if (this.user != null) { // 将 user 存到 users HashMap里 users.put(userId, this); // (测试)后台输出看用户信息 -// System.out.println(user); + // System.out.println(user); } -// 否则,断开连接(这里需要抛出异常) + // 否则,断开连接(这里需要抛出异常) else { this.session.close(); } @@ -81,25 +77,25 @@ public class WebSocketServer { public void onClose() { // 关闭链接 System.out.println("backend disconnected!"); -// 断开连接时,需要将 user 从 users 里面删掉 + // 断开连接时,需要将 user 从 users 里面删掉 if (this.user != null) { users.remove(this.user.getId()); -// 将匹配池数组删掉 + // 将匹配池数组删掉 matchPool.remove(this.user); } } - // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 + // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 @OnMessage public void onMessage(String message, Session session) { // 从 Client 接收消息 System.out.println("receive message"); -// 解析从前端接收到的请求 + // 解析从前端接收到的请求 JSONObject data = JSONObject.parseObject(message); String event = data.getString("event"); -// System.out.println(event); + // System.out.println(event); -// 匹配状态函数调用 + // 匹配状态函数调用 if ("start-matching".equals(event)) { startMatching(); } else if ("stop-matching".equals(event)) { @@ -112,12 +108,12 @@ public class WebSocketServer { error.printStackTrace(); } - // 从后端向前端发送信息 + // 从后端向前端发送信息 public void sendMessage(String message) { -// 异步通信过程,先加一个锁 + // 异步通信过程,先加一个锁 synchronized (this.session) { try { -// 将 message 发送到前端 + // 将 message 发送到前端 this.session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); @@ -125,37 +121,43 @@ public class WebSocketServer { } } - // 开始匹配的逻辑部分 + // 开始匹配的逻辑部分 private void startMatching() { System.out.println("start matching"); matchPool.add(this.user); while (matchPool.size()>=2){ -// 迭代器用于枚举前两个人进行匹配 + // 迭代器用于枚举前两个人进行匹配 Iterator it = matchPool.iterator(); User a = it.next(), b = it.next(); -// 取出两个人之后,从匹配池中将他们删除 + // 取出两个人之后,从匹配池中将他们删除 matchPool.remove(a); matchPool.remove(b); -// 将 a 配对成功的消息传回客户端 + // 匹配成功时,创建联机地图 + Game game = new Game(13,14,20); + game.createMap(); // 初始化地图 + + // 将 a 配对成功的消息传回客户端 JSONObject respA = new JSONObject(); respA.put("event","start-matching"); respA.put("opponent_username",b.getUsername()); respA.put("opponent_photo",b.getPhoto()); -// 获取 a 的链接,并通过 sendMessage 将消息传给前端 + respA.put("game_map",game.getG()); + // 获取 a 的链接,并通过 sendMessage 将消息传给前端 users.get(a.getId()).sendMessage(respA.toJSONString()); -// 同理,将 b 的匹配成功信息传回给前端 + // 同理,将 b 的匹配成功信息传回给前端 JSONObject respB = new JSONObject(); respB.put("event","start-matching"); respB.put("opponent_username",a.getUsername()); respB.put("opponent_photo",a.getPhoto()); + respB.put("game_map",game.getG()); users.get(b.getId()).sendMessage(respB.toJSONString()); } } - // 取消匹配的逻辑部分 + // 取消匹配的逻辑部分 private void stopMatching() { System.out.println("stop matching"); matchPool.remove(this.user); 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 new file mode 100644 index 0000000..7849eac --- /dev/null +++ b/backend/src/main/java/com/kob/backend/consumer/utils/Game.java @@ -0,0 +1,110 @@ +package com.kob.backend.consumer.utils; + +import java.util.Random; + +// 用来管理整个游戏流程 +public class Game { + // 游戏地图:行数,列数,内部障碍物数量 + private final Integer rows; + private final Integer cols; + private final Integer inner_walls_count; + // 地图数组 + private final int[][] g; + // 定义"上右下左"四个方向的 dx, dy偏移量 + private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; + + // 初始化构造函数 + public Game(Integer rows, Integer cols, Integer inner_walls_count) { + this.rows = rows; + this.cols = cols; + this.inner_walls_count = rows; + this.g = new int[rows][cols]; + } + + // 返回生成的地图 + public int[][] getG() { + return g; + } + + // 画地图 + public boolean draw() { + // 一开始现将所有障碍物初始化为 false + for (int r = 0; r < this.rows; r++) { + for (int c = 0; c < this.cols; c++) { + // 0 表示空地, 1 表示障碍物(墙) + g[r][c] = 0; + } + } + + // 地图左右边缘障碍物 + for (int r = 0; r < this.rows; r++) { + g[r][0] = g[r][this.cols - 1] = 1; + } + // 地图上下边缘障碍物 + for (int c = 0; c < this.cols; c++) { + g[0][c] = g[this.rows - 1][c] = 1; + } + + Random random = new Random(); + // 创建内部随机障碍物,每次计算时会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2 + for (int i = 0; i < this.inner_walls_count / 2; i++) { + // 避免位置重复:重复遍历 1000 次,只要找到了已经存在障碍物的位置就禁止随机 + for (int j = 0; j < 1000; j++) { + // 找出本次随机到的行-r 列-c 坐标 + int r = random.nextInt(this.rows); // random.nextInt(7) 返回 0-7之间的随机整数 + int c = random.nextInt(this.cols); + + // 中心对称:当本坐标或者它的中心对称坐标已经存在障碍物了,则重新计算下一个坐标 + if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1) + continue; + + // 避免内部障碍物覆盖掉左下角和右上角的 A-B 角色出发点,如果是这两个位置,则重新计算下一个坐标 + if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) + continue; + + // 将计算求得的随机障碍物合法的位置赋值为 1 ,以对该位置进行绘制(包括本坐标及其中心对称坐标) + g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = 1; + + // 1000 次遍历中,规定数量的内部障碍物已经够数之后就 break 掉 + break; + } + } + + // 确保 A-B 角色的运动区域是联通的:如果不连通,则直接在创建地图对象之前取消绘制( return false ) + return check_connectivity(this.rows - 2, 1, 1, this.cols - 2); + } + + // 联通检测方法---true(联通)---false(不通),参数: 起点坐标 sx,sy ,终点坐标 tx,ty + private boolean check_connectivity(int sx, int sy, int tx, int ty) { + // 起点就是终点时,结果联通,直接返回 true + if (sx == tx && sy == ty) return true; + g[sx][sy] = 1; + + //枚举"上右下左"四个方向,求当前点下一个相邻点的坐标 + for (int i = 0; i < 4; i++) { + int x = sx + dx[i]; + int y = sy + dy[i]; + + // 判断是否撞到障碍物( g[x][y] == 0 表示没有碰到障碍物 ),如果没有赚到障碍物,且可以找到重点的话,返回 true(联通) + if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0) { + if (check_connectivity(x, y, tx, ty)) { + // 还原状态 + g[sx][sy] = 0; + return true; + } + } + } + + // 还原状态 + g[sx][sy] = 0; + return true; + } + + public void createMap() { + // 循环绘制:如果发现哪次循环中画地图成功了,则跳出循环,绘制结束 + for (int i = 0; i < 1000; i++) { + if (draw()) + break; + } + } +} diff --git a/web/src/assets/scripts/GameMap.js b/web/src/assets/scripts/GameMap.js index 5392d7a..35c23b9 100644 --- a/web/src/assets/scripts/GameMap.js +++ b/web/src/assets/scripts/GameMap.js @@ -7,24 +7,26 @@ import { Wall } from "./Wall"; // 导出定义的 GameMap 游戏地图类 export class GameMap extends AcGameObject { // 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽 - constructor(ctx, parent) { + constructor(ctx, parent, store) { // super() 用于先执行基类的构造函数 super(); // 存下 ctx 和 parent this.ctx = ctx; this.parent = parent; + this.store = store; // 存下每个格子的绝对距离 this.L = 0; - // 定义棋盘格的行数和列数 +/* + // 定义棋盘格的行数和列数(前端生成地图时使用) // 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平 this.rows = 13; this.cols = 14; - - // 绘制棋盘内部区域的障碍物(墙)的数量 - this.inner_walls_count = 20; + // 绘制棋盘内部区域的障碍物(墙)的数量(前端生成地图时使用) + this.inner_walls_count = 30; + */ // 存储所有的墙 // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制 @@ -38,7 +40,8 @@ export class GameMap extends AcGameObject { ]; } - // 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标 +/* + // 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标(前端生成地图时使用) check_connectivity(g, sx, sy, tx, ty) { // 当起点坐标和中点坐标一致时,判断联通,直接返回 if (sx == tx && sy == ty) return true; @@ -58,8 +61,31 @@ export class GameMap extends AcGameObject { // 搜不到终点,返回 false return false; } + */ - // 创建墙函数 + // 创建障碍物(后端生成地图版本) + create_wall() { + // 取出后端生成后传到前端 store 中的 game_map + const g = this.store.state.pk.game_map; + + + // 枚举数组,将 g[r][c] == true 的部分绘制出来 + // 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作 + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + if (g[r][c]) { + // 将每个新生成的 Wall 对象 push 存入 walls 数组中 + this.walls.push(new Wall(r, c, this)); + } + } + } + + // 绘制成功则 return turn + return true; + } + + /* + // 创建障碍物(前端计算地图版本) create_wall() { // 创建一个墙格进行测试 // new Wall(0,0,this); @@ -134,6 +160,7 @@ export class GameMap extends AcGameObject { // 绘制成功则 return turn return true; } + */ // 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇 add_listening_events() { @@ -160,9 +187,12 @@ export class GameMap extends AcGameObject { start() { // 开始时调用一次创建墙的函数 - // 循环 1000 次,如果成功创建则 break ,否则继续循环创建 - for (let i = 0; i < 1000; i++) if (this.create_wall()) break; + // 循环 1000 次,如果成功创建则 break ,否则继续循环创建(前端生成地图时使用这一条) + // for (let i = 0; i < 1000; i++) if (this.create_wall()) break; + // 使用后端生成地图时,这里只需要调用一次就好 + this.create_wall(); + // 开始时启动监听方法 this.add_listening_events(); } diff --git a/web/src/components/GameMap.vue b/web/src/components/GameMap.vue index dbc04c1..7a61ef5 100644 --- a/web/src/components/GameMap.vue +++ b/web/src/components/GameMap.vue @@ -12,16 +12,20 @@ import { GameMap } from "@/assets/scripts/GameMap"; // 引入 canvas, onMounted 用于挂载操作定义 import { ref, onMounted } from "vue"; +import { useStore } from "vuex"; export default { setup() { + const store = useStore(); // 定义两个变量 parent 和 canvas, 一开始这两个变量都没有指向任何元素,因此 ref(null) let parent = ref(null); let canvas = ref(null); // 定义挂载函数,挂载完成后执行 onMounted(() => { - new GameMap(canvas.value.getContext("2d"), parent.value); + // new GameMap(canvas.value.getContext("2d"), parent.value); + // 改为后端(服务器)获取生成地图 + new GameMap(canvas.value.getContext("2d"), parent.value, store); }); return { diff --git a/web/src/store/pk.js b/web/src/store/pk.js index 1d1c980..82010b9 100644 --- a/web/src/store/pk.js +++ b/web/src/store/pk.js @@ -7,6 +7,7 @@ export default { // 对手信息 opponent_username: "", opponent_photo: "", + game_map: null, }, getters: {}, mutations: { @@ -23,6 +24,10 @@ export default { updateStatus(state, status) { state.status = status; }, + // 更新地图 + updateGameMap(state,game_map){ + state.game_map = game_map; + } }, actions: {}, module: {}, diff --git a/web/src/views/pk/PkIndexView.vue b/web/src/views/pk/PkIndexView.vue index dfd54be..e9fbd8c 100644 --- a/web/src/views/pk/PkIndexView.vue +++ b/web/src/views/pk/PkIndexView.vue @@ -42,6 +42,7 @@ export default { store.commit("updateSocket", socket); } + // 从后端传过来的数据信息 socket.onmessage = (msg) => { const data = JSON.parse(msg.data); // console.log(data); @@ -56,6 +57,8 @@ export default { setTimeout(() => { store.commit("updateStatus", "playing"); }, 2000); + // 匹配成功,从后端更新地图 + store.commit("updateGameMap",data.game_map) } }