地图改为后端生成+前端绘制
This commit is contained in:
		
							parent
							
								
									e6e6bc15e9
								
							
						
					
					
						commit
						e6c49ad600
					
				| @ -3,6 +3,7 @@ package com.kob.backend.consumer; | |||||||
| // WebSocket用于前后端通信 | // WebSocket用于前后端通信 | ||||||
| 
 | 
 | ||||||
| import com.alibaba.fastjson2.JSONObject; | import com.alibaba.fastjson2.JSONObject; | ||||||
|  | import com.kob.backend.consumer.utils.Game; | ||||||
| import com.kob.backend.consumer.utils.JwtAuthenticationUtil; | import com.kob.backend.consumer.utils.JwtAuthenticationUtil; | ||||||
| import com.kob.backend.mapper.UserMapper; | import com.kob.backend.mapper.UserMapper; | ||||||
| import com.kob.backend.pojo.User; | import com.kob.backend.pojo.User; | ||||||
| @ -22,10 +23,8 @@ import java.util.concurrent.CopyOnWriteArraySet; | |||||||
| public class WebSocketServer { | public class WebSocketServer { | ||||||
|     // 后端向前端发送信息,首先需要创建一个 session |     // 后端向前端发送信息,首先需要创建一个 session | ||||||
|     private Session session = null; |     private Session session = null; | ||||||
| 
 |  | ||||||
|     // 用户信息:定义一个成员变量 |     // 用户信息:定义一个成员变量 | ||||||
|     private User user; |     private User user; | ||||||
| 
 |  | ||||||
|     /* |     /* | ||||||
|         存储所有链接:对所有 websocket 可见的全局变量,存储为 static 静态变量 |         存储所有链接:对所有 websocket 可见的全局变量,存储为 static 静态变量 | ||||||
|         因为每个 websocket 实例都在一个独立的线程里,所以该公共变量应该是线程安全的 |         因为每个 websocket 实例都在一个独立的线程里,所以该公共变量应该是线程安全的 | ||||||
| @ -33,11 +32,8 @@ public class WebSocketServer { | |||||||
|         将 userId 映射到 webSocketServer |         将 userId 映射到 webSocketServer | ||||||
|     */ |     */ | ||||||
|     private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>(); |     private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>(); | ||||||
| 
 |  | ||||||
|     // 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool |     // 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool | ||||||
|     private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>(); |     private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>(); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     // 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 |     // 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 | ||||||
|     private static UserMapper userMapper; |     private static UserMapper userMapper; | ||||||
| 
 | 
 | ||||||
| @ -138,11 +134,16 @@ public class WebSocketServer { | |||||||
|             matchPool.remove(a); |             matchPool.remove(a); | ||||||
|             matchPool.remove(b); |             matchPool.remove(b); | ||||||
| 
 | 
 | ||||||
|  |             // 匹配成功时,创建联机地图 | ||||||
|  |             Game game = new Game(13,14,20); | ||||||
|  |             game.createMap();   // 初始化地图 | ||||||
|  | 
 | ||||||
|             // 将 a 配对成功的消息传回客户端 |             // 将 a 配对成功的消息传回客户端 | ||||||
|             JSONObject respA = new JSONObject(); |             JSONObject respA = new JSONObject(); | ||||||
|             respA.put("event","start-matching"); |             respA.put("event","start-matching"); | ||||||
|             respA.put("opponent_username",b.getUsername()); |             respA.put("opponent_username",b.getUsername()); | ||||||
|             respA.put("opponent_photo",b.getPhoto()); |             respA.put("opponent_photo",b.getPhoto()); | ||||||
|  |             respA.put("game_map",game.getG()); | ||||||
|             // 获取 a 的链接,并通过 sendMessage 将消息传给前端 |             // 获取 a 的链接,并通过 sendMessage 将消息传给前端 | ||||||
|             users.get(a.getId()).sendMessage(respA.toJSONString()); |             users.get(a.getId()).sendMessage(respA.toJSONString()); | ||||||
| 
 | 
 | ||||||
| @ -151,6 +152,7 @@ public class WebSocketServer { | |||||||
|             respB.put("event","start-matching"); |             respB.put("event","start-matching"); | ||||||
|             respB.put("opponent_username",a.getUsername()); |             respB.put("opponent_username",a.getUsername()); | ||||||
|             respB.put("opponent_photo",a.getPhoto()); |             respB.put("opponent_photo",a.getPhoto()); | ||||||
|  |             respB.put("game_map",game.getG()); | ||||||
|             users.get(b.getId()).sendMessage(respB.toJSONString()); |             users.get(b.getId()).sendMessage(respB.toJSONString()); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										110
									
								
								backend/src/main/java/com/kob/backend/consumer/utils/Game.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								backend/src/main/java/com/kob/backend/consumer/utils/Game.java
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -7,24 +7,26 @@ import { Wall } from "./Wall"; | |||||||
| // 导出定义的 GameMap 游戏地图类
 | // 导出定义的 GameMap 游戏地图类
 | ||||||
| export class GameMap extends AcGameObject { | export class GameMap extends AcGameObject { | ||||||
|   // 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
 |   // 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
 | ||||||
|   constructor(ctx, parent) { |   constructor(ctx, parent, store) { | ||||||
|     // super() 用于先执行基类的构造函数
 |     // super() 用于先执行基类的构造函数
 | ||||||
|     super(); |     super(); | ||||||
| 
 | 
 | ||||||
|     // 存下 ctx 和 parent
 |     // 存下 ctx 和 parent
 | ||||||
|     this.ctx = ctx; |     this.ctx = ctx; | ||||||
|     this.parent = parent; |     this.parent = parent; | ||||||
|  |     this.store = store; | ||||||
| 
 | 
 | ||||||
|     // 存下每个格子的绝对距离
 |     // 存下每个格子的绝对距离
 | ||||||
|     this.L = 0; |     this.L = 0; | ||||||
| 
 | 
 | ||||||
|     // 定义棋盘格的行数和列数
 | /*  | ||||||
|  |     // 定义棋盘格的行数和列数(前端生成地图时使用)
 | ||||||
|     // 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平
 |     // 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平
 | ||||||
|     this.rows = 13; |     this.rows = 13; | ||||||
|     this.cols = 14; |     this.cols = 14; | ||||||
| 
 |     // 绘制棋盘内部区域的障碍物(墙)的数量(前端生成地图时使用)
 | ||||||
|     // 绘制棋盘内部区域的障碍物(墙)的数量
 |     this.inner_walls_count = 30; | ||||||
|     this.inner_walls_count = 20; |  */ | ||||||
| 
 | 
 | ||||||
|     // 存储所有的墙
 |     // 存储所有的墙
 | ||||||
|     // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
 |     // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
 | ||||||
| @ -38,7 +40,8 @@ export class GameMap extends AcGameObject { | |||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标
 | /*  | ||||||
|  |   // 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标(前端生成地图时使用)
 | ||||||
|   check_connectivity(g, sx, sy, tx, ty) { |   check_connectivity(g, sx, sy, tx, ty) { | ||||||
|     // 当起点坐标和中点坐标一致时,判断联通,直接返回
 |     // 当起点坐标和中点坐标一致时,判断联通,直接返回
 | ||||||
|     if (sx == tx && sy == ty) return true; |     if (sx == tx && sy == ty) return true; | ||||||
| @ -58,8 +61,31 @@ export class GameMap extends AcGameObject { | |||||||
|     // 搜不到终点,返回 false
 |     // 搜不到终点,返回 false
 | ||||||
|     return 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() { |   create_wall() { | ||||||
|     // 创建一个墙格进行测试
 |     // 创建一个墙格进行测试
 | ||||||
|     // new Wall(0,0,this);
 |     // new Wall(0,0,this);
 | ||||||
| @ -134,6 +160,7 @@ export class GameMap extends AcGameObject { | |||||||
|     // 绘制成功则 return turn
 |     // 绘制成功则 return turn
 | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  */ | ||||||
| 
 | 
 | ||||||
|   // 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
 |   // 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
 | ||||||
|   add_listening_events() { |   add_listening_events() { | ||||||
| @ -160,8 +187,11 @@ export class GameMap extends AcGameObject { | |||||||
| 
 | 
 | ||||||
|   start() { |   start() { | ||||||
|     // 开始时调用一次创建墙的函数
 |     // 开始时调用一次创建墙的函数
 | ||||||
|     // 循环 1000 次,如果成功创建则 break ,否则继续循环创建
 |     // 循环 1000 次,如果成功创建则 break ,否则继续循环创建(前端生成地图时使用这一条)
 | ||||||
|     for (let i = 0; i < 1000; i++) if (this.create_wall()) break; |     // for (let i = 0; i < 1000; i++) if (this.create_wall()) break;
 | ||||||
|  | 
 | ||||||
|  |     // 使用后端生成地图时,这里只需要调用一次就好
 | ||||||
|  |     this.create_wall(); | ||||||
|      |      | ||||||
|     // 开始时启动监听方法
 |     // 开始时启动监听方法
 | ||||||
|     this.add_listening_events(); |     this.add_listening_events(); | ||||||
|  | |||||||
| @ -12,16 +12,20 @@ | |||||||
| import { GameMap } from "@/assets/scripts/GameMap"; | import { GameMap } from "@/assets/scripts/GameMap"; | ||||||
| // 引入 canvas, onMounted 用于挂载操作定义 | // 引入 canvas, onMounted 用于挂载操作定义 | ||||||
| import { ref, onMounted } from "vue"; | import { ref, onMounted } from "vue"; | ||||||
|  | import { useStore } from "vuex"; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   setup() { |   setup() { | ||||||
|  |     const store = useStore(); | ||||||
|     // 定义两个变量 parent 和 canvas, 一开始这两个变量都没有指向任何元素,因此 ref(null) |     // 定义两个变量 parent 和 canvas, 一开始这两个变量都没有指向任何元素,因此 ref(null) | ||||||
|     let parent = ref(null); |     let parent = ref(null); | ||||||
|     let canvas = ref(null); |     let canvas = ref(null); | ||||||
| 
 | 
 | ||||||
|     // 定义挂载函数,挂载完成后执行 |     // 定义挂载函数,挂载完成后执行 | ||||||
|     onMounted(() => { |     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 { |     return { | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ export default { | |||||||
|     // 对手信息
 |     // 对手信息
 | ||||||
|     opponent_username: "", |     opponent_username: "", | ||||||
|     opponent_photo: "", |     opponent_photo: "", | ||||||
|  |     game_map: null, | ||||||
|   }, |   }, | ||||||
|   getters: {}, |   getters: {}, | ||||||
|   mutations: { |   mutations: { | ||||||
| @ -23,6 +24,10 @@ export default { | |||||||
|     updateStatus(state, status) { |     updateStatus(state, status) { | ||||||
|       state.status = status; |       state.status = status; | ||||||
|     }, |     }, | ||||||
|  |     // 更新地图
 | ||||||
|  |     updateGameMap(state,game_map){ | ||||||
|  |       state.game_map = game_map; | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   actions: {}, |   actions: {}, | ||||||
|   module: {}, |   module: {}, | ||||||
|  | |||||||
| @ -42,6 +42,7 @@ export default { | |||||||
|         store.commit("updateSocket", socket); |         store.commit("updateSocket", socket); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // 从后端传过来的数据信息 | ||||||
|       socket.onmessage = (msg) => { |       socket.onmessage = (msg) => { | ||||||
|         const data = JSON.parse(msg.data); |         const data = JSON.parse(msg.data); | ||||||
|         // console.log(data); |         // console.log(data); | ||||||
| @ -56,6 +57,8 @@ export default { | |||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
|             store.commit("updateStatus", "playing"); |             store.commit("updateStatus", "playing"); | ||||||
|           }, 2000); |           }, 2000); | ||||||
|  |           // 匹配成功,从后端更新地图 | ||||||
|  |           store.commit("updateGameMap",data.game_map) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user