本节内容为实现前后端的 WebSocket 通信,并同步游戏地图的生成。
我们的游戏之后是两名玩家对战,因此需要实现联机功能,在这之前还需要实现一个匹配系统,能够匹配分数相近的玩家进行对战。
想要进行匹配就至少要有两个客户端,当两个客户端都向服务器发送匹配请求后并不会马上得到返回结果,一般会等待一段时间,这个时间是未知的,因此这个匹配是一个异步的过程,对于这种异步的过程或者是计算量比较大的过程我们都会用一个额外的服务来操作。
那么这个额外的用于匹配的服务可以称为 Matching System,这是另外一个程序(进程),当后端服务器接收到前端的请求后就会将请求发送给 Matching System,这个匹配系统维护了一堆用户的集合,它会不断地去匹配分数最接近的用户,当匹配成功一组用户后就会将结果返回给后端服务器,再由后端将匹配结果立即返回给对应的前端。这种服务就被称为微服务,可以用 Spring Cloud 实现。
用以前的 HTTP 请求很难达到这种效果,之前我们是在客户端向后端发送请求,且后端在短时间内就会返回结果,HTTP 请求只能满足这种一问一答式的服务。而我们现在需要实现的效果是客户端发送请求后不知道经过多长时间后端才会返回结果,对于这种情况需要使用 WebSocket 协议(WS),该协议不仅支持客户端向服务器发送请求,也支持服务器向客户端发送请求。
在前端向服务器发送请求后,服务器会维护好一个 WS 链接,这个链接其实就是一个 WebSocketServer
类的实例,所有和这个链接相关的信息都会存到这个类中。
1. 配置WebSocket
我们之前每次刷新网页就会随机生成游戏地图,该过程是在浏览器本地执行的,当我们要实现匹配功能时,地图就不能由两名玩家各自的客户端生成,否则就基本不可能完全一样了。
当匹配成功后应该由服务器端创建一个 Game 任务,将游戏放到该任务下执行,统一生成地图,且判断移动或者输赢等逻辑之后也应该移到后端来执行。
生成好地图后服务器就将地图传给两名玩家的前端,然后等待玩家的键盘输入或者是 Bot 代码的输入,Bot 代码的输入也属于一个微服务。
首先我们先在 pom.xml
文件中添加以下依赖:
spring-boot-starter-websocket
fastjson2
接着在 config
包下创建 WebSocketConfig
配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.kob.backend.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
|
然后我们创建一个 consumer
包,在其中创建 WebSocketServer
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package com.kob.backend.consumer;
import org.springframework.stereotype.Component;
import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint;
@Component @ServerEndpoint("/websocket/{token}") public class WebSocketServer { @OnOpen public void onOpen(Session session, @PathParam("token") String token) { }
@OnClose public void onClose() { }
@OnMessage public void onMessage(String message, Session session) { }
@OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } }
|
之前我们配置的 Spring Security 设置了屏蔽除了授权之外的其他所有链接,因此我们需要在 SecurityConfig
类中放行一下 WebSocket 的链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package com.kob.backend.config;
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/account/login/", "/user/account/register/").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/websocket/**"); } }
|
如果是使用新版的配置而不是使用 WebSecurityConfigurerAdapter
可以按以下方式配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.kob.backend.config;
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/account/login/", "/user/account/register/").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); }
@Bean public WebSecurityCustomizer webSecurityCustomizer(){ return (web) -> web.ignoring().antMatchers("/websocket/**"); } }
|
2. 前后端WebSocket通信
2.1 WS通信的建立
WebSocket 不属于单例模式(同一个时间每个类只能有一个实例,我们每建一个 WS 链接都会新创建一个实例),不是标准的 Spring 中的组件,因此在注入 Mapper
时不能用 @Autowired
直接注入,一般是将 @Autowired
写在一个 set()
方法上,Spring 会根据方法的参数类型从 IoC 容器中找到该类型的 Bean 对象注入到方法的行参中,并且自动反射调用该方法。
我们先假设前端传过来的是用户 ID 而不是 JWT 令牌:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.kob.backend.consumer;
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.concurrent.ConcurrentHashMap;
@Component @ServerEndpoint("/websocket/{token}") public class WebSocketServer { private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>(); private User user; private Session session = null;
private static UserMapper userMapper;
@Autowired public void setUserMapper(UserMapper userMapper) { WebSocketServer.userMapper = userMapper; }
@OnOpen public void onOpen(Session session, @PathParam("token") String token) { this.session = session; System.out.println("Connected!");
Integer userId = Integer.parseInt(token); this.user = userMapper.selectById(userId); users.put(userId, this); }
@OnClose public void onClose() { System.out.println("Disconnected!"); if (this.user != null) { users.remove(this.user.getId()); } }
@OnMessage public void onMessage(String message, Session session) { System.out.println("Receive message!"); }
@OnError public 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(); } } } }
|
然后我们先在前端的 PKIndexView
组件中调试,当组件被挂载完成后发出请求建立 WS 链接,当被卸载后关闭 WS 链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <PlayGround /> </template>
<script> import PlayGround from "@/components/PlayGround.vue"; import { onMounted, onUnmounted } from "vue"; import { useStore } from "vuex";
export default { components: { PlayGround, }, setup() { const store = useStore();
let socket = null; let socket_url = `ws://localhost:3000/websocket/${store.state.user.id}/`;
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); console.log(data); };
socket.onclose = () => { console.log("Disconnected!"); }; });
onUnmounted(() => { socket.close(); }); }, }; </script>
<style scoped></style>
|
现在我们在对战页面每次刷新后都可以在浏览器控制台或后端控制台中看到 WS 的输出信息。
接下来我们要将 WebSocket 存到前端的 store
中,在 store
目录下创建 pk.js
用来存储和对战页面相关的全局变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export default { state: { status: "matching", socket: null, opponent_username: "", opponent_photo: "", }, getters: {}, 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; }, }, actions: {}, modules: {}, };
|
同时要在 store/index.js
中引入进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { createStore } from "vuex"; import ModuleUser from "./user"; import ModulePk from "./pk";
export default createStore({ state: {}, getters: {}, mutations: {}, actions: {}, modules: { user: ModuleUser, pk: ModulePk, }, });
|
2.2 加入JWT验证
现在我们直接使用用户的 ID 建立 WS 链接,这是不安全的,因为前端可以自行修改这个 ID,因此就需要加入 JWT 验证。
WebSocket 中没有 Session 的概念,因此我们在验证的时候前端就不用将信息放到表头里了,直接放到链接中就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ...
<script> ...
export default { ...
setup() { ...
let socket_url = `ws://localhost:3000/websocket/${store.state.user.jwt_token}/`;
... }, }; </script>
...
|
验证的逻辑可以参考之前的 JwtAuthenticationTokenFilter
,我们可以把这个验证的模块单独写到一个文件中,在 consumer
包下创建 utils
包,然后创建一个 JwtAuthentication
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.kob.backend.consumer.utils;
import com.kob.backend.utils.JwtUtil; import io.jsonwebtoken.Claims;
public class JwtAuthentication { public static Integer getUserId(String token) { int userId = -1; try { Claims claims = JwtUtil.parseJWT(token); userId = Integer.parseInt(claims.getSubject()); } catch (Exception e) { throw new RuntimeException(e); } return userId; } }
|
然后就可以在 WebSocketServer
中解析 JWT 令牌:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.kob.backend.consumer;
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.concurrent.ConcurrentHashMap;
@Component @ServerEndpoint("/websocket/{token}") public class WebSocketServer { ...
@OnOpen public 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(); } }
... }
|
3. 前后端匹配业务
3.1 实现前端页面
我们需要实现一个前端的匹配页面,并能够切换匹配和对战页面,可以根据之前在 store
中存储的 status
状态来动态展示页面。首先在 components
目录下创建 MatchGround.vue
组件,其中需要展示玩家自己的头像和用户名以及对手的头像和用户名,当点击开始匹配按钮时向 WS 链接发送开始匹配的消息,点击取消按钮时发送取消匹配的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <template> <div class="matchground"> <div class="row"> <div class="col-md-6" style="text-align: center;"> <div class="photo"> <img class="img-fluid" :src="$store.state.user.photo"> </div> <div class="username"> {{ $store.state.user.username }} </div> </div> <div class="col-md-6" style="text-align: center;"> <div class="photo"> <img class="img-fluid" :src="$store.state.pk.opponent_photo"> </div> <div class="username"> {{ $store.state.pk.opponent_username }} </div> </div> <div class="col-md-12 text-center" style="margin-top: 14vh;"> <button @click="click_match_btn" type="button" class="btn btn-info btn-lg"> {{ match_btn_info }} </button> </div> </div> </div> </template>
<script> import { ref } from "vue"; import { useStore } from "vuex";
export default { setup() { const store = useStore(); let match_btn_info = ref("开始匹配");
const click_match_btn = () => { if (match_btn_info.value === "开始匹配") { match_btn_info.value = "取消"; store.state.pk.socket.send(JSON.stringify({ event: "start_match", })); } else { match_btn_info.value = "开始匹配"; store.state.pk.socket.send(JSON.stringify({ event: "stop_match", })); } };
return { match_btn_info, click_match_btn, }; }, }; </script>
<style scoped> div.matchground { width: 60vw; height: 70vh; margin: 40px auto; border-radius: 10px; background-color: rgba(50, 50, 50, 0.5); }
img { width: 35%; border-radius: 50%; margin: 14vh 0 1vh 0; }
.username { font-size: 24px; font-weight: bold; color: white; } </style>
|
3.2 实现前后端交互逻辑
当用户点击开始匹配按钮后,前端要向服务器发出一个请求,后端接收到请求后应该将该用户放入匹配池中,由于目前还没有实现微服务,因此我们先在 WebSocketServer
后端用一个 Set
维护正在匹配的玩家,当匹配池中满两名玩家就将其匹配在一起,然后将匹配结果返回给两名玩家的前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| package com.kob.backend.consumer;
import com.alibaba.fastjson2.JSONObject; 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 static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>(); private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>(); private User user; private Session session = null;
private static UserMapper userMapper;
@Autowired public void setUserMapper(UserMapper userMapper) { WebSocketServer.userMapper = userMapper; }
@OnOpen public 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(); } }
@OnClose public void onClose() { System.out.println("Disconnected!"); if (this.user != null) { users.remove(this.user.getId()); matchPool.remove(this.user); } }
@OnMessage public void onMessage(String message, Session session) { System.out.println("Receive message!"); JSONObject data = JSONObject.parseObject(message); String event = data.getString("event");
if ("start_match".equals(event)) { this.startMatching(); } else if ("stop_match".equals(event)) { this.stopMatching(); } }
@OnError public 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) { Iterator<User> it = matchPool.iterator(); User a = it.next(), b = it.next(); matchPool.remove(a); matchPool.remove(b);
JSONObject respA = new JSONObject(); respA.put("event", "match_success"); respA.put("opponent_username", b.getUsername()); respA.put("opponent_photo", b.getPhoto()); users.get(a.getId()).sendMessage(respA.toJSONString());
JSONObject respB = new JSONObject(); respB.put("event", "match_success"); respB.put("opponent_username", a.getUsername()); respB.put("opponent_photo", a.getPhoto()); users.get(b.getId()).sendMessage(respB.toJSONString()); } }
private void stopMatching() { System.out.println("Stop matching!"); matchPool.remove(this.user); } }
|
接着修改一下 PKIndexView
,当接收到 WS 链接从后端发送过来的匹配成功消息后需要更新对手的头像和用户名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| ...
<script> ...
export default { ...
setup() { ...
onMounted(() => { ...
socket.onmessage = (msg) => { const data = JSON.parse(msg.data); console.log(data);
if (data.event === "match_success") { store.commit("updateOpponent", { username: data.opponent_username, photo: data.opponent_photo, }); setTimeout(() => { store.commit("updateStatus", "playing"); }, 3000); } };
socket.onclose = () => { console.log("Disconnected!"); store.commit("updateStatus", "matching"); };
... });
... }, }; </script>
...
|
测试的时候需要用两个浏览器,如果没有两个浏览器可以在 Edge 浏览器的右上角设置菜单中新建 InPrivate 窗口,这样就可以自己登录两个不同的账号进行匹配测试。
3.3 同步游戏地图
现在匹配成功后两名玩家进入游戏时看到的地图是不一样的,因为目前地图还都是在每名玩家本地的浏览器生成的,那么我们就需要将生成地图的逻辑放到服务器端。
先在后端的 consumer.utils
包下创建 Game
类,用来管理整个游戏流程,然后我们在其中先实现地图的随机生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| package com.kob.backend.consumer.utils;
import java.util.Arrays; import java.util.Random;
public class Game { 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 };
public Game(Integer rows, Integer cols, Integer inner_walls_count) { this.rows = rows; this.cols = cols; this.inner_walls_count = inner_walls_count; this.g = new boolean[rows][cols]; }
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; 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); 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; } } } }
|
然后在 WebSocketServer
类中当匹配成功时创建游戏地图,暂时先将其存到局部变量中,之后再进行优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| while (matchPool.size() >= 2) { Iterator<User> it = matchPool.iterator(); User a = it.next(), b = it.next(); matchPool.remove(a); matchPool.remove(b);
Game game = new Game(13, 14, 20); game.createMap();
JSONObject respA = new JSONObject(); respA.put("event", "match_success"); respA.put("opponent_username", b.getUsername()); respA.put("opponent_photo", b.getPhoto()); respA.put("game_map", game.getG()); users.get(a.getId()).sendMessage(respA.toJSONString());
JSONObject respB = new JSONObject(); respB.put("event", "match_success"); 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()); }
|
接下来需要在前端的 store/pk.js
文件中将地图存为全局变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default { state: { ... game_map: null, }, getters: {}, mutations: { ... updateGameMap(state, game_map) { state.game_map = game_map; }, }, actions: {}, modules: {}, };
|
在 PKIndexView.vue
中当接收到匹配成功的消息后需要更新地图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| ...
<script> ...
export default { ...
setup() { ...
onMounted(() => { ...
socket.onmessage = (msg) => { const data = JSON.parse(msg.data); console.log(data);
if (data.event === "match_success") { store.commit("updateOpponent", { username: data.opponent_username, photo: data.opponent_photo, }); store.commit("updateGameMap", data.game_map);
setTimeout(() => { store.commit("updateStatus", "playing"); }, 3000); } };
... });
... }, }; </script>
...
|
然后需要在 GameMap.vue
中将全局变量传给游戏地图 GameMap.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ...
<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(() => { new GameMap(canvas.value.getContext("2d"), parent.value, store); });
return { parent, canvas, }; }, }; </script>
...
|
最后就可以在 GameMap.js
中将地图渲染出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ...
export class GameMap extends AcGameObject { constructor(ctx, parent, store) { ...
this.store = store; }
...
create_walls_online() { const g = this.store.state.pk.game_map;
for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (g[r][c]) { this.walls.push(new Wall(r, c, this)); } } } }
...
start() { this.create_walls_online(); this.add_listening_events(); }
... }
|