网页版Java(Spring/Spring Boot/Spring MVC)五子棋项目(四)对战模块
- 一、约定前后端交互接口
- 1. 建立连接 的接口
- 2. 针对落子的请求和响应 的接口
- 二、实现前端页面
- 游戏大厅页面
- 游戏大厅里的 java script
- 三、实现后端
- 一. 建立各种请求和相应的类
- 1. 客户端连接到游戏房间后, 服务器返回的响应.
- GameReadyResponse
- 2. 落子请求
- GameRequest
- 3. 落子响应
- GameResponse
- 二. 实现用户管理类 和 房间管理类
- OnlineUserManager
- Room
- RoomManager
- 三. 实现GameAPI
- 1. afterConnectionEstablished
- 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
- 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
- 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
- 4. 设置当前玩家上线!
- 5. 把两个玩家加入到游戏房间中.
- 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
- 2. afterConnectionClosed
- 断开连接
一、约定前后端交互接口
1. 建立连接 的接口
2. 针对落子的请求和响应 的接口
二、实现前端页面
游戏大厅页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>游戏房间</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_room.css">
</head>
<body><div class="nav">五子棋对战</div><div class="container"><div><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示区域 --><div id="screen"> 等待玩家连接中... </div></div></div><script src="js/script.js"></script>
</body>
</html>
游戏大厅里的 java script
<script>let gameInfo = {roomId: null,thisUserId: null,thatUserId: null,isWhite: true,}//// 设定界面显示相关操作//function setScreenText(me) {let screen = document.querySelector('#screen');if (me) {screen.innerHTML = "轮到你落子了!";} else {screen.innerHTML = "轮到对方落子了!";}}//// 初始化 websocket//// 此处写的路径要写作 /game, 不要写作 /game/let websocketUrl = "ws://" + location.host + "/game";let websocket = new WebSocket(websocketUrl);websocket.onopen = function() {console.log("连接游戏房间成功!");}websocket.close = function() {console.log("和游戏服务器断开连接!");}websocket.onerror = function() {console.log("和服务器的连接出现异常!");}window.onbeforeunload = function() {websocket.close();}// 处理服务器返回的响应数据websocket.onmessage = function(event) {console.log("[handlerGameReady] " + event.data);let resp = JSON.parse(event.data);if (!resp.ok) {alert("连接游戏失败! reason: " + resp.reason);// 如果出现连接失败的情况, 回到游戏大厅location.assign("/game_hall.html");return;}if (resp.message == 'gameReady') {gameInfo.roomId = resp.roomId;gameInfo.thisUserId = resp.thisUserId;gameInfo.thatUserId = resp.thatUserId;gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);// 初始化棋盘initGame();// 设置显示区域的内容setScreenText(gameInfo.isWhite);} else if (resp.message == 'repeatConnection') {alert("检测到游戏多开! 请使用其他账号登录!");location.assign("/login.html");}}//// 初始化一局游戏//function initGame() {// 是我下还是对方下. 根据服务器分配的先后手情况决定let me = gameInfo.isWhite;// 游戏是否结束let over = false;let chessBoard = [];//初始化chessBord数组(表示棋盘的数组)for (let i = 0; i < 15; i++) {chessBoard[i] = [];for (let j = 0; j < 15; j++) {chessBoard[i][j] = 0;}}let chess = document.querySelector('#chess');let context = chess.getContext('2d');context.strokeStyle = "#BFBFBF";// 背景图片let logo = new Image();logo.src = "image/sky.jpeg";logo.onload = function () {context.drawImage(logo, 0, 0, 450, 450);initChessBoard();}// 绘制棋盘网格function initChessBoard() {for (let i = 0; i < 15; i++) {context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 430);context.stroke();context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30);context.stroke();}}// 绘制一个棋子, me 为 truefunction oneStep(i, j, isWhite) {context.beginPath();context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);if (!isWhite) {gradient.addColorStop(0, "#0A0A0A");gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1");gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}chess.onclick = function (e) {if (over) {return;}if (!me) {return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行let col = Math.floor(x / 30);let row = Math.floor(y / 30);if (chessBoard[row][col] == 0) {// 发送坐标给服务器, 服务器要返回结果send(row, col);// 留到浏览器收到落子响应的时候再处理(收到响应再来画棋子)// oneStep(col, row, gameInfo.isWhite);// chessBoard[row][col] = 1;}}function send(row, col) {let req = {message: 'putChess',userId: gameInfo.thisUserId,row: row,col: col};websocket.send(JSON.stringify(req));}// 之前 websocket.onmessage 主要是用来处理了游戏就绪响应. 在游戏就绪之后, 初始化完毕之后, 也就不再有这个游戏就绪响应了. // 就在这个 initGame 内部, 修改 websocket.onmessage 方法~~, 让这个方法里面针对落子响应进行处理!websocket.onmessage = function(event) {console.log("[handlerPutChess] " + event.data);let resp = JSON.parse(event.data);if (resp.message != 'putChess') {console.log("响应类型错误!");return;}// 先判定当前这个响应是自己落的子, 还是对方落的子.if (resp.userId == gameInfo.thisUserId) {// 我自己落的子// 根据我自己子的颜色, 来绘制一个棋子oneStep(resp.col, resp.row, gameInfo.isWhite);} else if (resp.userId == gameInfo.thatUserId) {// 我的对手落的子oneStep(resp.col, resp.row, !gameInfo.isWhite);} else {// 响应错误! userId 是有问题的!console.log('[handlerPutChess] resp userId 错误!');return;}// 给对应的位置设为 1, 方便后续逻辑判定当前位置是否已经有子了. chessBoard[resp.row][resp.col] = 1;// 交换双方的落子轮次me = !me;setScreenText(me);// 判定游戏是否结束let screenDiv = document.querySelector('#screen');if (resp.winner != 0) {if (resp.winner == gameInfo.thisUserId) {// alert('你赢了!');screenDiv.innerHTML = '你赢了!';} else if (resp.winner = gameInfo.thatUserId) {// alert('你输了!');screenDiv.innerHTML = '你输了!';} else {alert("winner 字段错误! " + resp.winner);}// 回到游戏大厅// location.assign('/game_hall.html');// 增加一个按钮, 让玩家点击之后, 再回到游戏大厅~let backBtn = document.createElement('button');backBtn.innerHTML = '回到大厅';backBtn.onclick = function() {location.replace('/game_hall.html');}let fatherDiv = document.querySelector('.container>div');fatherDiv.appendChild(backBtn);}}}
</script>
三、实现后端
一. 建立各种请求和相应的类
1. 客户端连接到游戏房间后, 服务器返回的响应.
GameReadyResponse
package com.example.java_gobang.game;// 客户端连接到游戏房间后, 服务器返回的响应.
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public boolean isOk() {return ok;}public void setOk(boolean ok) {this.ok = ok;}public String getReason() {return reason;}public void setReason(String reason) {this.reason = reason;}public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId = roomId;}public int getThisUserId() {return thisUserId;}public void setThisUserId(int thisUserId) {this.thisUserId = thisUserId;}public int getThatUserId() {return thatUserId;}public void setThatUserId(int thatUserId) {this.thatUserId = thatUserId;}public int getWhiteUser() {return whiteUser;}public void setWhiteUser(int whiteUser) {this.whiteUser = whiteUser;}
}
2. 落子请求
GameRequest
3. 落子响应
GameResponse
二. 实现用户管理类 和 房间管理类
OnlineUserManager
package com.example.java_gobang.game;import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;@Component
public class OnlineUserManager {// 这个哈希表就用来表示当前用户在游戏大厅在线状态.private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();// 这个哈希表就用来表示当前用户在游戏房间的在线状态.private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();public void enterGameHall(int userId, WebSocketSession webSocketSession) {gameHall.put(userId, webSocketSession);}public void exitGameHall(int userId) {gameHall.remove(userId);}public WebSocketSession getFromGameHall(int userId) {return gameHall.get(userId);}public void enterGameRoom(int userId, WebSocketSession webSocketSession) {gameRoom.put(userId, webSocketSession);}public void exitGameRoom(int userId) {gameRoom.remove(userId);}public WebSocketSession getFromGameRoom(int userId) {return gameRoom.get(userId);}
}
Room
package com.example.java_gobang.game;import com.example.java_gobang.JavaGobangApplication;
import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;import java.io.IOException;
import java.util.UUID;// 这个类就表示一个游戏房间
public class Room {// 使用字符串类型来表示, 方便生成唯一值.private String roomId;private User user1;private User user2;// 先手方的玩家 idprivate int whiteUser;private static final int MAX_ROW = 15;private static final int MAX_COL = 15;// 这个二维数组用来表示棋盘// 约定:// 1) 使用 0 表示当前位置未落子. 初始化好的 int 二维数组, 就相当于是 全 0// 2) 使用 1 表示 user1 的落子位置// 3) 使用 2 表示 user2 的落子位置private int[][] board = new int[MAX_ROW][MAX_COL];// 创建 ObjectMapper 用来转换 JSONprivate ObjectMapper objectMapper = new ObjectMapper();// 引入 OnlineUserManager// @Autowiredprivate OnlineUserManager onlineUserManager;// 引入 RoomManager, 用于房间销毁// @Autowiredprivate RoomManager roomManager;private UserMapper userMapper;// 通过这个方法来处理一次落子操作.// 要做的事情:public void putChess(String reqJson) throws IOException {// 1. 记录当前落子的位置.GameRequest request = objectMapper.readValue(reqJson, GameRequest.class);GameResponse response = new GameResponse();// 当前这个子是玩家1 落的还是玩家2 落的. 根据这个玩家1 和 玩家2 来决定往数组中是写 1 还是 2int chess = request.getUserId() == user1.getUserId() ? 1 : 2;int row = request.getRow();int col = request.getCol();if (board[row][col] != 0) {// 在客户端已经针对重复落子进行过判定了. 此处为了程序更加稳健, 在服务器再判定一次.System.out.println("当前位置 (" + row + ", " + col + ") 已经有子了!");return;}board[row][col] = chess;// 2. 打印出当前的棋盘信息, 方便来观察局势. 也方便后面验证胜负关系的判定.printBoard();// 3. 进行胜负判定int winner = checkWinner(row, col, chess);// 4. 给房间中的所有客户端都返回响应.response.setMessage("putChess");response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);// 要想给用户发送 websocket 数据, 就需要获取到这个用户的 WebSocketSessionWebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());// 万一当前查到的会话为空(玩家已经下线了) 特殊处理一下if (session1 == null) {// 玩家1 已经下线了. 直接认为玩家2 获胜!response.setWinner(user2.getUserId());System.out.println("玩家1 掉线!");}if (session2 == null) {// 玩家2 已经下线. 直接认为玩家1 获胜!response.setWinner(user1.getUserId());System.out.println("玩家2 掉线!");}// 把响应构造成 JSON 字符串, 通过 session 进行传输.String respJson = objectMapper.writeValueAsString(response);if (session1 != null) {session1.sendMessage(new TextMessage(respJson));}if (session2 != null) {session2.sendMessage(new TextMessage(respJson));}// 5. 如果当前胜负已分, 此时这个房间就失去存在的意义了. 就可以直接销毁房间. (把房间从房间管理器中给移除)if (response.getWinner() != 0) {// 胜负已分System.out.println("游戏结束! 房间即将销毁! roomId=" + roomId + " 获胜方为: " + response.getWinner());// 更新获胜方和失败方的信息.int winUserId = response.getWinner();int loseUserId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);// 销毁房间roomManager.remove(roomId, user1.getUserId(), user2.getUserId());}}private void printBoard() {// 打印出棋盘System.out.println("[打印棋盘信息] " + roomId);System.out.println("=====================================================================");for (int r = 0; r < MAX_ROW; r++) {for (int c = 0; c < MAX_COL; c++) {// 针对一行之内的若干列, 不要打印换行System.out.print(board[r][c] + " ");}// 每次遍历完一行之后, 再打印换行.System.out.println();}System.out.println("=====================================================================");}// 使用这个方法来判定当前落子是否分出胜负.// 约定如果玩家1 获胜, 就返回玩家1 的 userId// 如果玩家2 获胜, 就返回玩家2 的 userId// 如果胜负未分, 就返回 0private int checkWinner(int row, int col, int chess) {// 1. 检查所有的行// 先遍历这五种情况for (int c = col - 4; c <= col; c++) {// 针对其中的一种情况, 来判定这五个子是不是连在一起了~// 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)try {if (board[row][c] == chess&& board[row][c + 1] == chess&& board[row][c + 2] == chess&& board[row][c + 3] == chess&& board[row][c + 4] == chess) {// 构成了五子连珠! 胜负已分!return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况, 就在这里直接忽略这个异常.continue;}}// 2. 检查所有列for (int r = row - 4; r <= row; r++) {try {if (board[r][col] == chess&& board[r + 1][col] == chess&& board[r + 2][col] == chess&& board[r + 3][col] == chess&& board[r + 4][col] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {continue;}}// 3. 检查左对角线for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {try {if (board[r][c] == chess&& board[r + 1][c + 1] == chess&& board[r + 2][c + 2] == chess&& board[r + 3][c + 3] == chess&& board[r + 4][c + 4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {continue;}}// 4. 检查右对角线for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {try {if (board[r][c] == chess&& board[r + 1][c - 1] == chess&& board[r + 2][c - 2] == chess&& board[r + 3][c - 3] == chess&& board[r + 4][c - 4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {continue;}}// 胜负未分, 就直接返回 0 了.return 0;}public int getWhiteUser() {return whiteUser;}public void setWhiteUser(int whiteUser) {this.whiteUser = whiteUser;}public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId = roomId;}public User getUser1() {return user1;}public void setUser1(User user1) {this.user1 = user1;}public User getUser2() {return user2;}public void setUser2(User user2) {this.user2 = user2;}public Room() {// 构造 Room 的时候生成一个唯一的字符串表示房间 id.// 使用 UUID 来作为房间 idroomId = UUID.randomUUID().toString();// 通过入口类中记录的 context 来手动获取到前面的 RoomManager 和 OnlineUserManageronlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);roomManager = JavaGobangApplication.context.getBean(RoomManager.class);userMapper = JavaGobangApplication.context.getBean(UserMapper.class);}public static void main(String[] args) {Room room = new Room();System.out.println(room.roomId);}
}
RoomManager
package com.example.java_gobang.game;import org.springframework.stereotype.Component;import java.util.concurrent.ConcurrentHashMap;// 房间管理器类.
// 这个类也希望有唯一实例.
@Component
public class RoomManager {private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>();public void add(Room room, int userId1, int userId2) {rooms.put(room.getRoomId(), room);userIdToRoomId.put(userId1, room.getRoomId());userIdToRoomId.put(userId2, room.getRoomId());}public void remove(String roomId, int userId1, int userId2) {rooms.remove(roomId);userIdToRoomId.remove(userId1);userIdToRoomId.remove(userId2);}public Room getRoomByRoomId(String roomId) {return rooms.get(roomId);}public Room getRoomByUserId(int userId) {String roomId = userIdToRoomId.get(userId);if (roomId == null) {// userId -> roomId 映射关系不存在, 直接返回 nullreturn null;}return rooms.get(roomId);}
}
三. 实现GameAPI
package com.example.java_gobang.api;import com.example.java_gobang.game.*;
import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import javax.annotation.Resource;
import java.io.IOException;@Component
public class GameAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate RoomManager roomManager;@Autowiredprivate OnlineUserManager onlineUserManager;@Resourceprivate UserMapper userMapper;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp = new GameReadyResponse();// 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)User user = (User) session.getAttributes().get("user");if (user == null) {resp.setOk(false);resp.setReason("用户尚未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)Room room = roomManager.getRoomByUserId(user.getUserId());if (room == null) {// 如果为 null, 当前没有找到对应的房间. 该玩家还没有匹配到.resp.setOk(false);resp.setReason("用户尚未匹配到!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)// 前面准备了一个 OnlineUserManagerif (onlineUserManager.getFromGameHall(user.getUserId()) != null|| onlineUserManager.getFromGameRoom(user.getUserId()) != null) {// 如果一个账号, 一边是在游戏大厅, 一边是在游戏房间, 也视为多开~~resp.setOk(true);resp.setReason("禁止多开游戏页面");resp.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 4. 设置当前玩家上线!onlineUserManager.enterGameRoom(user.getUserId(), session);// 5. 把两个玩家加入到游戏房间中.// 前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.// 因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)// 当前这个逻辑是在 game_room.html 页面加载的时候进行的.// 执行到当前逻辑, 说明玩家已经页面跳转成功了!!// 页面跳转, 其实是个大活~~ (很有可能出现 "失败" 的情况的)synchronized (room) {if (room.getUser1() == null) {// 第一个玩家还尚未加入房间.// 就把当前连上 websocket 的玩家作为 user1, 加入到房间中.room.setUser1(user);// 把先连入房间的玩家作为先手方.room.setWhiteUser(user.getUserId());System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");return;}if (room.getUser2() == null) {// 如果进入到这个逻辑, 说明玩家1 已经加入房间, 现在要给当前玩家作为玩家2 了room.setUser2(user);System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");// 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.// 通知这两个玩家说, 游戏双方都已经准备好了.// 通知玩家1noticeGameReady(room, room.getUser1(), room.getUser2());// 通知玩家2noticeGameReady(room, room.getUser2(), room.getUser1());return;}}// 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.// 这种情况理论上是不存在的, 为了让程序更加的健壮, 还是做一个判定和提示.resp.setOk(false);resp.setReason("当前房间已满, 您不能加入房间");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {GameReadyResponse resp = new GameReadyResponse();resp.setMessage("gameReady");resp.setOk(true);resp.setReason("");resp.setRoomId(room.getRoomId());resp.setThisUserId(thisUser.getUserId());resp.setThatUserId(thatUser.getUserId());resp.setWhiteUser(room.getWhiteUser());// 把当前的响应数据传回给玩家.WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 1. 先从 session 里拿到当前用户的身份信息User user = (User) session.getAttributes().get("user");if (user == null) {System.out.println("[handleTextMessage] 当前玩家尚未登录! ");return;}// 2. 根据玩家 id 获取到房间对象Room room = roomManager.getRoomByUserId(user.getUserId());// 3. 通过 room 对象来处理这次具体的请求room.putChess(message.getPayload());}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");if (user == null) {// 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.return;}WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());if (session == exitSession) {// 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户 " + user.getUsername() + " 游戏房间连接异常!");// 通知对手获胜了noticeThatUserWin(user);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");if (user == null) {// 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.return;}WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());if (session == exitSession) {// 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户 " + user.getUsername() + " 离开游戏房间!");// 通知对手获胜了noticeThatUserWin(user);}private void noticeThatUserWin(User user) throws IOException {// 1. 根据当前玩家, 找到玩家所在的房间Room room = roomManager.getRoomByUserId(user.getUserId());if (room == null) {// 这个情况意味着房间已经被释放了, 也就没有 "对手" 了System.out.println("当前房间已经释放, 无需通知对手!");return;}// 2. 根据房间找到对手User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();// 3. 找到对手的在线状态WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());if (webSocketSession == null) {// 这就意味着对手也掉线了!System.out.println("对手也已经掉线了, 无需通知!");return;}// 4. 构造一个响应, 来通知对手, 你是获胜方GameResponse resp = new GameResponse();resp.setMessage("putChess");resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));// 5. 更新玩家的分数信息int winUserId = thatUser.getUserId();int loseUserId = user.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);// 6. 释放房间对象roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());}
}
1. afterConnectionEstablished
1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
4. 设置当前玩家上线!
5. 把两个玩家加入到游戏房间中.
6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp = new GameReadyResponse();// 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)User user = (User) session.getAttributes().get("user");if (user == null) {resp.setOk(false);resp.setReason("用户尚未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)Room room = roomManager.getRoomByUserId(user.getUserId());if (room == null) {// 如果为 null, 当前没有找到对应的房间. 该玩家还没有匹配到.resp.setOk(false);resp.setReason("用户尚未匹配到!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)// 前面准备了一个 OnlineUserManagerif (onlineUserManager.getFromGameHall(user.getUserId()) != null|| onlineUserManager.getFromGameRoom(user.getUserId()) != null) {// 如果一个账号, 一边是在游戏大厅, 一边是在游戏房间, 也视为多开~~resp.setOk(true);resp.setReason("禁止多开游戏页面");resp.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}// 4. 设置当前玩家上线!onlineUserManager.enterGameRoom(user.getUserId(), session);// 5. 把两个玩家加入到游戏房间中.// 前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.// 因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)// 当前这个逻辑是在 game_room.html 页面加载的时候进行的.// 执行到当前逻辑, 说明玩家已经页面跳转成功了!!// 页面跳转, 其实是个大活~~ (很有可能出现 "失败" 的情况的)synchronized (room) {if (room.getUser1() == null) {// 第一个玩家还尚未加入房间.// 就把当前连上 websocket 的玩家作为 user1, 加入到房间中.room.setUser1(user);// 把先连入房间的玩家作为先手方.room.setWhiteUser(user.getUserId());System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");return;}if (room.getUser2() == null) {// 如果进入到这个逻辑, 说明玩家1 已经加入房间, 现在要给当前玩家作为玩家2 了room.setUser2(user);System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");// 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.// 通知这两个玩家说, 游戏双方都已经准备好了.// 通知玩家1noticeGameReady(room, room.getUser1(), room.getUser2());// 通知玩家2noticeGameReady(room, room.getUser2(), room.getUser1());return;}}// 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.// 这种情况理论上是不存在的, 为了让程序更加的健壮, 还是做一个判定和提示.resp.setOk(false);resp.setReason("当前房间已满, 您不能加入房间");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}
2. afterConnectionClosed
断开连接
@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");if (user == null) {// 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.return;}WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());if (session == exitSession) {// 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户 " + user.getUsername() + " 离开游戏房间!");// 通知对手获胜了noticeThatUserWin(user);}