文章目录
- 网页版在线五子棋
- 1. 项目介绍
- 2. 项目演示
- 3. 前置知识
- 3.1 WebSocket
- 3.2 代码示例
- 3.2.1 服务器代码
- 3.2.2 客户端代码
- 4. 需求分析和概要设计
- 4.1 用户模块
- 4.2 匹配模块
- 4.3 对战模块
- 5. 项目创建
- 6. 实现用户模块
- 6.1 编写数据库代码
- 6.1.1数据库设计
- 6.1.2 配置MyBatis
- 6.1.3 创建实体类
- 6.1.4 创建UserMapper
- 6.1.5 实现UserMapper.xml
- 6.2 约定前后端交互
- 6.3 服务器开发
- 6.4 客户端开发
- 6.4.1 登录页面
- 6.4.2 注册页面
- 7. 实现匹配模块
- 7.1 约定前后端交互接口
- 7.2 客户端开发
- 7.2.1 实现页面基本属性
- 7.2.2实现匹配功能
- 7.3 服务器开发
- 7.3.1 创建并注册MatchAPI类
- 7.3.2 实现用户管理类
- 7.3.3 创建匹配请求/响应对象
- 7.3.4 处理连接成功
- 7.3.5 处理开始匹配/取消匹配
- 7.3.6 实现匹配器
- 7.3.7 实现房间类
- 7.3.8 实现房间管理器类
- 7.3.9 处理连接关闭/异常
- 8. 实现对战模块
- 8.1 约定前后端交互
- 8.2 客户端开发
- 8.2.1 实现棋盘/棋子绘制
- 8.2.2 初始化websocket
- 8.2.3 发送落子请求
- 8.2.4 处理落子响应
- 8.3 服务器开发
- 8.3.1 创建落子请求/响应对象
- 8.3.2 处理连接成功
- 8.3.3 实现通知玩家就绪
- 8.3.4 玩家下线处理
- 8.3.5 手动注入bean
- 8.3.6 处理落子请求
- 8.3.7 实现对弈功能
- 8.3.8 打印棋盘
- 8.3.9 判决胜负
- 8.3.10 处理玩家中途退出
- 9. 部署到云服务器上
- 9.1 增添数据库
- 9.2 微调代码
- 9.3 打包
- 9.4 运行
- 9.5 验证
- 总结
网页版在线五子棋
1. 项目介绍
实现一个网页版在线对战五子棋
支持以下功能:
- 用户模块:用户注册、用户登录、用户天梯积分记录、用户比赛场数记录
- 匹配模块:根据玩家天梯积分进行匹配
- 对战模块:实现1v1的实时对战功能
核心技术:
- Spring/SpringBoot/SpringMVC
- Websocket
- MyBatis
- MySQL
- HTML/CSS/JS/AJAX
2. 项目演示
3. 前置知识
3.1 WebSocket
如果你了解过Http协议,那么应该知道Http协议是无状态、无连接、单向的应用层协议。它采用了请求-响应模式,由客户端发送一个请求,由服务端返回一个响应。它有一个弊端就是服务端无法主动向客户端发起消息。这样就导致客户端想要获取服务端连续的状态变化很困难,大多是web程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
举个例子,我们在餐馆点餐后,有两种选择:
①时不时跑到前台询问老板我的菜做好没,老板说没有,我溜达一圈后又来问菜做好没……循环直到我的菜做好了,我端着菜找个位置坐下用餐
②我直接找个位置坐下,等菜做好后,老板端着菜过来递给我然后用餐
第一种做法(轮询)就是使用客户端(我)一直向服务器(老板)发送请求,检查数据是否发生了变化(菜做好没)。
第二种做法(websocket)就是服务器(老板)直接向客户端(我)发送消息(菜做好了)
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程.
请求头
返回头
3.2 代码示例
Spring中内置了websocket,我们可以直接使用。
3.2.1 服务器代码
创建TestAPI类:
这个类用来处理websocket请求,并返回响应。
每个方法中都带有一个 session 对象, 这个 session 和 Servlet 的 session 并不相同, 而是 WebSocket 内部搞的另外一组 Session.
通过这个 Session 可以给客户端返回数据, 或者主动断开连接.
@Component
/*** 这是一个测试类* 继承自TextWebSocketHandler的类是一个webSocket消息处理类*/
public class TestAPI extends TextWebSocketHandler {@Override//用户建立连接后触发的方法public void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("连接成功");}@Override//收到文本消息后触发的方法protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println("收到消息 : " + message.getPayload());session.sendMessage(new TextMessage("我收到了你的消息" + message.getPayload()));}@Override//触发异常后触发的方法public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("连接异常");}@Override//关闭连接后触发的方法public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("关闭连接");}
}
创建WebSocketConfig类:
@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");}
}
3.2.2 客户端代码
创建test.html
<body><input type="text" id = "message"><input type="button" id = "submit" value="提交"><script>/* 创建一个websocket实例 */let url = "ws://127.0.0.1:8080/test"let websocket = new WebSocket(url)/* 给实例挂载一些回调函数 */websocket.onopen = function() {console.log("建立连接");}websocket.onmessage = function(e) {console.log("收到消息" + e.date);}websocket.onerror = function() {console.log("连接异常");}websocket.onclose = function() {console.log("连接关闭");}let input = document.querySelector('#message');let button = document.querySelector('#submit')button.onclick = function() {console.log("发送消息" + input.value);websocket.send(input.value);}</script>
</body>
启动服务器,观察效果:
这样服务器和客户端就实现了交互~
4. 需求分析和概要设计
整个项目分成以下三个模块
- 用户模块
- 匹配模块
- 对战模块
4.1 用户模块
该模块主要用于用户登录、注册、记录一些用户比赛信息。
用MySQL存储数据。
客户端提供登录注册页面。
服务器基于Spring + MyBatis来实现增删查改。
4.2 匹配模块
用户登录成功,进入游戏大厅,大厅里显示玩家的比赛信息。
同时显示一个匹配按钮,当玩家按下开始匹配,将玩家加入匹配队列,同时开始匹配变为匹配中……(点击停止)停止匹配后从队列中将玩家移除。
如果匹配成功,将进入游戏房间。
通过websocket实现通讯“开始匹配”、“停止匹配”、“匹配成功”。
4.3 对战模块
玩家匹配成功,则进入游戏房间界面
每两个玩家在同一个游戏房间
在游戏房间中显示棋盘,玩家点解棋盘实现落子功能
当五子连珠时,显示你赢了/你输了
页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.
- 准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
- 落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
- 胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.
5. 项目创建
使用idea创建一个SpringBoot项目
引入SpringBoot / Spring MVC / MyBatis /lombok依赖
6. 实现用户模块
6.1 编写数据库代码
6.1.1数据库设计
create database if not exists java_gobang;use java_gobang;drop table if exists user;
create table user (userId int primary key auto_increment,username varchar(50) unique ,password varchar(50),score int, -- 天梯积分totalCount int, -- 比赛总场数winCount int -- 获胜场数
);insert into user values(null,'zhangsan','123',1000,0,0);
insert into user values(null,'lisi','123',1000,0,0);
insert into user values(null,'wangwu','123',1000,0,0);
6.1.2 配置MyBatis
编写application.yml
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivermybatis:mapper-locations: classpath:mapper/**Mapper.xml
6.1.3 创建实体类
@Data
public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;
}
6.1.4 创建UserMapper
此类主要提供4个方法:
- selectByName : 根据用户名查找用户信息,实现登录
- insert :根据信息新增用户,用于注册
- userWin :给获胜者修改游戏分数
- userLose:给失败者修改游戏分数
@Mapper
public interface UserMapper {int insert(User user);User selectByName(String name);void userWin(int userId);void userLose(int userId);
}
6.1.5 实现UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.model.UserMapper"><insert id="insert">insert into user values(null, #{username}, #{password}, 1000, 0, 0);</insert><select id="selectByName" resultType="com.example.java_gobang.model.User">select * from user where username = #{username};</select><update id="userWin">update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30where userId = #{userId}</update><update id="userLose">update user set totalCount = totalCount + 1, score = score - 30where userId = #{userId}</update>
</mapper>
6.2 约定前后端交互
6.3 服务器开发
创建controller.UserController
实现三个方法:
- login :实现用户登录逻辑
- register : 实现用户注册逻辑
- userInfo:实现登录成功后查找用户分数逻辑
@RestController
//这个类用来实现三个方法
//①注册 ②登录 ③获取用户信息
public class UserController {@Autowiredprivate UserMapper userMapper;@PostMapping("/login")public Object login(String username, String password, HttpServletRequest req){User user = userMapper.selectByName(username);if(user == null || !user.getPassword().equals(password)){return new User();}System.out.println("登录" + username);HttpSession session = req.getSession(true);session.setAttribute("user",user);return user;}@PostMapping("/register")public Object register(String username,String password){User user = null;try {user = new User();user.setUsername(username);user.setPassword(password);System.out.println("register" + username);int ret = userMapper.insert(user);System.out.println("受影响的行数" + ret);//可能会触发一个主键重复的异常}catch (org.springframework.dao.DuplicateKeyException e){user = new User();//System.out.println("用户名重复");}return user;}@GetMapping("/userInfo")public Object getUserInfo(HttpServletRequest req){try{HttpSession session = req.getSession(false);User user = (User) session.getAttribute("user");//保证用户的分数是数据库中最新的数据User newUser = userMapper.selectByName(user.getUsername());return newUser;}catch (NullPointerException e){return new User();}}
}
6.4 客户端开发
6.4.1 登录页面
创建login.html
<!-- 导航栏 --><div class="nav"><span>五子棋对战</span></div><div class="login-container"><div class="login-dialog"><!-- 标题 --><h2>登录</h2><div class="row"><span>用户名</span><input type="text" id = "username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row-button"><button id="submit">提交</button></div><div class="register"><a href="register.html">注册</a></div></div></div>
创建css.common.css
html,body {height: 100%;background-image: url(../img/背景.jpg);background-size: cover;background-repeat: no-repeat;background-position: center;
}.nav{width: 100%;height: 50px;display: flex;background-color: rgba(51, 51, 51,0.4);color: white;padding-left: 20px;align-items: center;
}.container{height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;width: 100%;
}
创建css.login.css
.login-container{height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;width: 100%;
}.login-dialog{width: 400px;height: 320px;background-color: rgba(255,255,255,0.8);border-radius: 10px;
}.login-dialog h2{text-align: center;padding: 20px 0;
}.login-dialog .row{width: 100%;height: 50px;align-items: center;justify-content: center;display: flex;
}.login-dialog span{width: 100px;display: block;/* 字体加粗 */font-weight: 700;
}.row #username,#password{outline: none;border: none;width: 200px;height: 40px;font-size: 20px;text-indent: 10px;border-radius: 10px;
}.login-dialog .row-button{margin-top: 10px;
}.row-button #submit{width: 300px;border: none;height: 50px;color: white;background-color: rgb(0, 128, 0);font-size: 20px;border-radius: 10px;margin-left: 50px;
}.register a{align-items: center;margin-left: 50px;text-decoration: none;
}#submit:active{background-color: #666;
}
在login.html
中编写js代码,实现交互
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><script>let submit = document.querySelector('#submit');submit.onclick = function(){let username = document.querySelector('#username').value;let password = document.querySelector('#password').value;$.ajax({method:"post",url:"/login",data:{username : username,password : password},success: function(data){console.log(JSON.stringify(data));if(data && data.userId > 0){alert("登录成功");location.assign('game_hall.html');}else{alert("登录失败! 用户名或密码错误!")}}})}</script>
6.4.2 注册页面
创建register.html
<!-- 导航栏 --><div class="nav"><span>五子棋对战</span></div><div class="register-container"><div class="register-dialog"><!-- 标题 --><h2>注册</h2><div class="row"><span>用户名</span><input type="text" id = "username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row-button"><button id="submit">提交</button></div></div></div>
css部分可以使用css.common.css部分
在register.html
中编写js代码实现交互
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><script>let submit = document.querySelector('#submit');submit.onclick = function(){let username = document.querySelector('#username').value;let password = document.querySelector('#password').value;$.ajax({method:"post",url:"/register",data:{username: username,password: password},success: function(data){console.log(JSON.stringify(data));if(data && data.username){alert("注册成功");location.assign('login.html');}else{alert("注册失败")}}})}</script>
7. 实现匹配模块
7.1 约定前后端交互接口
7.2 客户端开发
7.2.1 实现页面基本属性
创建 game_hall.html
screen用于显示玩家分数
button作为匹配按钮
<div class="nav">五子棋对战</div><div class="container"><!-- 这个用来存放用户的比赛信息 --><div><div id="screen"></div><button id="match-button">开始匹配</button></div></div>
创建game_hall.css
#screen {width: 400px;height: 200px;font-size: 20px;background-color: gray;color: white;border-radius: 10px;text-align: center;line-height: 100px;
}#match-button {width: 400px;height: 50px;font-size: 20px;color: white;background-color: orange;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;
}#match-button:active {background-color: gray;
}
编写js代码获取用户信息
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script><script>/* 获取用户信息 */$.ajax({method: 'get',url: '/userInfo',success: function(data) {let screen = document.querySelector('#screen');if(data.username == null){alert("当前尚未登录,请先登录!");location.replace("/login.html");}screen.innerHTML = '玩家: ' + data.username + ', 分数: ' + data.score + "<br> 比赛场次: " + data.totalCount + ", 获胜场次: " + data.winCount;}});
7.2.2实现匹配功能
编辑 game_hall.html
的 js 部分代码.
- 点击匹配按钮, 就会进入匹配逻辑. 同时按钮上提示 “匹配中……(点击停止)” 字样.
- 再次点击匹配按钮, 则会取消匹配.
- 当匹配成功后, 服务器会返回匹配成功响应, 页面跳转到 game_room.html
/* 处理匹配功能 */let url = 'ws://' + location.host + '/findMatch';let websocket = new WebSocket(url);let button = document.querySelector('#match-button');/* 点击开始匹配 */button.onclick = function(){/* 这个可以判断websocket是否处于连接状态OPEN是一个常数1 ,readstate=1代表连接状态 */if(websocket.readyState == websocket.OPEN){if(button.innerHTML == '开始匹配'){console.log("开始匹配");/* JSON对象转为字符串 */websocket.send(JSON.stringify({message:'startMatch',}));}else if(button.innerHTML == '匹配中……(点击停止)'){console.log("停止匹配");websocket.send(JSON.stringify({message:'stopMatch',}));}}else{console.log("当前你的连接已经断开,请重新连接");location.replace('/login.html');}}/* 处理服务器的响应 *//* 这个函数是当收到来自服务器的消息时调用的 */websocket.onmessage = function(e){/* 字符串转为JSON对象 */let resp = JSON.parse(e.data);if(!resp.ok){console.log("游戏大厅发生错误" + resp.reason);location.replace('/login.html');return;}if(resp.message == 'startMatch'){console.log("进入匹配队列成功");button.innerHTML = '匹配中……(点击停止)'}else if(resp.message == 'stopMatch'){console.log("移除匹配队列成功");button.innerHTML = '开始匹配';}else if(resp.message == 'MatchSuccess'){console.log("匹配成功,进入游戏界面");location.replace('/game_room.html');}else if(resp.message == 'repeatConnection'){alert("检测到当前为多开,请使用其他账号登录");location.replace("/login.html");}else{console.log("非法的message" + resp.message);}}/* 监听窗口关闭事件,当窗口关闭时,主动断开websocket链接,防止还没断开链接就关闭窗口server报错 */window.onbeforeunload = function () {websocket.close();}
7.3 服务器开发
7.3.1 创建并注册MatchAPI类
创建 api.MatchAPI
, 继承自 TextWebSocketHandler 作为处理 websocket 请求的入口类.
@Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Componentpublic class MatchAPI extends TextWebSocketHandler {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}
修改 config.WebSocketConfig
, 把 MatchAPI 注册进去.
@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");//拦截器,可以获取到HttpSession中的session供webSocket中的session使用registry.addHandler(matchAPI,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());}
}
7.3.2 实现用户管理类
创建 game.OnlineUserManager
类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.
- 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
- 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
- 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.
由于存在两个页面, 游戏大厅和游戏房间, 使用两个 哈希表 来分别存储两部分的会话.
涉及线程安全使用ConcurrentHashMap哈希表
@Component
//这个类用来管理用户的在线状态
public class OnlineUserManager {private ConcurrentHashMap<Integer, WebSocketSession> game_hall = new ConcurrentHashMap<>();private ConcurrentHashMap<Integer, WebSocketSession> game_room = new ConcurrentHashMap<>();//用户进入游戏大厅public void enterGameHall(int userId,WebSocketSession session){game_hall.put(userId,session);}//用户离开游戏大厅public void exitGameHall(int userId){game_hall.remove(userId);}//获取用户信息public WebSocketSession getGameHallSession(int userId){return game_hall.get(userId);}//用户进入游戏房间public void enterGameRoom(int userId,WebSocketSession session){game_room.put(userId,session);}//用户离开游戏房间public void exitGameRoom(int userId){game_room.remove(userId);}//获取用户信息public WebSocketSession getGameRoomSession(int userId){return game_room.get(userId);}}
7.3.3 创建匹配请求/响应对象
创建 game.MatchRequest
类
@Data
public class MatchRequest {private String message = "";
}
创建 game.MatchResponse
类
@Data
public class MatchResponse {private boolean ok = true;private String reason = "";private String message = "";
}
7.3.4 处理连接成功
实现MatchAPI中的afterConnectionEstablished方法
@Override//处理用户连接public void afterConnectionEstablished(WebSocketSession session) throws Exception {//session.getAttributes()获取到的是一个map,里面存放了了HttpSession中的getAttribute里的所有对象User user = (User) session.getAttributes().get("user");if(user == null){//玩家还未登陆就进入游戏大厅了MatchResponse response = new MatchResponse();response.setOk(false);response.setReason("[afterConnectionEstablished]玩家尚未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}//检查玩家的上线状态(是否多开)//在给玩家设置上线状态时,需要先判断之前玩家是否已经登录过了if (onlineUserManager.getGameHallSession(user.getUserId()) != null|| onlineUserManager.getGameRoomSession(user.getUserId()) != null){MatchResponse response = new MatchResponse();response.setOk(true);response.setReason("当前游戏禁止多开");response.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}//当玩家获取到身份信息后,就可以给玩家设置上线状态了onlineUserManager.enterGameHall(user.getUserId(),session);System.out.println("当前玩家" + user.getUsername() + "进入游戏大厅");}
7.3.5 处理开始匹配/取消匹配
实现MatchAPI中的 handleTextMessage
@Override//处理开始/取消匹配protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){System.out.println("[handleTextMessage]玩家尚未登录");return;}System.out.println("开始匹配" + user.getUserId() + "message" + message.toString());//将解析得到的JSON请求数据转换为一个MatchRequest对象MatchRequest request = objectMapper.readValue(message.getPayload(),MatchRequest.class);MatchResponse response = new MatchResponse();if(request.getMessage().equals("startMatch")){//加入匹配器中//TODOmatch.add(user);response.setMessage("startMatch");}else if(request.getMessage().equals("stopMatch")){//从匹配器中移除//TODOmatch.remove(user);response.setMessage("stopMatch");}else{response.setOk(false);response.setReason("非法的匹配请求");}session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}
7.3.6 实现匹配器
创建 game.Matcher
类.
涉及线程安全需处理
@Component
//匹配器
public class Match {@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate RoomManager roomManager;private ObjectMapper objectMapper = new ObjectMapper();//游戏玩家分为三档//第一档://2000以下(不含2000)//第二档://2000-3000(不含3000)//第三档://3000以上private Queue<User> normalQueue = new LinkedList<>();private Queue<User> highQueue = new LinkedList<>();private Queue<User> veryHighQueue = new LinkedList<>();public void add(User user){if(user.getScore() < 2000){synchronized (normalQueue){normalQueue.offer(user);normalQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入normalQueue");}else if(user.getScore() >= 2000 && user.getScore() < 3000){synchronized (highQueue){highQueue.offer(user);highQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入highQueue");}else{synchronized (veryHighQueue){veryHighQueue.offer(user);veryHighQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入veryHighQueue");}}public void remove(User user){if(user.getScore() < 2000){synchronized (normalQueue){normalQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出normalQueue");}else if(user.getScore() >= 2000 && user.getScore() < 3000){synchronized (highQueue){highQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出highQueue");}else{synchronized (veryHighQueue){veryHighQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出veryHighQueue");}}//启动三个线程循环调用各自的队列private Match(){new Thread(){@Overridepublic void run() {while(true){handlerMatch(normalQueue);}}}.start();new Thread(){@Overridepublic void run() {while(true){handlerMatch(highQueue);}}}.start();new Thread(){@Overridepublic void run() {while(true){handlerMatch(veryHighQueue);}}}.start();}private void handlerMatch(Queue<User> matchQueue){synchronized (matchQueue){try{//五子棋需要两个人,当队列中人数少于2时等待while(matchQueue.size() < 2){matchQueue.wait();}User user1 = matchQueue.poll();User user2 = matchQueue.poll();System.out.println("匹配出两个玩家" + user1.getUsername() +" " + user2.getUsername());WebSocketSession session1 = onlineUserManager.getGameHallSession(user1.getUserId());WebSocketSession session2 = onlineUserManager.getGameHallSession(user2.getUserId());if(session1 == null){matchQueue.offer(user2);return;}if(session2 == null){matchQueue.offer(user1);return;}//防止多开if (session1 == session2){matchQueue.add(user1);}// 将两个玩家加入对战房间Room room = new Room();roomManager.add(user1.getUserId(),user2.getUserId(),room);//给玩家1发送匹配成功的信息MatchResponse response1 = new MatchResponse();response1.setOk(true);response1.setMessage("MatchSuccess");session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response1)));//给玩家2发送匹配成功的信息MatchResponse response2 = new MatchResponse();response2.setOk(true);response2.setMessage("MatchSuccess");session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response2)));}catch (IOException | InterruptedException e){e.printStackTrace();}}}
}
7.3.7 实现房间类
匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中.
创建 game.Room
类
- 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
- 房间内要记录对弈的玩家双方信息.
- 记录先手方的 ID
- 记录一个 二维数组 , 作为对弈的棋盘.
- 记录一个 OnlineUserManager, 以备后面和客户端进行交互.
@Data
public class Room {//由于Room不能是唯一的,所以不能注入到Spring中,从而也不可以用 Autowired注入这三个bean//因此我们需要手动注入这三个bean后续会说怎么处理private OnlineUserManager onlineUserManager;private RoomManager roomManager;private UserMapper userMapper;private ObjectMapper objectMapper = new ObjectMapper();private String roomId;private User user1;private User user2;// 先手方的用户 idprivate int whiteUserId = 0;// 棋盘, 数字 0 表示未落子位置. 数字 1 表示玩家 1 的落子. 数字 2 表示玩家 2 的落子private static final int MAX_ROW = 15;private static final int MAX_COL = 15;private int[][] chessBoard = new int[MAX_ROW][MAX_COL];public Room() {// 使用 uuid 作为唯一身份标识roomId = UUID.randomUUID().toString();}
7.3.8 实现房间管理器类
Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象.需要一个管理器对象来管理所有的 Room.
创建 game.RoomManager
类
- 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
- 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
- 提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).
@Component
public class RoomManager {//存储所有的Room房间ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();//存储用户和房间的关联关系ConcurrentHashMap<Integer ,String> userIdToRoomId = new ConcurrentHashMap<>();public void add(int user1Id,int user2Id,Room room){rooms.put(room.getRoomId(),room);userIdToRoomId.put(user1Id,room.getRoomId());userIdToRoomId.put(user2Id,room.getRoomId());}public void remove(int user1Id,int userId2,String roomId){rooms.remove(roomId);userIdToRoomId.remove(user1Id);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){return null;}return getRoomByRoomId(roomId);}
}
7.3.9 处理连接关闭/异常
实现MatchAPI中的afterConnectionClosed
@Override//异常连接处理public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");try{WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());if(tmpSession == session){onlineUserManager.exitGameHall(user.getUserId());}//TODO 从匹配器中移除match.remove(user);System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");}catch (NullPointerException e){System.out.println("[handleTransportError]当前用户尚未登录");}}@Override//处理玩家断开连接public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");try{WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());if(tmpSession == session){onlineUserManager.exitGameHall(user.getUserId());}//TODO 从匹配器中移除match.remove(user);System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");}catch (NullPointerException e){System.out.println("[afterConnectionClosed]当前用户尚未登录");}}
8. 实现对战模块
8.1 约定前后端交互
8.2 客户端开发
创建 game_room.html
, 表示对战页面.
<div class="nav">联机五子棋</div><div class="container"><div><canvas id="chess" width="450px" height="450px"></canvas><div id="screen">等待玩家连接中...</div></div></div><script src="js/script.js"></script>
创建 css/game_room.css
#screen {font-size: 22px;text-align: center;background-color: rgba(255,255,255,0,7);color: yellow;margin-bottom: 20px;
}
8.2.1 实现棋盘/棋子绘制
创建 js/script
这段代码可以直接复制粘贴,不需要深究其中含义
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;}}// TODO 实现发送落子请求逻辑, 和处理落子响应逻辑.
}initGame();
8.2.2 初始化websocket
在刚才代码中加入websocket
//使用location.host 是为了后续部署到云服务器上做准备的
//也可写作127.0.0.1:8080
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.areplacessign("/game_hall.html");return;}if (resp.message == 'readyGame') {gameInfo.roomId = resp.roomId;gameInfo.thisUserId = resp.thisUserId;gameInfo.thatUserId = resp.thatUserId;gameInfo.isWhite = (resp.whiteUserId == resp.thisUserId);// 初始化棋盘initGame();// 设置显示区域的内容setScreenText(gameInfo.isWhite);} else if (resp.message == 'repeatConnection') {alert("检测到游戏多开! 请使用其他账号登录!");location.replace("/login.html");}
}
8.2.3 发送落子请求
修改刚刚的onclick方法
注释掉原有的 onStep 和 修改 chessBoard 的操作, 放到接收落子响应时处理.
实现 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) {// 发送坐标给服务器, 服务器要返回结果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));}
8.2.4 处理落子响应
在 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.style.backgroundColor = "green";backBtn.style.width = "450px";backBtn.style.height = "50px";backBtn.style.border = "none";backBtn.style.borderRadius = "10px";backBtn.onclick = function() {location.replace('/game_hall.html');}let fatherDiv = document.querySelector('.container>div');fatherDiv.appendChild(backBtn);}}
8.3 服务器开发
创建 api.GameAPI
, 处理 websocket 请求.
@Component
public class GameAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate RoomManager roomManager;// 这个是管理 game 页面的会话@Autowiredprivate OnlineUserManager onlineUserManager;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}
修改 WebSocketConfig
, 将 GameAPI 进行注册.
@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");//拦截器,可以获取到HttpSession中的session供webSocket中的session使用registry.addHandler(matchAPI,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());registry.addHandler(gameAPI,"/game").addInterceptors(new HttpSessionHandshakeInterceptor());}
}
8.3.1 创建落子请求/响应对象
创建game.GameRequest
@Data
public class GameRequest {private String message = "pusChess";private int userId;private int row;private int col;
}
创建game.GameResponse
@Data
public class GameResponse {private String message = "putChess";private int userId;private int row;private int col;private int winner;//获胜者id
}
创建 game.GameReadyResponse
类
@Data
public class GameReadyResponse {private String message = "readyGame";private boolean ok = true;private String reason;private String roomId;private int thisUserId = 0;private int thatUserId = 0;private int whiteUserId = 0;
}
8.3.2 处理连接成功
实现 GameAPI
的 afterConnectionEstablished 方法.
@Override//处理用户连接房间成功public void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp = new GameReadyResponse();User user = (User) session.getAttributes().get("user");if(user == null){resp.setOk(false);resp.setReason("[afterConnectionEstablished]当前用户尚未登录");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}Room room = roomManager.getRoomByUserId(user.getUserId());if(room == null){resp.setOk(false);resp.setReason("用户匹配尚未成功,不能开始游戏");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}System.out.println("游戏连接 roomId = " + room.getRoomId() + " userID = " + user.getUserId());//判断游戏是否多开if(onlineUserManager.getGameHallSession(user.getUserId()) != null ||onlineUserManager.getGameRoomSession(user.getUserId()) != null){resp.setOk(false);resp.setReason("当前游戏禁止多开");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//更新用户会话//游戏大厅和游戏房间的会话是不一样的onlineUserManager.enterGameRoom(user.getUserId(),session);//一个房间有两个玩家,因此使用时需要考虑到线程安全synchronized (room){//设置use1为先手if(room.getUser1() == null){room.setUser1(user);room.setWhiteUserId(user.getUserId());System.out.println("玩家1" + user.getUsername() + "准备就绪");return;}if(room.getUser2() == null){room.setUser2(user);System.out.println("玩家2" + user.getUsername() + "准备就绪");//通知玩家1\2\游戏就绪了notifyGameReady(room,room.getUser1().getUserId(),room.getUser2().getUserId());notifyGameReady(room,room.getUser2().getUserId(),room.getUser1().getUserId());return;}resp.setOk(true);resp.setReason("房间已经满了");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}}
8.3.3 实现通知玩家就绪
private void notifyGameReady(Room room,int thisUserId,int thatUserId) throws IOException {GameReadyResponse response = new GameReadyResponse();response.setOk(true);response.setThisUserId(thisUserId);response.setThatUserId(thatUserId);response.setWhiteUserId(room.getWhiteUserId());WebSocketSession session = onlineUserManager.getGameRoomSession(thisUserId);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}
8.3.4 玩家下线处理
也要注意多开
@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}WebSocketSession session1 = onlineUserManager.getGameRoomSession(user.getUserId());if(session1 != session){System.out.println("当前会话不是游戏中玩家的会话");return;}System.out.println("连接出错 userId = " + user.getUserId());onlineUserManager.exitGameRoom(user.getUserId());noticeThatUserWin(user);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}WebSocketSession session1 = onlineUserManager.getGameRoomSession(user.getUserId());if(session1 != session){System.out.println("当前会话不是游戏中玩家的会话");return;}System.out.println("用户退出 userId = " + user.getUserId());onlineUserManager.exitGameRoom(user.getUserId());noticeThatUserWin(user);}
8.3.5 手动注入bean
在启动类中加入这个
修改room
8.3.6 处理落子请求
@Override//落子请求protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}Room room = roomManager.getRoomByUserId(user.getUserId());room.putChess(message.getPayload());}
8.3.7 实现对弈功能
实现 room 中的 putChess 方法.
//用这个方法实现落子响应public void putChess(String message) throws IOException {GameRequest request = new GameRequest();GameResponse response = new GameResponse();request = objectMapper.readValue(message,GameRequest.class);int row = request.getRow();int col = request.getCol();//判断是谁下的字//做出约定://①如果是玩家一,则下的子为1,//②是玩家而,则下的子是2int chess = request.getUserId() == user1.getUserId() ? 1 : 2;if(chessBoard[row][col] != 0){System.out.println("下的子有误" + request);return;}//1.进行落子chessBoard[row][col] = chess;printBoard();//2.检查游戏是否结束int winner = checkWinner(chess,row,col);System.out.println(winner);//3.把响应写回给玩家response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//4.检查玩家的在线状态WebSocketSession session1 = onlineUserManager.getGameRoomSession(user1.getUserId());WebSocketSession session2 = onlineUserManager.getGameRoomSession(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掉线");}//传回响应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){//更新数据userMapper.userWin(response.getWinner() == user1.getUserId() ? user1.getUserId() : user2.getUserId());userMapper.userLose(response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId());//销毁房间roomManager.remove(user1.getUserId(),user2.getUserId(),roomId);System.out.println("游戏结束,房间已销毁 roomId" + roomId + "获胜方" + response.getWinner());}}
8.3.8 打印棋盘
实现room中的PrintBoard
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(chessBoard[r][c] + " ");}System.out.println();}System.out.println("------------------------");}
8.3.9 判决胜负
实现room中的checkWinner
这个方法其实很简单
(假设为行,其余三种也是一样)当出现五子连珠时,这最后一步肯定在这个五个子中
的一个,那么我们只需判断每次落子后左边4个和右边4个是否和自己颜色一样即可。
private int checkWinner(int chess, int row, int col) {// 以 row, col 为中心for (int c = col - 4; c <= col; c++) {// 针对其中的一种情况, 来判定这五个子是不是连在一起了~// 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)try {if (chessBoard[row][c] == chess&& chessBoard[row][c + 1] == chess&& chessBoard[row][c + 2] == chess&& chessBoard[row][c + 3] == chess&& chessBoard[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 (chessBoard[r][col] == chess&& chessBoard[r + 1][col] == chess&& chessBoard[r + 2][col] == chess&& chessBoard[r + 3][col] == chess&& chessBoard[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 (chessBoard[r][c] == chess&& chessBoard[r + 1][c + 1] == chess&& chessBoard[r + 2][c + 2] == chess&& chessBoard[r + 3][c + 3] == chess&& chessBoard[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 (chessBoard[r][c] == chess&& chessBoard[r + 1][c - 1] == chess&& chessBoard[r + 2][c - 2] == chess&& chessBoard[r + 3][c - 3] == chess&& chessBoard[r + 4][c - 4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {continue;}}// 胜负未分, 就直接返回 0 了.return 0;
8.3.10 处理玩家中途退出
在 GameAPI 中
//如果玩家掉线通知对手获胜private void noticeThatUserWin(User user) throws IOException {Room room = roomManager.getRoomByUserId(user.getUserId());if(room == null){System.out.println("房间已经释放,无需通知");return;}User thatUser = room.getUser1() == user ? room.getUser2() : room.getUser1();WebSocketSession session = onlineUserManager.getGameRoomSession(thatUser.getUserId());if(session == null){//这情况意味着对手也掉线了System.out.println("该玩家已掉线,无需通知");return;}//发送响应通知对手GameResponse response = new GameResponse();response.setUserId(thatUser.getUserId());response.setWinner(thatUser.getUserId());session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));//更新玩家分数userMapper.userWin(thatUser.getUserId());userMapper.userLose(user.getUserId());//销毁房间roomManager.remove(user.getUserId(),thatUser.getUserId(),room.getRoomId());System.out.println("游戏结束,房间已销毁 roomId" + room.getRoomId() + "获胜方" + user.getUserId());}
9. 部署到云服务器上
9.1 增添数据库
将我们写的db.sql直接复制到云服务器上。
9.2 微调代码
9.3 打包
通过maven打包
9.4 运行
使用java -jar + 包名即可
9.5 验证
总结
此项目中包含了许多问题,如多开账号的处理、玩家突然掉线的处理、玩家按了回退之后的处理、多线程下线程安全的问题……但是作为一个项目来说,功能还是不太全面,后续预计将进行改善增添功能如:玩家观战、生成对局回放、生成AI对手等等,现在时间较紧迫,只能先做出这几个功能。