网页五子棋-对战
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 接口设计
- 前端页面
- 实现棋盘和棋子的绘制
- 客户端websocket
- 服务器实现游戏房间
- 线程安全问题
- 测试
- 发送落子请求
- 前端
- 服务端
- 客户端
- 测试
- 异常问题
- 手动注入对象
- 服务端打印棋盘
- 判定胜负
- 玩家掉线
- 分数更新
- 第五个棋子无法显示
- 页面回退问题
- 额外可以添加的功能
- 上传到服务器
- 总结
前言
接口设计
增加一点
这个是匹配的前后端接口设计
前端页面
<!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>
canvas 是html5引入的新标签,就是一个画布,可以在画布上画画
所以棋盘和棋子都是画上去的
实现棋盘和棋子的绘制
这些canvas的api我们就不用自己写了,直接引入一个script.js和一个sky的图片就可以了
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
//////////////////////////////////////////////////
// TODO//////////////////////////////////////////////////
// 初始化一局游戏
//////////////////////////////////////////////////
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) {// TODO 发送坐标给服务器, 服务器要返回结果oneStep(col, row, gameInfo.isWhite);chessBoard[row][col] = 1;}}
}initGame();
这个就是初始的js
客户端websocket
let websocket = new WebSocket("ws://localhost:8080/game");
websocket.onopen = function (evt) {console.log("对战链接成功");
};websocket.onclose = function (evt) {console.log("对战链接关闭");
};
websocket.onerror = function (evt) {console.log("对战链接错误");
};window.onbeforeunload = function () {//这个是在页面关闭的时候触发的websocket.close();
};websocket.onmessage = function (evt) {console.log(evt.data)let data = JSON.parse(evt.data);if(!data.ok){alert("对战返回类型错误"+data.reason);console.log("对战返回类型错误"+data.reason);location.assign("/game_hall.html")}if(data.message !== "gameReady"){console.log("对战返回类型错误"+data.message);alert("对战返回类型错误"+data.message);location.assign("/game_hall.html")}gameInfo.isWhite=data.isWhite;gameInfo.roomId=data.roomId;gameInfo.thisUserId=data.thisUserId;gameInfo.thatUserId=data.thatUserId;initGame();setScreenText(gameInfo.isWhite);
};
服务器实现游戏房间
@Component
public class GameAPI extends TextWebSocketHandler {@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RoomManager roomManager;@Autowiredprivate OnlineUserManager onlineUserManager;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {User user = (User)session.getAttributes().get("user");GameReadyResponse res = new GameReadyResponse();//检验是否登录if(user==null){res.setOk( false);res.setReason("请先登录");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}//检验是否点击了匹配,防止是直接登录然后跳转到房间里面String roomId = roomManager.getRoomIdByUserId(user.getUserId());if(roomId==null){res.setOk( false);res.setReason("请先点击匹配");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}//检验是否是多开的,一个匹配,一个游戏,或者,两个游戏,或者两个匹配都是多开的//所以不同的页面要有不同的在线管理器,因为gameHall只能管理匹配页面的在线状态,如果跳转了页面,就会close,然后这个页面的user就不在线了if(onlineUserManager.getGameRoom(user.getUserId())!=null || onlineUserManager.getGameHall(user.getUserId())!=null){//表示这是多开的res.setOk( false);res.setReason("请勿多开");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}//走到这里说明没有多开,那么就是正常的了,正常加入在线状态onlineUserManager.enterGameRoom(user.getUserId(), session);//可以把用户加入房间了//如果当初在匹配的时候就把用户加入房间的话,那如果在切换页面的时候掉线了咋办,又在房间,但是却不在线,就很难搞,所以我们在连接成功,不掉线的时候,才把用户加入房间Room room = roomManager.getRoomByRoomID(roomId);if(room.getUser1()== null){room.setUser1(user);room.setWhiteUser(user.getUserId());//让第一个用户先走System.out.println("用户"+user.getUsername()+"加入房间");return;}if(room.getUser2()== null){room.setUser2(user);System.out.println("用户"+user.getUsername()+"加入房间");//走到这里说明两个用户都加入房间了,可以开始游戏了,先通知两个用户成功了//然后通知两个用户notice(room,room.getUser1(),room.getUser2());notice(room,room.getUser2(),room.getUser1());}//后面的理论不会发生,但是为了严谨res.setOk(false);res.setReason("房间已经满了");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res))); }private void notice(Room room, User user1, User user2) {GameReadyResponse res = new GameReadyResponse();res.setOk( true);res.setMessage("gameReady");res.setRoomId(room.getRoomId());res.setThisUserId(user1.getUserId());res.setThatUserId(user2.getUserId());res.setWhiteUser(room.getWhiteUser());WebSocketSession gameRoom = onlineUserManager.getGameRoom(user1.getUserId());try {gameRoom.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));} catch (Exception e) {e.printStackTrace();}}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User)session.getAttributes().get("user");GameReadyResponse res = new GameReadyResponse();//防止多开删除原来的sessionWebSocketSession gameRoom = onlineUserManager.getGameRoom(user.getUserId());if(gameRoom== session){onlineUserManager.exitGameRoom(user.getUserId());}}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User)session.getAttributes().get("user");GameReadyResponse res = new GameReadyResponse();//防止多开删除原来的sessionWebSocketSession gameRoom = onlineUserManager.getGameRoom(user.getUserId());if(gameRoom== session){onlineUserManager.exitGameRoom(user.getUserId());//不在线了}}
}
@Data
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;private Integer thisUserId;private Integer thatUserId;private Integer whiteUser;
}
@Data
public class GameRequest {private String message;private String userId;private Integer row;private Integer col;
}
@Data
public class GameResponse {private String message;private String userId;private Integer row;private Integer col;private Integer winner;
}
线程安全问题
在房间页面的时候,刚刚建立websocket链接的时候,两个客户端都会去建立连接,意思就是有两个线程,两个线程都会去修改同一个room类,所以就会有线程安全的问题了
synchronized (room){if(room.getUser1()== null){room.setUser1(user);room.setWhiteUser(user.getUserId());//让第一个用户先走System.out.println("用户"+user.getUsername()+"加入房间");return;}if(room.getUser2()== null){room.setUser2(user);System.out.println("用户"+user.getUsername()+"加入房间");//走到这里说明两个用户都加入房间了,可以开始游戏了,先通知两个用户成功了//然后通知两个用户notice(room,room.getUser1(),room.getUser2());notice(room,room.getUser2(),room.getUser1());}}
这样就可以了
测试
js代码会在浏览器中产生缓存,所以改了js代码一定要ctrl+f5
还有一个问题就是,在匹配的时候,如果多开会关闭页面,跳转到登录页面,但是正常匹配成功也会关闭
意思就是我们无法区分正常关闭页面和多开关闭页面,而多开关闭页面要切换到登录页面,正常切换的话就是room页面
所以我们要对多开的响应类型也进行处理
意思就是设置为true,messgae为repeatConnection,就是在我们意料之内的东西,都要进行分类处理
if(onlineUserManager.getGameRoom(user.getUserId())!=null || onlineUserManager.getGameHall(user.getUserId())!=null){//表示这是多开的res.setOk( true);res.setReason("请勿多开");res.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}
if(user==null){res.setOk( true);res.setReason("请先登录");res.setMessage("notLogin");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}
if(!data.ok){alert("对战返回类型错误"+data.reason);console.log("对战返回类型错误"+data.reason);location.assign("/game_hall.html")}if(data.message === "gameReady"){gameInfo.isWhite=data.whiteUser===data.thisUserId;gameInfo.roomId=data.roomId;gameInfo.thisUserId=data.thisUserId;gameInfo.thatUserId=data.thatUserId;initGame();setScreenText(gameInfo.isWhite);}else if(data.message === "repeatConnection"){alert('请勿重复连接');location.assign('/login.html')console.log('收到了非法响应')}else if(data.message === "notLogin"){alert('请勿重复登录');location.assign('/login.html')}else{console.log("对战返回类型错误"+data.message);alert("对战返回类型错误"+data.message);location.assign("/game_hall.html")}
还有一个小bug,就是线程run方法那里必须一直循环
@Overridepublic void run() {while (true){handlerMatch(veryHighQueue);}}
这样就可以了
if(room.getUser2()== null){room.setUser2(user);System.out.println("用户"+user.getUsername()+"加入房间");//走到这里说明两个用户都加入房间了,可以开始游戏了,先通知两个用户成功了//然后通知两个用户notice(room,room.getUser1(),room.getUser2());notice(room,room.getUser2(),room.getUser1());return;}
还有这里要加上一个return
这样就成功了
发送落子请求
前端
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;}}function send(row, col) {let data = {message: "putChess",roomId: gameInfo.roomId,row: row,col: col,}websocket.send(JSON.stringify(data));}
服务端
客户端和服务端都有一个二维数组,客户端的二维数组是0或者1,表示这个位置下没下
服务端表示的是0,1,2,1表示user1下的,2表示user2下的,0表示未知
所以说服务端处理得更详细一点,为什么要在服务端进行是否胜利的判断呢,因为客户端判断的话,容易产生外挂,所以要在服务端判断才好
@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {User user = (User)session.getAttributes().get("user");if(user==null){GameReadyResponse res = new GameReadyResponse();res.setOk( true);res.setMessage("notLogin");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));return;}//先获取房间Room room = roomManager.getRoomByRoomID(roomManager.getRoomIdByUserId(user.getUserId()));///下棋room.putChess(message.getPayload());}
private int[][] board = new int[15][15];private ObjectMapper objectMapper = new ObjectMapper();@Resourceprivate OnlineUserManager onlineUserManager;public void putChess(String payload) throws Exception {GameRequest gameRequest = objectMapper.readValue(payload, GameRequest.class);GameResponse response = new GameResponse();response.setUserId(gameRequest.getUserId());response.setMessage("putChess");int row = gameRequest.getRow();int col = gameRequest.getCol();if(board[row][col] != 0){System.out.println("row:"+row+"col:"+col+"这个位置已经被下过了");//说明这个位置已经被下了,直接返回,什么都不干,客户端是判断了的,这里在判断一下,但是什么都不干return;}board[row][col] = Objects.equals(gameRequest.getUserId(), user1.getUserId()) ? 1:2;response.setRow(row);response.setCol(col);//TODO 判断是否结束游戏int winner = checkWinner(row,col);response.setWinner(winner);//获取sessionWebSocketSession session1 = onlineUserManager.getGameRoom(user1.getUserId());WebSocketSession session2 = onlineUserManager.getGameRoom(user2.getUserId());if(session1==null){System.out.println("user1掉线");response.setWinner(user2.getUserId());//一个挂机,另一个直接获胜}if(session2==null){System.out.println("user2掉线");response.setWinner(user1.getUserId());//一个挂机,另一个直接获胜} //通知两方的userif(session1!=null){session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}if(session2!=null){session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}}private int checkWinner(int row, int col) {return 0;}
客户端
客户端要接受消息,我们在initGame里面重写websocket.onmessage方法,这样就可以进行新的处理了
因为这个重写方法是在后面执行的
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;}}function send(row, col) {let data = {message: "putChess",roomId: gameInfo.roomId,row: row,col: col,}websocket.send(JSON.stringify(data));}websocket.onmessage = function (evt) {let data = JSON.parse(evt.data);if(data.message !== "putChess"){alert("对战返回类型错误")return;}if(data.message==="putChess"){//开始下棋,服务端返回数据以后才会进行把棋子弄在浏览器上if(data.userId===gameInfo.thisUserId){oneStep(data.col, data.row, gameInfo.isWhite);}else{oneStep(data.col, data.row, !gameInfo.isWhite);}chessBoard[data.row][data.col] = 1;//表示这个位置已经下过了me = !me;setScreenText(me);//告诉该谁下了if(data.winner!==0){//胜负已分if(data.winner===gameInfo.thisUserId){alert("恭喜你,你赢了!");location.assign("/game_hall.html")}else if(data.winner===gameInfo.thatUserId){alert("很抱歉,你输了!");location.assign("/game_hall.html")} else {alert("未知错误!");location.assign("/game_hall.html")}}}}
测试
异常问题
在afterConnectionClosed和handleTransportError中链接已经断开了,所以不能send了,所以就会出错,所以user为null的时候不要发送数据了,直接打印日志吧
@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {try{User user = (User)session.getAttributes().get("user");//获取用户信息 ,这个能获取多亏了addInterceptors//要区分一下是多开关闭还是手动关闭的,多开关闭的话就什么都不干就是了WebSocketSession tmpSession = onlineUserManager.getGameHall(user.getUserId());if(tmpSession==session){//正常关闭System.out.println("用户:"+user.getUsername()+"离开匹配");matcher.remove(user);onlineUserManager.exitGameHall(user.getUserId());}}catch(NullPointerException e){//因为可能直接没有登录就去匹配了,所以user为null,这样就不行System.out.println("用户未登录,MatchAPI,afterConnectionClosed");}}
手动注入对象
还有一个问题就是在Room类中注入了bean
但是room本身并没有注入bean,所以不能使用bean对象
但是room不能注入bean,因为room不是单例的
怎么办呢
只能手动注入对象了
@SpringBootApplication
public class JavaGobangApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) {context = SpringApplication.run(JavaGobangApplication.class, args);}}
启动类中这样设计,其中context 是run返回的对象,包含所有的bean
而且他是一个静态变量,public的
public Room(){roomId = UUID.randomUUID().toString();onlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);roomManager = JavaGobangApplication.context.getBean(RoomManager.class); }
这样就可以获取room对象了
这样就成功了,就在未注入bean的类中使用bean了
服务端打印棋盘
private void printBoard() {System.out.println("打印棋盘");System.out.println("=======================================================================");for(int i=0;i<MAX_ROW;i++){for(int j=0;j<MAX_COL;j++){System.out.print(board[i][j]+" ");}System.out.println();}System.out.println("=======================================================================");}
判定胜负
private int checkWinner(int row, int col,int chess) {//先判定行是否是五子连珠for(int c = col-4; c<=col; c++){//这个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 (IndexOutOfBoundsException e){//说明数组越界了,越界的就不用判断了continue;}}//判定列是否是五子连珠for(int r = row-4; r<=row; r++){//这个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 (IndexOutOfBoundsException e){//说明数组越界了,越界的就不用判断了continue;}}//判定左斜线是否是五子连珠for(int r = row-4,c=col-4; r<=row && c<=col; r++,c++){//这个r是五子中的最上边的子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 (IndexOutOfBoundsException e){//说明数组越界了,越界的就不用判断continue;}}//判定右斜线是否是五子连珠for(int r = row-4,c=col+4; r<=row && c>=col; r++,c--){//这个r是五子中的最上边的子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 (IndexOutOfBoundsException e){//说明数组越界了,越界的就不用判断continue;}}return 0;}
玩家掉线
在room中要处理玩家掉线的情况,谁掉线谁就输了,然后要告诉另一方胜利
我们前面已经处理了一部分的玩家掉线的情况,就是在下子的时候,可以去判断两方是否掉线
还有一种就是万一还没有下子就掉线了呢
所以还要处理
触发了handleTransportError和afterConnectionClosed就可能是掉线了
我们用方法来处理
@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {try{User user = (User)session.getAttributes().get("user");//防止多开删除原来的sessionWebSocketSession gameRoom = onlineUserManager.getGameRoom(user.getUserId());if(gameRoom== session){noticeThatUser(user);onlineUserManager.exitGameRoom(user.getUserId());}}catch (NullPointerException e){System.out.println("用户未登录,GameAPI,handleTransportError");}}
private void noticeThatUser(User user) {//先查询到roomString roomId = roomManager.getRoomIdByUserId(user.getUserId());if(roomId==null){//说明是正常关闭 ,正常胜利关闭的房间,不是掉线return;}Room room = roomManager.getRoomByRoomID(roomId);if(room==null){//说明是正常关闭 ,正常胜利关闭的房间,不是掉线return;}//不要roomManager.getRoomByRoomID(roomManager.getRoomIdByUserId(user.getUserId())),因为roomId可能为nullSystem.out.println("用户"+user.getUsername()+"掉线了");GameResponse res = new GameResponse();res.setMessage("putChess");res.setUserId(0);res.setRow(0);res.setCol(0);res.setWinner(user==room.getUser1()?room.getUser2().getUserId():room.getUser1().getUserId());//设置胜利者WebSocketSession session = onlineUserManager.getGameRoom(res.getWinner());try {session.sendMessage(new TextMessage(objectMapper.writeValueAsString(res)));//给胜利者发送消息} catch (Exception e) {e.printStackTrace();}//消除房间roomManager.remove(room,room.getUser1().getUserId(),room.getUser2().getUserId());}
分数更新
就是胜利者和失败者要对分数进行更新,在结束对战以后
结束对战,在Room类的putChess和handleTransportError和afterConnectionClosed都可以结束对战的
所以这两个地方都要进行分数更新
<update id="userWin">update user set score = score + 30, totalCount = totalCount + 1, winCount = winCount + 1 where userId = #{userId};</update><update id="userLose">update user set score = score - 30, totalCount = totalCount + 1 where userId = #{userId};</update>
//如果胜负确定,则销毁房间if(response.getWinner()!=0){//更新分数userMapper.userWin(response.getWinner());userMapper.userLose(Objects.equals(response.getWinner(), user1.getUserId()) ?user2.getUserId():user1.getUserId());roomManager.remove(this,user1.getUserId(),user2.getUserId());}
但是这样还不行
@GetMapping("/userInfo")@ResponseBodypublic Object userInfo(HttpServletRequest request){HttpSession session = request.getSession(false);if(session != null){return (User) session.getAttribute("user");}else{return new User();}}
因为前端展示用户基本信息,用的是userInfo这个接口,而userInfo用的是修改之前的session ,所以还要修改一下
@GetMapping("/userInfo")@ResponseBodypublic Object userInfo(HttpServletRequest request){HttpSession session = request.getSession(false);if(session != null){User user = (User) session.getAttribute("user");User newUser = userMapper.selectByName(user.getUsername());session.setAttribute("user", newUser);return (User) session.getAttribute("user");}else{return new User();}}
这样应该就可以了
第五个棋子无法显示
什么意思呢,意思就是在显示胜利的时候,没有把五子连珠的效果显示出来,然后就跳转了,就是这个意思
为什么没有显示出来呢
websocket.onmessage = async function (evt) {console.log("gameInfo.thisUserId:"+gameInfo.thisUserId)console.log("gameInfo.thatUserId:"+gameInfo.thatUserId)let data = JSON.parse(evt.data);console.log("data.winner:"+data.winner)if (data.message !== "putChess") {alert("对战返回类型错误")return;}if (data.message === "putChess") {//开始下棋,服务端返回数据以后才会进行把棋子弄在浏览器上if (data.userId === gameInfo.thisUserId) {oneStep(data.col, data.row, gameInfo.isWhite);} else if(data.userId === gameInfo.thatUserId) {oneStep(data.col, data.row, !gameInfo.isWhite);} else{//表示掉线未下的情况alert("对手掉线了,恭喜你,你赢了!")location.assign("/game_hall.html")return;}chessBoard[data.row][data.col] = 1;//表示这个位置已经下过了me = !me;setScreenText(me);//告诉该谁下了if (data.winner !== 0) {//胜负已分if (data.winner === gameInfo.thisUserId) {alert("恭喜你,你赢了!");location.assign("/game_hall.html")} else if (data.winner === gameInfo.thatUserId) {alert("很抱歉,你输了!");location.assign("/game_hall.html")} else {alert("未知错误!");location.assign("/game_hall.html")}}}}
为什么会这样呢
明明我们是先oneStep,然后在alert,为什么没有先显示五子连珠,而是先
alert,这个是因为oneStep是先把内容画在内存中的,必须等这个函数结束完了,才会显示出来,这个原理就是printf一样,必须先程序结束后,才会打印到屏幕上,一开始是先打印在缓冲区中的
但是,因为alert会阻塞这个函数,然后点击了alert就马上跳转了,所以这个函数结束的时候,如果胜负已分,就是跳转的时候,也是显示五子连珠的时候,显示五子连珠的时间非常短,所以我们就看不到
所以我们的处理方法是不要用alert来表示胜利了,而是用下面的那个文本框来表示胜利还是失败,然后胜负已分的时候在增加一个按钮,来表示可以返回游戏大厅
websocket.onmessage = async function (evt) {console.log("gameInfo.thisUserId:"+gameInfo.thisUserId)console.log("gameInfo.thatUserId:"+gameInfo.thatUserId)let data = JSON.parse(evt.data);console.log("data.winner:"+data.winner)if (data.message !== "putChess") {alert("对战返回类型错误")return;}if (data.message === "putChess") {//开始下棋,服务端返回数据以后才会进行把棋子弄在浏览器上let screen = document.querySelector('#screen');if (data.userId === gameInfo.thisUserId) {oneStep(data.col, data.row, gameInfo.isWhite);} else if(data.userId === gameInfo.thatUserId) {oneStep(data.col, data.row, !gameInfo.isWhite);} else{//表示掉线未下的情况// alert("对手掉线了,恭喜你,你赢了!")// location.assign("/game_hall.html")screen.innerHTML="对手掉线了,恭喜你,你赢了!"//然后在增加一个按钮来返回大厅let backButton = document.createElement('button');backButton.textContent = "返回大厅";backButton.classList.add("return-btn");//便于弄csslet father = document.querySelector('#screen').parentElement; // 获取 screen 的父容器father.appendChild(backButton); // 添加按钮到页面上backButton.onclick = function () {location.assign("/game_hall.html")}return;}chessBoard[data.row][data.col] = 1;//表示这个位置已经下过了me = !me;setScreenText(me);//告诉该谁下了if (data.winner !== 0) {//胜负已分if (data.winner === gameInfo.thisUserId) {// alert("恭喜你,你赢了!");// location.assign("/game_hall.html")screen.innerHTML="恭喜你,你赢了!"} else if (data.winner === gameInfo.thatUserId) {// alert("很抱歉,你输了!");// location.assign("/game_hall.html")screen.innerHTML="很抱歉,你输了!"} else {// alert("未知错误!");// location.assign("/game_hall.html")screen.innerHTML="未知错误!"}//然后在增加一个按钮来返回大厅let backButton = document.createElement('button');backButton.textContent = "返回大厅";backButton.classList.add("return-btn");//便于弄csslet father = document.querySelector('#screen').parentElement; // 获取 screen 的父容器father.appendChild(backButton); // 添加按钮到页面上backButton.onclick = function () {location.assign("/game_hall.html")}}}}
这样就可以了
页面回退问题
我们如果不小心退出room房间了,就是点击了《-
就算是挂机了
但是如果又点击->的话,还是会返回原来的那个页面的了
虽然返回了原来的那个页面,但是挂机已经是事实了,我们看到都已经打印了连接关闭了
虽然返回了room,但是不是匹配进入的room
而是非法点击->返回的
这个是不能正常下棋的
但是我们也不能禁止用户不要点击回退按钮
怎么办呢
先说一下为什么,因为浏览器会把以前访问过的页面缓存下来,所以可以回退,而且这个缓存的页面不会刷新请求和链接的,意思就是这个缓存如果回退过来了是不会重新刷新请求和建立websocket连接的
怎么避免了,我们可以把location.assign改为location.replace
这样就是覆盖了,就不会回退到以前的页面了
所以我们把除了login的跳转的页面的location.assign都改为replace
location.replace('/game_room.html')
这样回退就都会退回到login页面了
额外可以添加的功能
上传到服务器
如果要上传到服务器的话,就只需要修改数据库密码,和websocket建立的连接的ip地址和端口号就可以了
let url = "ws://"+location.host+"/game";
let websocket = new WebSocket(url);
这样就可以了
其中location.host就是我们访问网址的ip地址和端口号
就是这里的东西
总结
gitee:
java_gobang