[网页五子棋][匹配对战]落子实现思路、发送落子请求、处理落子响应
文章目录
- 落子实现思路
- 发送落子请求
- 处理落子响应
- 两种棋盘的区别
- 实现 handleTextMessage
- 实现对弈功能
- 控制台打印棋盘
- 完善前端逻辑
落子实现思路
先来实现:点击棋盘,能发送落子请求
- 客户端 1 点击了棋盘位置,先不着急画子,而是给服务器发送一个
websocket
请求(前面约定的落子请求格式,是谁在哪个位子上落子) - 服务器内部需要维护一个棋盘
- 服务器根据落子请求,在棋盘上进行更新
- 还需要进行胜负判定
- 最后将响应(格式为前面约定的落子响应格式)返回给两个客户端
- 两个客户端收到落子响应之后,再在棋盘上绘制棋子
服务器需要把棋盘上的变化,广播给房间中的每个玩家
发送落子请求
修改 onclick
函数,在落子操作时加入发送请求的逻辑
- 实现
send
,通过websocket
发送落子请求
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) { // TODO 发送坐标给服务器,服务器要返回结果 send(row, col); // 留到浏览器收到落子响应的时候再处理(收到响应再来画棋子) // oneStep(col, row, gameInfo.isWhite); // chessBoard[row][col] = 1; }
}
// TODO 实现发送落子请求逻辑和处理落子响应逻辑
function send(row, col) { console.log("send"); let request = { message: "putChess", userId: gameInfo.thisUserId, row: row, col: col, } websocket.send(JSON.stringify(request));
}
处理落子响应
两种棋盘的区别
服务器的棋盘实现:
public class Room { // ......private int[][] board = new int[15][15];//......
}
- 这个二维数组用来表示棋盘
- 约定:
- 使用
0
表示当前位置未落子,初始化好的int
二维数组,相当于是全0
- 使用
1
表示user1
的落子位置 - 使用
2
表示user2
的落子位置
- 使用
客户端和服务器这两边的二维数组的区别
- 服务器的数组元素有三种状态
- 服务器这边的二维数组,起到的效果就是进行判定胜负,就得知道玩家 1 和玩家 2 的子都在哪
- 客户端的数组元素只有两种状态
- 客户端的二维数组只是用来判定这里有没有子,就不需要区分这个子是谁
- 判定有没有子,主要是为了避免重复落子
如果直接在客户端来判定胜负关系,是否可行呢?
- 不太可行
- 游戏中的关键逻辑,还是要交给服务器来进行判定
外挂:工作过程主要就是篡改客户端这边的数据和逻辑
为什么吃鸡的外挂多?
- 因为这种射击类的游戏,要求网络延时极低,不然残血时一颗子弹打向你,等你大包打起来了这颗子弹的扣血响应才来
- 所以这类游戏的关键判定只能放在客户端,就给了不法分子篡改客户端数据的机会,就造成了射击类游戏外挂泛滥
实现 handleTextMessage
实现 gameAPI
中的 handleTextMessage
方法
@Override
protected 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());
}
实现对弈功能
实现 room
中的 putChess
方法
- 先把请求解析成请求对象
- 根据请求对象中的信息,往棋盘上落子
- 落子完毕后,为了方便测试,可以打印出棋盘的当前状况
- 检查游戏是否结束
- 构造落子响应,写回给每个玩家
- 写回的时候如果发现某个玩家掉线,则判定另一方为获胜
- 如果游戏胜负已分,则修改玩家的分数,并销毁房间
package org.example.java_gobang.game; import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.java_gobang.JavaGobangApplication;
import org.example.java_gobang.model.User;
import org.example.java_gobang.model.UserMapper;
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; // 先手方的玩家 id private 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 用来转换 JSON private ObjectMapper objectMapper = new ObjectMapper(); // 引入 OnlineUserManager // @Autowired private OnlineUserManager onlineUserManager; private UserMapper userMapper; // 引入 RoomManager,用于房间销毁 // @Autowired private RoomManager roomManager; // 通过这个方法来处理一次落子操作 // 要做的事情: // 1. 记录当前落子的位置 // 2. 进行胜负判定 // 3. 给客户端返回响应 public void putChess(String jsonString) throws IOException { // 1. 记录当前落子的位置 GameRequest request = objectMapper.readValue(jsonString, GameRequest.class); GameResponse response = new GameResponse(); // 当前这个子是玩家1 落的还是玩家2 落的。根据这个玩家1 和玩家2 来决定往数组中是写 1 还是 2 int 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(); // 2. 进行胜负判定 int winner = checkWinner(row, col); // 3. 给房间中的所有客户端都返回响应 response.setMessage("putChess"); response.setUserId(request.getUserId()); response.setRow(row); response.setCol(col); response.setWinner(winner); // 要想给用户发送 websocket 数据,就需要获取到这个用户的 WebSocketSession WebSocketSession session1 = onlineUserManager.getFromGameRoom(getUser1().getUserId()); WebSocketSession session2 = onlineUserManager.getFromGameRoom(getUser2().getUserId()); // 万一当前查到的会话为空(玩家已经下线了)特殊处理一下 if (session1 == null) { // 玩家1 已经下线了,直接认为玩家2 获胜! response.setWinner(user2.getUserId()); System.out.println("玩家1 掉线!"); } if (session2 == null) { // 玩家2 已经下线,直接认为玩家1 获胜! response.setWinner(user2.getUserId()); System.out.println("玩家2 掉线!"); } String responseJson = objectMapper.writeValueAsString(response); if (session1 != null) { session1.sendMessage(new TextMessage(responseJson)); } if(session2 != null) { session2.sendMessage(new TextMessage(responseJson)); } // 4. 如果胜负已分,就把 room 从房间管理器中销毁 if (response.getWinner() != 0) { System.out.println("游戏结束!房间即将销毁! roomId=" + roomId + " 获胜方为:" + response.getWinner()); // 销毁房间 roomManager.remove(roomId, user1.getUserId(), user2.getUserId()); } }//......//......
}
控制台打印棋盘
实现打印棋盘的逻辑
private void printBoard() { // 打印出棋盘 System.out.println("[打印棋盘信息] "); 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("=======================================");
}
完善前端逻辑
- 之前
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[row][col] = 1; // 交换双方的落子轮次 me != me; setScreenText(me); // 判定游戏是否结束 if (resp.winner != 0) { if (resp.winner == gameInfo.thisUserId){ alert('你赢了!'); } else if (resp.winner == gameInfo.thatUserId) { alert('你输了!'); } else { console.log("winner 字段错误!" + resp.winner); } // 胜负已分,回到游戏大厅 location.assign('game_hall.html'); }
}