有没有给宝宝做辅食的网站,wordpress q a插件,电子商务系统的开发方式,wordpress插件 flyzoo目录 一、项目背景 用户模块 匹配模块 对战模块 二、核心技术 三、相关知识 WebSocket 原理 报文格式 代码 服务器代码 客户端代码 四、项目创建 4.1、实现用户模块 编写数据库代码 数据库设计 配置MyBatis 创建实体类 创建UserMapper 创建UserMapper接口 实现UserMapper.xml 前… 目录 一、项目背景 用户模块 匹配模块 对战模块 二、核心技术 三、相关知识 WebSocket 原理 报文格式 代码 服务器代码 客户端代码 四、项目创建 4.1、实现用户模块 编写数据库代码 数据库设计 配置MyBatis 创建实体类 创建UserMapper 创建UserMapper接口 实现UserMapper.xml 前后端接口交互 登录接口 请求 响应 注册接口 请求 响应 获取用户信息 请求 响应 服务器开发 客户端开发 登录页面 注册页面 4.2、实现匹配模块 前后端接口交互 客户端开发 实现页面基本结构 实现匹配功能 服务器开发 创建并注册MatchAPI类 实现用户管理器 创建匹配请求/响应对象 处理上线下线状态 处理开始匹配/取消匹配请求 实现匹配器 创建房间类 实现房间管理器 实现匹配器 4.3、实现对战模块 前后端交互接口 客户端开发 实现页面基本结构 实现棋盘绘制 初始化websocket 发送落子请求 处理落子响应 服务器开发 创建落子请求/响应对象 处理连接成功 玩家下线的处理 处理落子请求 修改Room类 实现对弈功能 处理途中玩家掉线 更新玩家分数 五、部署云服务器 构造数据库中的数据 调整websocket建立连接的url 打包上传 通过外网访问 六、后续扩展功能 计时 保存棋谱/录像回放 观战功能 界面聊天 人机对战 一、项目背景 用户模块 用户的注册和登录 管理用户的天梯分数比赛场数获胜场数等信息 匹配模块 依据用户的天梯积分实现匹配机制 对战模块 把两个匹配到的玩家放到一个游戏房间中对方通过网页的形式来进行对战比赛 二、核心技术 Spring/SpringBoot/SpringMVC WebSocket MySQL MyBatis HTML/CSS/JS/Ajax 三、相关知识 WebSocket 原理 WebSocket使得客户端和服务器之间的数据交换变得更加简单允许服务端主动向客户端推送数据。在WebSocket API中浏览器和服务器只需要完成一次握手两者之间就直接可以创建持久性的连接并进行双向数据传输。 报文格式 代码 spring内置websocket可以直接进行使用 服务器代码 新建api.TestAPI类 用来处理websocket请求并返回响应websocket内置一组session通过这个session可以给客户端返回数据或者主动断开连接
Component
public class TestAPI extends TextWebSocketHandler {Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println(连接成功);}Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println(接收消息message.getPayload());}Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println(连接异常);}Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println(连接关闭);}
} 创建config.WebSocketConfig类 这个类用来配置请求路径和TextWebSocketHandler之间的关系
Configuration
EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {Autowiredprivate TestAPI testAPI;Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(testAPI,/test);}
} 客户端代码
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0titleTestAPI/title
/head
bodyinput typetext idmessagebutton idsubmit提交/buttonscript//创建websocket实例let websocketnew WebSocket(ws://127.0.0.1:8080/test);//给实例挂一些回调函数websocket.onopenfunction(){console.log(建立连接);}websocket.onmessagefunction(e){console.log(收到消息e.data);}websocket.onerrorfunction(){console.log(连接异常);}websocket.onclosefunction(){console.log(连接关闭);}//实现点击按钮后通过websocket发送请求let inputdocument.querySelector(#message);let buttondocument.querySelector(#submit);button.onclickfunction(){console.log(发送消息input.value);websocket.send(input.value);}/script
/body
/html 四、项目创建 4.1、实现用户模块 编写数据库代码 数据库设计 创建user表表示用户信息和分数信息
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 value(null,baekhyun,2012,1000,0,0);
insert into user value(null,DO,2012,1000,0,0);
insert into user value(null,sehun,2012,1000,0,0);
insert into user value(null,sohu,2012,1000,0,0);
insert into user value(null,chanyeol,2012,1000,0,0);
insert into user value(null,kai,2012,1000,0,0); 配置MyBatis 创建application.yml
# 配置数据库的连接字符串
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncodingutf8useSSLfalseusername: rootpassword: 19930112driver-class-name: com.mysql.cj.jdbc.Driver#
mybatis:mapper-locations: classpath:mapper/**Mapper.xml 创建实体类
public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;public int getUserId() {return userId;}public void setUserId(int userId) {this.userId userId;}public String getUsername() {return username;}public void setUsername(String username) {this.username username;}public String getPassword() {return password;}public void setPassword(String password) {this.password password;}public int getScore() {return score;}public void setScore(int score) {this.score score;}public int getTotalCount() {return totalCount;}public void setTotalCount(int totalCount) {this.totalCount totalCount;}public int getWinCount() {return winCount;}public void setWinCount(int winCount) {this.winCount winCount;}
} 创建UserMapper 创建UserMapper接口
package com.example.java_gobang.model;Mapper
public interface UserMapper {//根据用户名来查询用户的信息用于登录功能User selectByName(String username);//往数据库里插入一个用户用于注册功能void insert(User user);
}实现UserMapper.xml
?xml version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.example.java_gobang.model.UserMapperselect idselectByName resultTypecom.example.java_gobang.model.Userselect * from user where username#{username};/selectinsert idinsertinsert into user values(null,#{username},#{password},1000,0,0);/insert/mapper 前后端接口交互 登录接口 请求 POST /login HTTP/ 1.1 Content-Type: application/x-www-form-urlencoded usernamebaekhyunpassword2012 响应 HTTP/ 1.1 200 OK Content-Type: application/json { userId: 1, username: baekhyun, score: 1000, totalCount: 0, winCount: 0 } 如果登录失败, 返回的是一个无效的user对象 注册接口 请求 POST /register HTTP/ 1.1 Content-Type: application/x-www-form-urlencoded usernamebaekhyunpassword2012 响应 HTTP/ 1.1 200 OK Content-Type: application/json { userId: 1, username: baekhyun, score: 1000, totalCount: 0, winCount: 0 } 获取用户信息 请求 GET /userInfo HTTP/ 1.1 响应 HTTP/ 1.1 200 OK Content-Type: application/json { userId: 1, username: baekhyun, score: 1000, totalCount: 0, winCount:0 } 服务器开发 实现三种方法 login用来实现登录逻辑 register用来实现注册逻辑 getUserInfo用来实现登录成功后显示用户分数的信息
RestController
public class UserAPI {Resourceprivate UserMapper userMapper;PostMapping(/login)ResponseBodypublic Object login(String username, String password, HttpServletRequest req){//根据username在数据库中进行查询//如果找到匹配的用户并且密码也一致就认为登录成功User user userMapper.selectByName(username);System.out.println([login] usernameusername);if (usernull || !user.getPassword().equals(password)){System.out.println(登录失败);return new User();}HttpSession httpsessionreq.getSession(true);httpsession.setAttribute(user,user);return user;}PostMapping(/register)ResponseBodypublic Object register(String username,String password){try {User usernew User();user.setUsername(username);user.setPassword(password);userMapper.insert(user);return user;}catch (org.springframework.dao.DuplicateKeyException){User usernew User();return user;}}GetMapping(/userinfo)ResponseBodypublic Object getUserInfo(HttpServletRequest req){try {HttpSession httpSessionreq.getSession(false);User user(User) httpSession.getAttribute(user);return user;}catch (NullPointerException e){return new User();}}
} 客户端开发 登录页面 login.html
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title登录/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/login.css
/head
bodydiv classnav五子棋对战/divdiv classlogin-containerdiv classlogin-dialog!-- 标题 --h3登录/h3!-- 输入用户名 --div classrowspan用户名/spaninput typetext idusername/div!-- 输入密码 --div classrowspan密码/spaninput typepassword idpassword/div!-- 提交按钮 --div classrowbutton idsubmit提交/button /div/div /div
/body
/html common.css
* {margin: 0;padding: 0;box-sizing: border-box;
}html, body {height: 100%;background-image: url(../image/1.png);background-repeat: no-repeat;background-position: center;background-size: cover;
}.nav {width: 100%;height: 50px;background-color: rgb(51, 51, 51);color: white;display: flex;align-items: center;line-height: 50px;padding-left: 20px;
}.container {height: calc(100% - 50px);width: 100%;display: flex;justify-content: center;align-items: center;background-color: rgba(255, 255, 255, 0.7);
} login.css
.login-container {width: 100%;height: calc(100% - 50px);display: flex;justify-content: center;align-items: center;
}.login-dialog {width: 400px;height: 320px;background-color: rgba(255, 255, 255, 0.8);border-radius: 10px;
}.login-dialog h3 {text-align: center;padding: 50px 0;
}.login-dialog .row {width: 100%;height: 50px;display: flex;justify-content: center;align-items: center;
}.login-dialog .row span {display: block;/* 设置固定宽度, 能让文字和后面的输入框之间有间隙 */width: 100px;font-weight: 700;
}.login-dialog #username,
.login-dialog #password {width: 200px;height: 40px;font-size: 20px;text-indent: 10px;border-radius: 10px;border: none;outline: none;
}.login-dialog .submit-row {margin-top: 10px;
}.login-dialog #submit {width: 300px;height: 50px;color: white;background-color: rgb(133, 23, 23);border: none;border-radius: 10px;font-size: 20px;
}.login-dialog #submit:active {background-color: #666;
} 通过 jQuery 中的 AJAX 和服务器进行交互在login.html中写js script src./js/jquery.min.js/scriptscriptlet usernameInputdocument.querySelector(#username);let passwordInputdocument.querySelector(#password);let submitButtondocument.querySelector(#submit);submitButton.onclickfunction(){$.ajax({type: post,url: /login,data:{username:usernameInput.value,password:passwordInput.value,},success:function(body){//请求执行成功的回调函数//判定当前是否登录成功//如果登录成功服务器会返回当前的user对象//如果登录失败服务器则会返回一个空的user对象if(body body.userId0){//登录成功alert(登录成功);//重定向跳转到游戏大厅页面location.assign(/game_hall.html);}else{alert(登录失败);}},error:function(){//请求执行失败的回调函数alert(登录失败);}});}/script 注册页面 register.html
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title注册/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/login.css
/head
bodydiv classnav五子棋对战/divdiv classlogin-containerdiv classlogin-dialog!-- 标题 --h3注册/h3!-- 输入用户名 --div classrowspan用户名/spaninput typetext idusername/div!-- 输入密码 --div classrowspan密码/spaninput typepassword idpassword/div!-- 提交按钮 --div classrowbutton idsubmit提交/button /div/div /div
/body
/html 4.2、实现匹配模块 前后端接口交互 连接 ws://127.0.0.1:8080/findMatch 请求 { message: startMatch / stopMatch,} 响应1收到请求后立即响应 { ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false reason: , // 错误原因 message: startMatch / stopMatch } 响应2匹配成功后的响应 { ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false reason: , // 错误原因 message: matchSuccess, } 客户端开发 实现页面基本结构 game_hall.html
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏大厅/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_hall.css
/head
bodydiv classnav五子棋对战/divdiv classcontainerdiv!--展示用户信息--div idscreen/div!--匹配按钮--div idmatch-button开始匹配/div/div/div
/body
/html game_hall.css
#screen {width: 400px;height: 200px;font-size: 20px;background-color: gray;color: white;border-radius: 10px;}#match-button {width: 400px;height: 50px;font-size: 20px;line-height: 50px;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 srcjs/jquery.min.js/scriptscript$.ajax({type:get,url:/userInfo,success:function(body){let screenDivdocument.querySelector(#screen);screenDiv.innerHTML玩家body.username分数body.scorebr 比赛场次:body.totalCount获胜次数body.winCount},error:function(){alert(获取用户信息失败);}});/script 实现匹配功能 点击匹配按钮就会进入匹配逻辑同时按钮上提示“匹配中...点击取消” 再次点击匹配按钮则会取消匹配 当匹配成功后服务器会返回匹配成功响应页面跳转到游戏房间 //初始化websockrt并且实现前端的匹配逻辑let websocketnew WebSocket(ws://127.0.0.1:8080/findMatch);websocket.onopenfunction(){console.log(onopen);}websocket.onclosefunction(){console.log(onclose);}websocket.onerrorfunction(){console.log(onerror);}//监听页面关闭事件在页面关闭之前手动调用这里的websocket的close方法window.onbeforeloadfunction(){websocket.close();}//处理服务器返回的响应websocket.onmessagefunction(e){//针对服务器返回的响应数据这个响应就是针对“开始匹配”/“结束匹配”来对应的//解析得到的响应对象返回的数据是一个JSON字符串解析成js对象let respJSON.parse(e.data);if(!resp.ok){console.log(游戏大厅中接收到了失败响应resp.reason);return;}if(resp.messagestartMatch){//开始匹配请求发送成功console.log(进入匹配队列成功);matchButton.innerHTML匹配中...(点击停止);}else if(resp.messagestopMatch){//结束匹配请求发送成功console.log(离开匹配队列成功);matchButton.innerHTML开始匹配;}else if(resp.messagematchSuccess){// 匹配到了对手console.log(匹配成功进入游戏界面);location.assign(/game_room.html);}else{console.log(接受了非法的响应messageresp.message);}}//给匹配按钮添加一个点击事件let matchButtondocument.querySelector(#match-button);matchButton.onclickfunction(){//在触发websocket请求之前先确认websocket连接是否好if(websocket.readyStatewebsocket.OPEN){//如果当前readyState处于OPEN状态说明连接好着//发送的数据开始匹配/停止匹配if (matchbutton.innerHTML 开始匹配) {console.log(开始匹配!);websocket.send(JSON.stringify({message: startMatch,}));} else if (matchbutton.innerHTML 匹配中...(点击停止)) {console.log(停止匹配!);websocket.send(JSON.stringify({message: stopMatch,}));}}else{//连接是异常状态alert(当前您的连接已经断开请重新登录);location.assign(/login.html);}}/script 服务器开发 创建并注册MatchAPI类 创建MatchAPI
Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMappernew ObjectMapper();Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);}Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);}Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);}Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);}
} 修改WebSocketConfig 在 addHandler 之后, 再加上一个 .addInterceptors(new HttpSessionHandshakeInterceptor()) 代码, 这样可以把之前登录过程中往 HttpSession 中存放的数据(主要是 User 对象), 放到 WebSocket 的 session 中. 方便后面的代码中获取到当前用户信息
Configuration
EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {Autowiredprivate TestAPI testAPI;Autowiredprivate MatchAPI matchAPI;Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(testAPI,/test);registry.addHandler(matchAPI,/findMatch).addInterceptors(new HttpSessionHandshakeInterceptor());}
} 实现用户管理器 创建 OnlineUserManager 类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession. 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中. 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除. 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.
Component
public class OnlineUserManager {//这个哈希表用来表示当前用户在游戏大厅的在线状态private HashMapInteger, WebSocketSession gameHallnew HashMap();public void enterGameHall(int userId,WebSocketSession webSocketSession){gameHall.put(userId,webSocketSession);}public void exitGameHall(int userId){gameHall.remove(userId);}public WebSocketSession getFromGameHall(int userId){return gameHall.get(userId);}
} 给 MatchAPI 注入 OnlineUserManager Autowiredprivate OnlineUserManager onlineUserManager; 创建匹配请求/响应对象 创建MatchRequest类
//表示一个websocket的匹配请求
public class MatchRequest {private String message;public String getMessage() {return message;}public void setMessage(String message) {this.message message;}
} 创建MatchResponse类
//表示一个websocket的匹配响应
public class MatchResponse {private boolean ok;private String reason;private String message;public boolean isOk() {return ok;}public void setOk(boolean ok) {this.ok ok;}public String getReason() {return reason;}public void setReason(String reason) {this.reason reason;}public String getMessage() {return message;}public void setMessage(String message) {this.message message;}
} 处理上线下线状态 当前是使用HashMap来存储用户的在线状态的如果是多线程访问一个HashMap容易出现线程安全问题所以针对HashMap进行修改 private ConcurrentHashMapInteger, WebSocketSession gameHallnew ConcurrentHashMap();实现 afterConnectionEstablished 方法. 通过参数中的 session 对象, 拿到之前登录时设置的 User 信息. 使用 onlineUserManager 来管理用户的在线状态 . 先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开). 设置玩家的上线状态.
//通过这个类来处理匹配功能中的websocket请求
Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMappernew ObjectMapper();Autowiredprivate OnlineUserManager onlineUserManager;Autowiredprivate Matcher matcher;Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {//玩家上线加入到onlineUserManager中//1、先获取到当前用户的身份信息谁在游戏大厅中建立的连接//由于在注册webSocket时加上了.addInterceptors(new HttpSessionHandshakeInterceptor()能够getAttributes()//这个逻辑就是把HttpSession中的Attribute拿到WebSocketSession中了//在Http登录逻辑中往HttpSession中存入了User数据httpsession.setAttribute(user,user)//此时就可以在WebSocketSession中把之前HttpSession里存的User对象给拿到了try {User user(User) session.getAttributes().get(user);//2、先判定当前用户是否已经登录过是在线状态如果已经在线不进行后续逻辑WebSocketSession tmpSessiononlineUserManager.getFromGameHall(user.getUserId());if (tmpSession!null){//当前已经登录过了告知客户端重复登录了MatchResponse responsenew MatchResponse();response.setOk(false);response.setReason(当前禁止多开);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));session.close();return;}//3、拿到身份信息之后就可以把玩家设置为在线状态onlineUserManager.enterGameHall(user.getUserId(), session);System.out.println(玩家user.getUsername()进入游戏大厅);}catch (NullPointerException e){e.printStackTrace();//出现空指针异常说明当前用户的身份信息为空用户未登录//把当前用户尚未登录这个信息返回回去MatchResponse responsenew MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行玩家匹配功能);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}} Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {try {//玩家下线退出onlineUserManagerUser user(User) session.getAttributes().get(user);WebSocketSession tmpSessiononlineUserManager.getFromGameHall(user.getUserId());if (tmpSessionsession){onlineUserManager.exitGameHall(user.getUserId());}//如果玩家正在匹配中websocket连接断开了就应该移除匹配队列matcher.remove(user);}catch (NullPointerException e){e.printStackTrace();MatchResponse responsenew MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行玩家匹配功能);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}}Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {try {//玩家下线退出onlineUserManagerUser user(User) session.getAttributes().get(user);WebSocketSession tmpSessiononlineUserManager.getFromGameHall(user.getUserId());if (tmpSessionsession){onlineUserManager.exitGameHall(user.getUserId());}//如果玩家正在匹配中websocket连接断开了就应该移除匹配队列matcher.remove(user);}catch (NullPointerException e){e.printStackTrace();MatchResponse responsenew MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行玩家匹配功能);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}}
} 处理开始匹配/取消匹配请求 实现 handleTextMessage 先从会话中拿到当前玩家的信息. 解析客户端发来的请求 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除. 此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑. Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {//实现处理开始匹配请求和处理停止匹配请求User user(User) session.getAttributes().get(user);//获取到客户端给服务器发送的数据String payloadmessage.getPayload();//当前这个数据是一个JSON格式的字符串需要转成java对象MatchRequest requestobjectMapper.readValue(payload,MatchRequest.class);MatchResponse responsenew MatchResponse();if (request.getMessage().equals(startMatch)){//进入匹配队列//先创建一个类表示匹配队列把当前用户加进去//把玩家信息放入匹配队列之后就可以返回一个响应给客户端response.setOk(true);response.setMessage(startMatch);}else if (request.getMessage().equals(stopMatch)){//退出匹配队列//先创建一个类表示匹配队列把当前用户取进去//把玩家信息放入匹配队列之后就可以返回一个响应给客户端response.setOk(true);response.setMessage(stopMatch);}else{//非法情况response.setOk(false);response.setReason(非法的匹配请求);}} 实现匹配器 创建 game.Matcher 类. 在 Matcher 中创建三个队列 (队列中存储 User 对象), 分别表示不同的段位的玩家. (此处约定 2000 一档, 2000-3000 一档, 3000 一档) 提供 add 方法, 供 MatchAPI 类来调用, 用来把玩家加入匹配队列. 提供 remove 方法, 供 MatchAPI 类来调用, 用来把玩家移出匹配队列. 同时 Matcher 找那个要记录 OnlineUserManager, 来获取到玩家的 Session.
//这个类表示匹配器通过这个类完成整个匹配功能
Component
public class Matcher {//创建三个匹配队列private QueueUser normalQueuenew LinkedList();private QueueUser highQueuenew LinkedList();private QueueUser veryHighQueuenew LinkedList();Autowiredprivate OnlineUserManager onlineUserManager;//操作匹配队列的方法//把玩家放入到匹配队列中public void add(User user){if (user.getScore()2000){synchronized (normalQueue){normalQueue.offer(user);}System.out.println(把玩家user.getUsername()加入到了normalQueue中);}else if (user.getScore()2000 user.getScore()3000){synchronized (highQueue){highQueue.offer(user);}System.out.println(把玩家user.getUsername()加入到了highQueue中);}else {synchronized (veryHighQueue){veryHighQueue.offer(user);}System.out.println(把玩家user.getUsername()加入到了veryHighQueue中);}}//当玩家点击停止匹配时就需要把玩家从匹配队列中删除public void remove(User user){if (user.getScore()2000){normalQueue.remove(user);}else if (user.getScore()2000 user.getScore()3000){highQueue.remove(user);}else {veryHighQueue.remove();}}} 修改 game.Matcher , 实现匹配逻辑. 在 Matcher 的构造方法中, 创建一个线程, 使用该线程扫描每个队列, 把每个队列的头两个元素取出来, 匹配到一组中. public Matcher(){//创建三个线程分别针对三个匹配队列进行操作Thread t1new Thread(){Overridepublic void run() {//扫描normalQueuewhile (true){handlerMatch(normalQueue);}}};t1.start();Thread t2new Thread(){Overridepublic void run() {//扫描highQueuewhile (true){handlerMatch(highQueue);}}};t2.start();Thread t3new Thread(){Overridepublic void run() {//扫描veryHighQueuewhile (true){handlerMatch(veryHighQueue);}}};t3.start();} 实现 handlerMatch 由于 handlerMatch 在单独的线程中调用. 因此要考虑到访问队列的线程安全问题. 需要加上锁. 每个队列分别使用队列对象本身作为锁即可. 在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列. private void handlerMatch(QueueUser matchQueue) {synchronized (matchQueue){try {//1、检测队列中元素个数是否达到2while (matchQueue.size()2){matchQueue.wait();}//2、尝试从队列中取出两个玩家User player1 matchQueue.poll();User player2 matchQueue.poll();System.out.println(匹配出两个玩家player1.getUsername(),player2.getUsername());//3、获取到玩家的websocket的会话WebSocketSession session1onlineUserManager.getFromGameHall(player1.getUserId());WebSocketSession session2onlineUserManager.getFromGameHall(player2.getUserId());if (session1null){//如果玩家1不在线了就把玩家2重新放回到匹配队列中matchQueue.offer(player2);return;}if (session2null){matchQueue.offer(player1);return;}if (session1session2){matchQueue.offer(player1);return;}//4、把这两个玩家放到同一个房间//5、给玩家反馈匹配成功的信息MatchResponse response1new MatchResponse();response1.setOk(true);response1.setMessage(matchSuccess);String json1objectMapper.writeValueAsString(response1);session1.sendMessage(new TextMessage(json1));MatchResponse response2new MatchResponse();response2.setOk(true);response2.setMessage(matchSuccess);String json2objectMapper.writeValueAsString(response2);session2.sendMessage(new TextMessage(json2));}catch (InterruptedException | IOException e){e.printStackTrace();}}需要给上面的插入队列元素, 删除队列元素也加上锁. //操作匹配队列的方法//把玩家放入到匹配队列中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中);}} 创建房间类 匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中. 创建 game.Room 类 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识房间内要记录对弈的玩家双方信息
//这个类就表示一个游戏房间
public class Room {//使用字符串来表示方便生成唯一值private String roomId;private User user1;private User user2;public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId roomId;}public User getUser1() {return user1;}public void setUser1(User user1) {this.user1 user1;}public User getUser2() {return user2;}public void setUser2(User user2) {this.user2 user2;}public Room(){//构造Room的时候生成一个唯一的字符串来表示房间id//使用UUID来作为房间idroomId UUID.randomUUID().toString();}
} 实现房间管理器 Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象. 需要一个管理器对象来管理所有的 Room. 创建 game.RoomManager 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象 再使用一个 Hash 表, 保存 userId - roomId 的映射, 方便根据玩家来查找所在的房间. 提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).
//房间管理器
//这个类也有唯一实例
Component
public class RoomManager {private ConcurrentHashMapString,Room roomsnew ConcurrentHashMap();private ConcurrentHashMapInteger,String userIdToRoomIdnew ConcurrentHashMap();public void add(Room room,int userId1,int userId2){rooms.put(room.getRoomId(),room);userIdToRoomId.put(userId1,room.getRoomId());userIdToRoomId.put(userId2,room.getRoomId());}public void remove(String roomId,int userId1,int userId2){rooms.remove(roomId);userIdToRoomId.remove(userId1);userIdToRoomId.remove(userId2);}public Room getRoomByRoomId(String roomId){return rooms.get(roomId);}public Room getRoomByUserId(int userId){String roomIduserIdToRoomId.get(userId);if (roomIdnull){//userId--》roomId映射关系不存在return null;}return rooms.get(roomId);}} 实现匹配器 给 Matcher 找注入 RoomManager 对象修改 Matcher.handlerMatch Autowiredprivate RoomManager roomManager;//4、把这两个玩家放到同一个房间Room roomnew Room();roomManager.add(room, player1.getUserId(), player2.getUserId()); 4.3、实现对战模块 前后端交互接口 建立连接 ws://127.0.0.1:8080/game 连接响应 { message: gameReady, // 游戏就绪 ok: true, // 是否成功. reason: , // 错误原因 roomId: abcdef, // 房间号. thisUserId: 1, // 玩家自己的 id thatUserId: 2, // 对手的 id whiteUser: 1, // 先手方的 id} 落子请求 { message: putChess, userId: 1, row: 0, col: 0} 落子响应 { message: putChess, userId: 1, row: 0, col: 0, winner: 0} 客户端开发 实现页面基本结构 创建 game_room.html, 表示对战页面.
!DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏房间/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_room.css
/head
bodydiv classnav五子棋对战/divdiv classcontainerdiv!--棋盘区域需要基于canvas进行实现--canvas idchess width450px height450px/canvas!--显示区域--div idscreen等待玩家连接中。。。/div/div/div
/body
/html 实现棋盘绘制 创建script.js 使用一个二维数组来表示棋盘. 虽然胜负是通过服务器判定的, 但是客户端的棋盘可以避免 一个位置重复落子 这样的情况 oneStep 函数起到的效果是在一个指定的位置上绘制一个棋子. 可以区分出绘制白字还是黑子. 参数是横坐标和纵坐标, 分别对应列和行. 用 onclick 来处理用户点击事件 . 当用户点击的时候通过这个函数来控制绘制棋子 . me 变量用来表示当前是否轮到我落子. over 变量用来表示游戏结束.
gameInfo{roomId:null,thisUserId:null,thatUserId:null,isWhite:true,
}//设定页面显示相关操作
function setScreenText(me){let screendocument.querySelector(#screen);if(me){screen.innerHTML轮到你落子了;}else{screen.innerHTML轮到对方落子了;}
}//初始化websocket
//初始化一局游戏
function initGame(){//根据服务器分配的先后手情况决定谁先下let megameInfo.isWhite;//游戏是否结束let overfalse;let chessBoard[];//初始化chessBoard数组表示棋盘的数组for(let i0;i15;i){chessBoard[i][];for(let j0;j15;j){chessBoard[i][j]0;}}let chessdocument.querySelector(#chess);let contextchess.getContext(2d);context.strokeStyle#BFBFBF;//背景图片let logonew Image();logo.srcimage/ee.jpeg;logo.onloadfunction(){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(); 初始化websocket 在 game_room.html 中, 加入 websocket 的连接代码, 实现前后端交互. 先删掉原来的 initGame 函数的调用. 一会在获取到服务器反馈的就绪响应之后, 再初始化棋盘. 创建 websocket 对象, 并注册 onopen/onclose/onerror 函数. 其中在 onerror 中做一个跳转到游戏大厅的逻辑. 当网络异常断开, 则回到大厅. 实现 onmessage 方法. onmessage 先处理游戏就绪响应.
//初始化websocket
let websocketnew WebSocket(ws://127.0.0.1:8080/game);websocket.onopenfunction(){console.log(连接游戏房间成功!);
}websocket.onclosefunction(){console.log(和游戏服务器断开连接!);
}websocket.onerrorfunction(){console.log(和服务器的连接出现异常);
}window.onbeforeunloadfunction(){websocket.close();
}websocket.onmessagefunction(event){console.log([handlerGameReady]event.data);let respJSON.parse(event.data);if(resp.message!gameReady){console.log(响应类型错误);return;}if(!resp.ok){alert(游戏连接失败reasonresp.reason);//如果出现连接失败的情况回到游戏大厅location.assign(/game_hall.html);return;}//初始化游戏信息gameInfo.roomIdresp.roomId;gameInfo.thisUserIdresp.thisUserId;gameInfo.thatUserIdresp.thatUserId;gameInfo.isWhiteresp.isWhite;//初始化棋盘initGame();//设置显示区域内容setScreenText(gameInfo.isWhite);
} 发送落子请求 修改 onclick 函数, 在落子操作时加入发送请求的逻辑 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 req{message:putChess,userId:gameInfo.thisUserId,row:row,col:col};websocket.send(JSON.stringify(req));} 处理落子响应 在 initGame 中, 修改 websocket 的 onmessage 在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了在处理落子响应中要处理胜负手.
//在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了websocket.onmessagefunction(event){console.log([handlerPutChess]event.data);let respJSON.parse(event.data);if(resp.message!putChess){console.log(响应类型错误);return;}//判断当前这个响应是自己落的子还是别人落的子if(resp.userIdgameInfo.thisUserId){//自己落的子oneStep(resp.col,resp.row,gameInfo.isWhite);}else if(resp.userIdgameInfo.thatUserId){//对手落的子oneStep(resp.col,resp.row,!gameInfo.isWhite);}else{//响应错误userId有问题console.log([handlerPutChess] resp UserId错误);return;}//给对应的位置设为1方便后续逻辑判定当前位置是否已经有子chessBoard[row][col]1;//交换双方的落子轮次me!me;setScreenText(me);//判定游戏是否结束if(resp.winner!0){if(resp.winnergameInfo.thisUserId){alert(你赢了);}else if(resp.winnergameInfo.thatUserId){alert(你输了);}else{alert(winner字段错误!resp.winner);}//回到游戏大厅location.assign(/game_hall.html);}} 服务器开发 创建并注册GameAPI类 创建 api.GameAPI , 处理 websocket 请求.
Component
public class GameAPI extends TextWebSocketHandler {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 进行注册 Autowiredprivate GameAPI gameAPI;registry.addHandler(gameAPI,/game).addInterceptors(new HttpSessionHandshakeInterceptor()); 创建落子请求/响应对象 创建 game.GameReadyResponse 类
//客户端连接到游戏房间后服务器返回的响应
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;public String getMessage() {return message;}public void setMessage(String message) {this.message message;}public boolean isOk() {return ok;}public void setOk(boolean ok) {this.ok ok;}public String getReason() {return reason;}public void setReason(String reason) {this.reason reason;}public String getRoomId() {return roomId;}public void setRoomId(String roomId) {this.roomId roomId;}public int getThisUserId() {return thisUserId;}public void setThisUserId(int thisUserId) {this.thisUserId thisUserId;}public int getThatUserId() {return thatUserId;}public void setThatUserId(int thatUserId) {this.thatUserId thatUserId;}public int getWhiteUser() {return whiteUser;}public void setWhiteUser(int whiteUser) {this.whiteUser whiteUser;}
} 创建 game.GameRequest 类
//落子请求
public class GameRequest {private String message;private int userId;private int row;private int col;public String getMessage() {return message;}public void setMessage(String message) {this.message message;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId userId;}public int getRow() {return row;}public void setRow(int row) {this.row row;}public int getCol() {return col;}public void setCol(int col) {this.col col;}
} 创建 game.GameResponse 类
//落子响应
public class GameResponse {private String message;private int userId;private int row;private int col;private int winner;public String getMessage() {return message;}public void setMessage(String message) {this.message message;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId userId;}public int getRow() {return row;}public void setRow(int row) {this.row row;}public int getCol() {return col;}public void setCol(int col) {this.col col;}public int getWinner() {return winner;}public void setWinner(int winner) {this.winner winner;}
} 处理连接成功 实现 GameAPI 的 afterConnectionEstablished 方法. 首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息. 然后要判定当前玩家是否是在房间中. 接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接. Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse respnew GameReadyResponse();//1、先获取到用户的身份信息从HttpSession中拿到当前用户的对象User user(User) session.getAttributes().get(user);if (usernull){resp.setOk(false);resp.setReason(用户尚未登录);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//2、判定当前用户是否已经进入房间使用房间管理器进行查询Room roomroomManager.getRoomByUserId(user.getUserId());if (roomnull){//如果为null当前没有找到对应的房间该玩家还没有匹配到resp.setOk(false);resp.setReason(用户尚未匹配到);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//3、判断是不是多开if (onlineUserManager.getFromGameHall(user.getUserId())!null|| onlineUserManager.getFromGameRoom(user.getUserId())!null){//如果一个账号一边是在游戏大厅一边是在游戏房间也是为多开resp.setOk(false);resp.setReason(禁止多开游戏界面);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//4、设置当前玩家上线onlineUserManager.enterGameRoom(user.getUserId(), session);//5、把两个玩家加入到游戏房间if (room.getUser1()null){//第一个玩家还尚未加入房间room.setUser1(user);//把先连入房间的玩家设为先手方room.setWhiteUser(user.getUserId());System.out.println(玩家user.getUsername()已经准备就绪作为玩家1);return;}if (room.getUser2()null){//玩家1已经进入房间room.setUser2(user);System.out.println(玩家user.getUsername()已经准备就绪作为玩家2);//当两个玩家都加入成功之后就要让服务器给这两个玩家都返回websocket的响应数据//通知这两个玩家游戏双方都准备好了//通知玩家1noticeGameReady(room,room.getUser1(),room.getUser2());//通知玩家2noticeGameReady(room,room.getUser2(),room.getUser1());return;}//6、此处如果又有玩家尝试连接同一个房间就提示报错resp.setOk(false);resp.setReason(当前房间已满您不能加入房间);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));} 实现通知玩家就绪 private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {GameReadyResponse respnew GameReadyResponse();resp.setMessage(gameReady);resp.setOk(true);resp.setReason();resp.setRoomId(room.getRoomId());resp.setThisUserId(thisUser.getUserId());resp.setThatUserId(thatUser.getUserId());resp.setWhiteUser(room.getWhiteUser());//把当前的响应数据传回给玩家WebSocketSession webSocketSessiononlineUserManager.getFromGameRoom(thisUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));} 玩家下线的处理 Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user(User) session.getAttributes().get(user);if (usernull){//在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSessiononlineUserManager.getFromGameRoom(user.getUserId());if (sessionexitSession){//避免在多开的情况下第二个用户退出连接动作onlineUserManager.exitGameRoom(user.getUserId());}System.out.println(当前用户user.getUsername()游戏房间连接异常);}Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user(User) session.getAttributes().get(user);if (usernull){//在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSessiononlineUserManager.getFromGameRoom(user.getUserId());if (sessionexitSession){//避免在多开的情况下第二个用户退出连接动作onlineUserManager.exitGameRoom(user.getUserId());}System.out.println(当前用户user.getUsername()离开游戏房间);} 处理落子请求 实现 handleTextMessage Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {//1、先从session里拿到当前用户的身份信息User user(User) session.getAttributes().get(user);if (usernull){System.out.println([handleTextMessage]当前玩家尚未登录);return;}//2、根据玩家id获取到房间对象Room roomroomManager.getRoomByUserId(user.getUserId());//3、通过room对象来处理这次的具体请求room.putChess(message.getPayload());} 修改Room类 由于我们的 Room 并没有通过 Spring 来管理. 因此内部就无法通过 Autowired 来自动注入. 需要手动的通过 SpringBoot 的启动类来获取里面的对象.
SpringBootApplication
public class JavaGobangApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) {contextSpringApplication.run(JavaGobangApplication.class, args);}} public Room(){//构造Room的时候生成一个唯一的字符串来表示房间id//使用UUID来作为房间idroomId UUID.randomUUID().toString();//通过入口类中记录的context来手动获取到前面的RoomManager和OnlineUserManageronlineUserManager JavaGobangApplication.context.getBean(OnlineUserManager.class);roomManagerJavaGobangApplication.context.getBean(RoomManager.class);} 实现对弈功能 实现 room 中的 putChess 方法. //二维数组用来表示棋盘//使用0表示当前位置未落子//使用1表示user1的落子位置//使用2表示user2的落子位置private int[][] boardnew int[15][15];//创建objectMapper用来转换JSONprivate ObjectMapper objectMappernew ObjectMapper();Autowiredprivate OnlineUserManager onlineUserManager;//引入roommanager用于房间销毁Autowiredprivate RoomManager roomManager;//通过这个方法来处理一次落子操作public void putChess(String reqJson) throws IOException {//1、记录当前落子的情况GameRequest requestobjectMapper.readValue(reqJson,GameRequest.class);GameResponse responsenew GameResponse();//判断当前是玩家1落子还是玩家2int chessrequest.getUserId()user1.getUserId()?1:2;int row request.getRow();int col request.getCol();if (board[row][col]!0){System.out.println(当前位置(row,col)已经有子了);return;}board[row][col]chess;//2、进行胜负判定int winnercheckWinner(row,col);//3、给客户端返回响应response.setMessage(putChess);response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//要想给用户发送websocket数据就要获得这个用户的websocketSessionWebSocketSession session1onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2onlineUserManager.getFromGameRoom(user2.getUserId());if (session1null){response.setWinner(user2.getUserId());System.out.println(玩家1掉线);}if (session2null){response.setWinner(user1.getUserId());System.out.println(玩家2掉线);}//把响应构造成Json字符串通过session进行传输String respJsonobjectMapper.writeValueAsString(response);if (session1!null){session1.sendMessage(new TextMessage(respJson));}if (session2!null){session2.sendMessage(new TextMessage(respJson));}//4、如果当前胜负已分就把room从管理器中销毁if (response.getWinner()!0){System.out.println(游戏结束房间即将销毁roomIdroomId获胜方为response.getWinner());//销毁房间roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}} 实现打印棋盘的逻辑 private void printBoard() {//打印出棋盘System.out.println(打印棋盘信息roomId);System.out.println();for (int r0;rMAX_ROW;r){for (int c0;cMAX_COL;c){System.out.print(board[r][c] );}System.out.println();}System.out.println();} 实现胜负判定 //如果玩家1获胜就返回玩家1的userId//胜负未分返回0private int checkWinner(int row, int col,int chess) {//1、检查所有的行//先遍历这五种情况for (int ccol-4;ccol;c){//针对其中一种情况来判断这五个子是不是连在一起try {if (board[row][c]chess board[row][c1]chess board[row][c2]chess board[row][c3]chess board[row][c4]chess){return chess1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//2、判定所有列for (int rrow-4;rrow;r){//针对其中一种情况来判断这五个子是不是连在一起try {if (board[r][col]chess board[r1][col]chess board[r2][col]chess board[r3][col]chess board[r4][col]chess){return chess1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//3、左对角线for (int rrow-4,ccol-4;rrow ccol;r,c){//针对其中一种情况来判断这五个子是不是连在一起try {if (board[r][c]chess board[r1][c1]chess board[r2][c2]chess board[r3][c3]chess board[r4][c4]chess){return chess1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}//4、右对角线for (int rrow-4,ccol4;rrow ccol;r,c--){//针对其中一种情况来判断这五个子是不是连在一起try {if (board[r][c]chess board[r1][c-1]chess board[r2][c-2]chess board[r3][c-3]chess board[r4][c-4]chess){return chess1?user1.getUserId():user2.getUserId();}}catch (ArrayIndexOutOfBoundsException e){continue;}}return 0;} 处理途中玩家掉线 在GameAPI中的handleTransportError和afterConnectionClosed添加noticeThatUserWin方法 private void noticeThatUserWin(User user) throws IOException {//1、根据当前玩家找到玩家所在的房间Room roomroomManager.getRoomByUserId(user.getUserId());if (roomnull){//该房间已经被释放没有“对手”System.out.println(当前房间已经被释放无需通知对手);return;}//2、根据房间找对手User thatUser(userroom.getUser1())?room.getUser2():room.getUser1();//3、找到对手的在线状态WebSocketSession webSocketSessiononlineUserManager.getFromGameRoom(thatUser.getUserId());if (webSocketSessionnull){//意味着对手掉线了System.out.println(对手也已经掉线了无需通知);return;}//4、构造一个响应来通知对手你是获胜方GameResponse respnew GameResponse();resp.setMessage(putChess);resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));//5、释放房间对象roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());} 更新玩家分数 修改UserMapper和UserMapper.xml
Mapper
public interface UserMapper {//根据用户名来查询用户的信息用于登录功能User selectByName(String username);//往数据库里插入一个用户用于注册功能void insert(User user);//总比赛场数1获胜场数1天梯分数30void userWin(int userId);//总比赛场数1获胜场数不变天梯分数-30void userLose(int userId);
} update iduserWinupdate user set totalCounttotalCount1,winCountwinCount1,scorescore30where userId#{userId}/updateupdate iduserLoseupdate user set totalCounttotalCount1,scorescore-30where userId#{userId}/update 修改putChess方法 //通过这个方法来处理一次落子操作public void putChess(String reqJson) throws IOException {//1、记录当前落子的情况GameRequest requestobjectMapper.readValue(reqJson,GameRequest.class);GameResponse responsenew GameResponse();//判断当前是玩家1落子还是玩家2int chessrequest.getUserId()user1.getUserId()?1:2;int row request.getRow();int col request.getCol();if (board[row][col]!0){System.out.println(当前位置(row,col)已经有子了);return;}board[row][col]chess;//2、打印出当前的棋盘信息printBoard();//3、进行胜负判定int winnercheckWinner(row,col,chess);//4、给客户端返回响应response.setMessage(putChess);response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//要想给用户发送websocket数据就要获得这个用户的websocketSessionWebSocketSession session1onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2onlineUserManager.getFromGameRoom(user2.getUserId());if (session1null){response.setWinner(user2.getUserId());System.out.println(玩家1掉线);}if (session2null){response.setWinner(user1.getUserId());System.out.println(玩家2掉线);}//把响应构造成Json字符串通过session进行传输String respJsonobjectMapper.writeValueAsString(response);if (session1!null){session1.sendMessage(new TextMessage(respJson));}if (session2!null){session2.sendMessage(new TextMessage(respJson));}//5、如果当前胜负已分就把room从管理器中销毁if (response.getWinner()!0){System.out.println(游戏结束房间即将销毁roomIdroomId获胜方为response.getWinner());int winUserIdresponse.getWinner();int loseUserIdresponse.getWinner()user1.getUserId()?user2.getUserId():user1.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);//销毁房间roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}} 修改GameAPI中noticeThatUserWin方法 private void noticeThatUserWin(User user) throws IOException {//1、根据当前玩家找到玩家所在的房间Room roomroomManager.getRoomByUserId(user.getUserId());if (roomnull){//该房间已经被释放没有“对手”System.out.println(当前房间已经被释放无需通知对手);return;}//2、根据房间找对手User thatUser(userroom.getUser1())?room.getUser2():room.getUser1();//3、找到对手的在线状态WebSocketSession webSocketSessiononlineUserManager.getFromGameRoom(thatUser.getUserId());if (webSocketSessionnull){//意味着对手掉线了System.out.println(对手也已经掉线了无需通知);return;}//4、构造一个响应来通知对手你是获胜方GameResponse respnew GameResponse();resp.setMessage(putChess);resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));//5、更新玩家信息int winUserIdthatUser.getUserId();int loseUserIduser.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);//6、释放房间对象roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());} 五、部署云服务器 构造数据库中的数据 调整websocket建立连接的url let websocketUrlws:// location.host/findMatch;let websocketnew WebSocket(websocketUrl); 打包上传 通过外网访问 五子棋实战 六、后续扩展功能 计时 一步落子过程中, 玩家能思考的时间. 保存棋谱/录像回放 首先需要在数据库中创建一个新的表, 用来表示每个玩家的游戏房间编号服务器把每一局对局, 玩家轮流落子的位置都记录下来(比如保存到一个文本文件中)然后玩家可以选定某个曾经的比赛, 在页面上回放出对局的过程. 观战功能 在游戏大厅除了显示匹配按钮之外, 还能显示当前所有的对局房间玩家可以选中某个房间, 以观众的形式加入到房间中. 同时能实时的看到选手的对局情况. 界面聊天 同一个房间中的选手之间可以发送文本消息或者在对战中可接受到游戏大厅好友的消息 人机对战 支持 AI 功能, 实现人机对战. 根据以上扩展功能后续将对此项目进行扩充敬请期待