网页版在线五子棋
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 {
@Autowired
private TestAPI testAPI;
@Override
public 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%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
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 + 30
where userId = #{userId}
</update>
<update id="userLose">
update user set totalCount = totalCount + 1, score = score - 30
where userId = #{userId}
</update>
</mapper>
6.2 约定前后端交互
6.3 服务器开发
创建controller.UserController
实现三个方法:
- login :实现用户登录逻辑
- register : 实现用户注册逻辑
- userInfo:实现登录成功后查找用户分数逻辑
@RestController
//这个类用来实现三个方法
//①注册 ②登录 ③获取用户信息
public class UserController {
@Autowired
private 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();
@Component
public class MatchAPI extends TextWebSocketHandler {
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
}
修改 config.WebSocketConfig
, 把 MatchAPI 注册进去.
@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestAPI testAPI;
@Autowired
private MatchAPI matchAPI;
@Autowired
private GameAPI gameAPI;
@Override
public 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")){
//加入匹配器中
//TODO
match.add(user);
response.setMessage("startMatch");
}else if(request.getMessage().equals("stopMatch")){
//从匹配器中移除
//TODO
match.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 {
@Autowired
private OnlineUserManager onlineUserManager;
@Autowired
private 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(){
@Override
public void run() {
while(true){
handlerMatch(normalQueue);
}
}
}.start();
new Thread(){
@Override
public void run() {
while(true){
handlerMatch(highQueue);
}
}
}.start();
new Thread(){
@Override
public 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;
// 先手方的用户 id
private 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 为 true
function 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();
@Autowired
private RoomManager roomManager;
// 这个是管理 game 页面的会话
@Autowired
private OnlineUserManager onlineUserManager;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
}
修改 WebSocketConfig
, 将 GameAPI 进行注册.
@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestAPI testAPI;
@Autowired
private MatchAPI matchAPI;
@Autowired
private GameAPI gameAPI;
@Override
public 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 玩家下线处理
也要注意多开
@Override
public 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);
}
@Override
public 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,
//②是玩家而,则下的子是2
int 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对手等等,现在时间较紧迫,只能先做出这几个功能。