本节内容为同步玩家的移动与碰撞检测,并实现游戏结束的前端界面以及持久化游戏对局信息。
1. 同步玩家位置
1.1 游戏信息的记录
两名玩家初始位置需要由服务器确定,且之后的每次移动都需要在服务器上判断。我们需要在 Game
类中添加 Player
类用来记录玩家的信息,在 consumer.utils
包下创建 Player
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 List<Integer> steps; }
然后就可以在 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 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 <>()); playerB = new Player (idB, 1 , cols - 2 , new ArrayList <>()); } public Player getPlayerA () { return playerA; } public Player getPlayerB () { return playerB; } ... }
在 WebSocketServer
中创建 Game
时传入两名玩家的 ID,并且我们将与游戏内容相关的信息全部包装到一个 JSONObject
类中:
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 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 ) { 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 , 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 (); 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()); JSONObject respB = new JSONObject (); 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
中创建两名玩家的信息:
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 export default { state : { ... a_id : 0 , a_sx : 0 , a_sy : 0 , b_id : 0 , b_sx : 0 , b_sy : 0 , gameObject : null , }, 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
中用到:
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 ... <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
数据:
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 ... <script > ... export default { ... setup ( ) { ... onMounted (() => { ... socket.onmessage = (msg ) => { ... if (data.event === "match_success" ) { ... store.commit ("updateGame" , data.game ); ... } }; ... }); ... }, }; </script > <style scoped > </style >
1.2 实现多线程同步移动
我们需要实现两名玩家的客户端以及服务器端的移动同步,假如 Client1 发出了移动指令,那么就会将这个消息发送给服务器,同理另一个客户端 Client2 发出移动指令时也会将消息发送给服务器,服务器在接收到两名玩家的消息后再将消息同步给两名玩家。
我们在 WebSocketServer
中会维护一个游戏 Game
,这个 Game
也有自己的执行流程,它会先创建地图 creatMap
,接着会一步一步执行,即 nextStep
,每一步会等待两名玩家的操作,这个操作可以是键盘输入,也可以是由 Bot 代码执行的微服务返回回来的结果。获取输入后会将结果发送给一个评判系统 judge
,来判断两名玩家下一步是不是合法的,如果有一方不合法就游戏结束。
在等待用户输入时会有一个时间限制,比如5秒,如果有一方还没有输入则表示输了,同样也是游戏结束。否则如果两方输入的下一步都是合法的则继续循环 nextStep
。这个 nextStep
流程是比较独立的,而且每个游戏对局都有这个独立的过程,如果 Game
是单线程的,那么在等待用户输入时这个线程就会卡死,如果有多个游戏对局的话那么只能先卡死在某个对局中,其他对局的玩家体验就会很差,因此 Game
不能作为一个单线程来处理,每次在等待用户输入时都需要另起一个新的线程,这就涉及到了多线程的通信以及加锁的问题。
ReentrantLock
是 Java 中常用的锁,属于乐观锁类型,多线程并发情况下能保证共享数据安全性,线程间有序性。它的作用和 synchronize
是一样的,都是实现锁的功能,但是 ReentrantLock
需要手写代码对锁进行获取和释放(一定要在 finally
块里面释放),要不然就永远死锁了。ReentrantLock
通过原子操作和阻塞实现锁原理,一般使用 lock()
获取锁,unlock()
释放锁。
我们将 Game
类继承自 Thread
类即可转为多线程,然后需要实现 Thread
的入口函数,使用快捷键 Alt + Insert
,选择重写方法,需要重写的是 run()
方法,这是新线程的入口函数:
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 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 ; private Integer nextStepB = null ; private ReentrantLock lock = new ReentrantLock (); private String status = "playing" ; private String loser = "" ; 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 <>()); 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(); 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 ; 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 ; } } } private boolean nextStep () { try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0 ; i < 50 ; i++) { try { Thread.sleep(100 ); lock.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) { WebSocketServer.users.get(playerA.getId()).sendMessage(message); WebSocketServer.users.get(playerB.getId()).sendMessage(message); } private void sendMove () { 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 () { JSONObject resp = new JSONObject (); resp.put("event" , "result" ); resp.put("loser" , loser); sendAllMessage(resp.toJSONString()); } @Override public void run () { for (int i = 0 ; i < 1000 ; i++) { if (nextStep()) { judge(); if ("playing" .equals(status)) { sendMove(); } 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(); break ; } } } }
然后前端 GameMap.js
中在移动时需要向后端通信,现在两名玩家的键盘输入操作就只需要 W/S/A/D
了:
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 import { AcGameObject } from "./AcGameObject" ;import { Wall } from "./Wall" ;import { Snake } from "./Snake" ;export class GameMap extends AcGameObject { ... add_listening_events ( ) { this .ctx .canvas .focus (); 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
,然后需要接收前端的移动请求:
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 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 { public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap <>(); private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet <>(); private User user; private Session session = null ; private Game game = 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(); } else if ("move" .equals(event)) { move(data.getInteger("direction" )); } } @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); 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 (); 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()); JSONObject respB = new JSONObject (); 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
中处理接收到后端发来的移动消息以及游戏结束消息:
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 > <PlayGround v-if ="$store.state.pk.status === 'playing'" /> <MatchGround v-else /> </template > <script > 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 ); 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 (() => { 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 (); }); }, }; </script > <style scoped > </style >
2. 同步碰撞检测
现在还需要将碰撞检测放到后端进行判断,先将 Snake.js
中的碰撞检测判断代码删掉,并将死后变白的逻辑放到 render()
函数中:
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 ... export class Snake extends AcGameObject { ... next_step ( ) { ... } ... render ( ) { ... ctx.fillStyle = this .color ; if (this .status === "die" ) { ctx.fillStyle = "white" ; } ... } }
接下来需要实现后端中的 judge()
方法,在判断的时候需要知道当前蛇的身体有哪些,先在 comsumer.utils
包下创建 Cell
类表示身体的每一格:
1 2 3 4 5 6 7 8 9 10 11 12 13 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
类中创建一个方法能够根据玩家历史走过的路径找出当前这条蛇身体的每一格:
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 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 List<Integer> steps; private boolean check_tail_increasing (int step) { if (step <= 7 ) return true ; return step % 3 == 1 ; } public List<Cell> getCells () { List<Cell> 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()
方法:
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 ... public class Game extends Thread { ... private boolean check_valid (List<Cell> cellsA, List<Cell> cellsB) { int n = cellsA.size(); Cell headCellA = cellsA.get(n - 1 ); if (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 () { List<Cell> cellsA = playerA.getCells(); List<Cell> 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
变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { state : { ... loser : "none" , }, getters : {}, mutations : { ... updateLoser (state, loser ) { state.loser = loser; }, }, actions : {}, modules : {}, };
然后在 PKIndexView
组件的游戏结束处理语句块中添加更新 loser
的语句:
1 store.commit ("updateLoser" , data.loser );
游戏结束后需要给用户展示谁赢谁输的界面,并提供一个重开按钮,在 components
目录下创建 ResultBoard.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 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 <template > <div class ="card text-bg-secondary text-center" > <div class ="card-header" style ="font-size: 26px;" > 游戏结束 </div > <div class ="card-body" style ="background-color: rgba(255, 255, 255, 0.4);" > <div class ="result_board_text" v-if ="$store.state.pk.loser === 'all'" > Draw </div > <div class ="result_board_text" v-else-if ="$store.state.pk.loser === 'A' && $store.state.pk.a_id.toString() === $store.state.user.id" > Lose </div > <div class ="result_board_text" v-else-if ="$store.state.pk.loser === 'B' && $store.state.pk.b_id.toString() === $store.state.user.id" > Lose </div > <div class ="result_board_text" v-else > Win </div > <div class ="result_board_btn" > <button @click ="returnHome" type ="button" class ="btn btn-info btn-lg" > 返回主页 </button > </div > </div > </div > </template > <script > 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, }; }, }; </script > <style 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 ; } </style >
注意 state.pk
中的 a_id
和 b_id
是整数,而之前 state.user
中的 id
是字符串,因此需要做类型转换再判断是否相等。
4. 持久化游戏状态
4.1 创建数据库表
最后我们还需要将游戏过程存到数据库中,方便用户之后回看游戏录像,在数据库中创建 record
表用来记录每局对战的信息:
id: int
(主键、自增、非空)
a_id: int
a_sx: int
a_sy: int
b_id: int
b_sx: int
b_sy: int
a_steps: varchar(1000)
b_steps: varchar(1000)
map: varchar(1000)
loser: varchar(10)
createtime: datetime
创建该数据库表的 SQL 语句如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
类如下:
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 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
类如下:
1 2 3 4 5 6 7 8 9 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 BaseMapper <Record> {}
4.2 保存游戏对局信息
可以在向前端发送游戏结果消息之前将对局信息存下来,首先需要在 WebSocketServer
中将 RecordMapper
创建出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... @Component @ServerEndpoint("/websocket/{token}") public class WebSocketServer { ... public static RecordMapper recordMapper; @Autowired public void setRecordMapper (RecordMapper recordMapper) { WebSocketServer.recordMapper = recordMapper; } ... }
然后在 Player
中创建辅助函数用来返回 steps
的字符串形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ... @Data @NoArgsConstructor @AllArgsConstructor public class Player { ... private List<Integer> steps; ... public String getStringSteps () { StringBuilder res = new StringBuilder (); for (int d: steps) { res.append(d); } return res.toString(); } }
最后就可以在 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 ... public class Game extends Thread { ... private String getStringMap () { 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 () { JSONObject resp = new JSONObject (); resp.put("event" , "result" ); resp.put("loser" , loser); saveRecord(); sendAllMessage(resp.toJSONString()); } ... }