大家好,欢迎来到IT知识分享网。
目录
一、项目背景
用户模块
用户的注册和登录
管理用户的天梯分数,比赛场数,获胜场数等信息
匹配模块
依据用户的天梯积分,实现匹配机制
对战模块
把两个匹配到的玩家放到一个游戏房间中,对方通过网页的形式来进行对战比赛
二、核心技术
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 { @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()); } @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("连接关闭"); } }
创建config.WebSocketConfig类
这个类用来配置请求路径和TextWebSocketHandler之间的关系
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private TestAPI testAPI; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(testAPI,"/test"); } }
客户端代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TestAPI</title> </head> <body> <input type="text" id="message"> <button id="submit">提交</button> <script> //创建websocket实例 let websocket=new WebSocket("ws://127.0.0.1:8080/test"); //给实例挂一些回调函数 websocket.onopen=function(){ console.log("建立连接!"); } websocket.onmessage=function(e){ console.log("收到消息!"+e.data); } websocket.onerror=function(){ console.log("连接异常!"); } websocket.onclose=function(){ console.log("连接关闭!"); } //实现点击按钮后,通过websocket发送请求 let input=document.querySelector('#message'); let button=document.querySelector('#submit'); button.onclick=function(){ 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?characterEncoding=utf8&&useSSL=false username: root password: "" driver-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 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"> <select id="selectByName" resultType="com.example.java_gobang.model.User"> select * from user where username=#{username}; </select> <insert id="insert"> insert into user values(null,#{username},#{password},1000,0,0); </insert> </mapper>
前后端接口交互
登录接口
请求
POST /login HTTP/
1.1
Content-Type: application/x-www-form-urlencoded
username=baekhyun&password=2012
响应
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
username=baekhyun&password=2012
响应
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 { @Resource private UserMapper userMapper; @PostMapping("/login") @ResponseBody public Object login(String username, String password, HttpServletRequest req){ //根据username在数据库中进行查询 //如果找到匹配的用户,并且密码也一致,就认为登录成功 User user= userMapper.selectByName(username); System.out.println("[login] username="+username); if (user==null || !user.getPassword().equals(password)){ System.out.println("登录失败!"); return new User(); } HttpSession httpsession=req.getSession(true); httpsession.setAttribute("user",user); return user; } @PostMapping("/register") @ResponseBody public Object register(String username,String password){ try { User user=new User(); user.setUsername(username); user.setPassword(password); userMapper.insert(user); return user; }catch (org.springframework.dao.DuplicateKeyException){ User user=new User(); return user; } } @GetMapping("/userinfo") @ResponseBody public Object getUserInfo(HttpServletRequest req){ try { HttpSession httpSession=req.getSession(false); User user=(User) httpSession.getAttribute("user"); return user; }catch (NullPointerException e){ return new User(); } } }
客户端开发
登录页面
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/login.css"> </head> <body> <div class="nav"> 五子棋对战 </div> <div class="login-container"> <div class="login-dialog"> <!-- 标题 --> <h3>登录</h3> <!-- 输入用户名 --> <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 id="submit">提交</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"></script> <script> let usernameInput=document.querySelector("#username"); let passwordInput=document.querySelector("#password"); let submitButton=document.querySelector("#submit"); submitButton.onclick=function(){ $.ajax({ type: 'post', url: '/login', data:{ username:usernameInput.value, password:passwordInput.value, }, success:function(body){ //请求执行成功的回调函数 //判定当前是否登录成功 //如果登录成功,服务器会返回当前的user对象 //如果登录失败,服务器则会返回一个空的user对象 if(body && body.userId>0){ //登录成功 alert("登录成功"); //重定向跳转到游戏大厅页面 location.assign('/game_hall.html'); }else{ alert("登录失败!"); } }, error:function(){ //请求执行失败的回调函数 alert("登录失败!"); } }); } </script>
注册页面
register.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>注册</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/login.css"> </head> <body> <div class="nav"> 五子棋对战 </div> <div class="login-container"> <div class="login-dialog"> <!-- 标题 --> <h3>注册</h3> <!-- 输入用户名 --> <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 id="submit">提交</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 lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>游戏大厅</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/game_hall.css"> </head> <body> <div class="nav">五子棋对战</div> <div class="container"> <div> <!--展示用户信息--> <div id="screen"></div> <!--匹配按钮--> <div id="match-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 src="js/jquery.min.js"></script> <script> $.ajax({ type:'get', url:'/userInfo', success:function(body){ let screenDiv=document.querySelector('#screen'); screenDiv.innerHTML='玩家:'+body.username+"分数:"+body.score+"<br> 比赛场次:"+body.totalCount+"获胜次数:"+body.winCount }, error:function(){ alert("获取用户信息失败!"); } }); </script>
实现匹配功能
点击匹配按钮,就会进入匹配逻辑,同时按钮上提示“匹配中…(点击取消)”
再次点击匹配按钮,则会取消匹配
当匹配成功后,服务器会返回匹配成功响应,页面跳转到游戏房间
//初始化websockrt,并且实现前端的匹配逻辑 let websocket=new WebSocket('ws://127.0.0.1:8080/findMatch'); websocket.onopen=function(){ console.log("onopen"); } websocket.onclose=function(){ console.log("onclose"); } websocket.onerror=function(){ console.log("onerror"); } //监听页面关闭事件,在页面关闭之前,手动调用这里的websocket的close方法 window.onbeforeload=function(){ websocket.close(); } //处理服务器返回的响应 websocket.onmessage=function(e){ //针对服务器返回的响应数据,这个响应就是针对“开始匹配”/“结束匹配”来对应的 //解析得到的响应对象,返回的数据是一个JSON字符串,解析成js对象 let resp=JSON.parse(e.data); if(!resp.ok){ console.log("游戏大厅中接收到了失败响应!"+resp.reason); return; } if(resp.message=='startMatch'){ //开始匹配请求发送成功 console.log("进入匹配队列成功!"); matchButton.innerHTML='匹配中...(点击停止)'; }else if(resp.message=='stopMatch'){ //结束匹配请求发送成功 console.log("离开匹配队列成功!"); matchButton.innerHTML='开始匹配'; }else if(resp.message=='matchSuccess'){ // 匹配到了对手 console.log("匹配成功!进入游戏界面!"); location.assign("/game_room.html"); }else{ console.log("接受了非法的响应!message="+resp.message); } } //给匹配按钮添加一个点击事件 let matchButton=document.querySelector('#match-button'); matchButton.onclick=function(){ //在触发websocket请求之前,先确认websocket连接是否好 if(websocket.readyState==websocket.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 objectMapper=new ObjectMapper(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { super.handleTransportError(session, exception); } @Override public 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 { @Autowired private TestAPI testAPI; @Autowired private MatchAPI matchAPI; @Override public 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 HashMap<Integer, WebSocketSession> gameHall=new 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
@Autowired private 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 ConcurrentHashMap<Integer, WebSocketSession> gameHall=new ConcurrentHashMap<>();
实现 afterConnectionEstablished 方法.
通过参数中的 session 对象, 拿到之前登录时设置的 User 信息.
使用 onlineUserManager 来管理用户的在线状态 .
先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开).
设置玩家的上线状态.
//通过这个类来处理匹配功能中的websocket请求 @Component public class MatchAPI extends TextWebSocketHandler { private ObjectMapper objectMapper=new ObjectMapper(); @Autowired private OnlineUserManager onlineUserManager; @Autowired private Matcher matcher; @Override public 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 tmpSession=onlineUserManager.getFromGameHall(user.getUserId()); if (tmpSession!=null){ //当前已经登录过了,告知客户端重复登录了 MatchResponse response=new 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 response=new MatchResponse(); response.setOk(false); response.setReason("您尚未登录!不能进行玩家匹配功能!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); } } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { try { //玩家下线,退出onlineUserManager User user=(User) session.getAttributes().get("user"); WebSocketSession tmpSession=onlineUserManager.getFromGameHall(user.getUserId()); if (tmpSession==session){ onlineUserManager.exitGameHall(user.getUserId()); } //如果玩家正在匹配中,websocket连接断开了,就应该移除匹配队列 matcher.remove(user); }catch (NullPointerException e){ e.printStackTrace(); MatchResponse response=new MatchResponse(); response.setOk(false); response.setReason("您尚未登录!不能进行玩家匹配功能!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { try { //玩家下线,退出onlineUserManager User user=(User) session.getAttributes().get("user"); WebSocketSession tmpSession=onlineUserManager.getFromGameHall(user.getUserId()); if (tmpSession==session){ onlineUserManager.exitGameHall(user.getUserId()); } //如果玩家正在匹配中,websocket连接断开了,就应该移除匹配队列 matcher.remove(user); }catch (NullPointerException e){ e.printStackTrace(); MatchResponse response=new MatchResponse(); response.setOk(false); response.setReason("您尚未登录!不能进行玩家匹配功能!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); } } }
处理开始匹配/取消匹配请求
实现 handleTextMessage
先从会话中拿到当前玩家的信息.
解析客户端发来的请求
判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.
@Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { //实现处理开始匹配请求和处理停止匹配请求 User user=(User) session.getAttributes().get("user"); //获取到客户端给服务器发送的数据 String payload=message.getPayload(); //当前这个数据是一个JSON格式的字符串,需要转成java对象 MatchRequest request=objectMapper.readValue(payload,MatchRequest.class); MatchResponse response=new 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 Queue<User> normalQueue=new LinkedList<>(); private Queue<User> highQueue=new LinkedList<>(); private Queue<User> veryHighQueue=new LinkedList<>(); @Autowired private 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 t1=new Thread(){ @Override public void run() { //扫描normalQueue while (true){ handlerMatch(normalQueue); } } }; t1.start(); Thread t2=new Thread(){ @Override public void run() { //扫描highQueue while (true){ handlerMatch(highQueue); } } }; t2.start(); Thread t3=new Thread(){ @Override public void run() { //扫描veryHighQueue while (true){ handlerMatch(veryHighQueue); } } }; t3.start(); }
实现 handlerMatch
由于 handlerMatch 在单独的线程中调用. 因此要考虑到访问队列的线程安全问题. 需要加上锁.
每个队列分别使用队列对象本身作为锁即可.
在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列.
private void handlerMatch(Queue<User> matchQueue) { synchronized (matchQueue){ try { //1、检测队列中元素个数是否达到2 while (matchQueue.size()<2){ matchQueue.wait(); } //2、尝试从队列中取出两个玩家 User player1= matchQueue.poll(); User player2= matchQueue.poll(); System.out.println("匹配出两个玩家:"+player1.getUsername()+","+player2.getUsername()); //3、获取到玩家的websocket的会话 WebSocketSession session1=onlineUserManager.getFromGameHall(player1.getUserId()); WebSocketSession session2=onlineUserManager.getFromGameHall(player2.getUserId()); if (session1==null){ //如果玩家1不在线了,就把玩家2重新放回到匹配队列中 matchQueue.offer(player2); return; } if (session2==null){ matchQueue.offer(player1); return; } if (session1==session2){ matchQueue.offer(player1); return; } //4、把这两个玩家放到同一个房间 //5、给玩家反馈匹配成功的信息 MatchResponse response1=new MatchResponse(); response1.setOk(true); response1.setMessage("matchSuccess"); String json1=objectMapper.writeValueAsString(response1); session1.sendMessage(new TextMessage(json1)); MatchResponse response2=new MatchResponse(); response2.setOk(true); response2.setMessage("matchSuccess"); String json2=objectMapper.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来作为房间id roomId= UUID.randomUUID().toString(); } }
实现房间管理器
Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象.
需要一个管理器对象来管理所有的 Room.
创建 game.RoomManager
使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).
//房间管理器 //这个类也有唯一实例 @Component public class RoomManager { private ConcurrentHashMap<String,Room> rooms=new ConcurrentHashMap<>(); private ConcurrentHashMap<Integer,String> userIdToRoomId=new 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 roomId=userIdToRoomId.get(userId); if (roomId==null){ //userId--》roomId映射关系不存在 return null; } return rooms.get(roomId); } }
实现匹配器
给 Matcher 找注入 RoomManager 对象,修改 Matcher.handlerMatch
@Autowired private RoomManager roomManager; //4、把这两个玩家放到同一个房间 Room room=new 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 lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>游戏房间</title> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/game_room.css"> </head> <body> <div class="nav">五子棋对战</div> <div class="container"> <div> <!--棋盘区域,需要基于canvas进行实现--> <canvas id="chess" width="450px" height="450px"> </canvas> <!--显示区域--> <div id="screen">等待玩家连接中。。。</div> </div> </div> </body> </html>
实现棋盘绘制
创建script.js
使用一个二维数组来表示棋盘. 虽然胜负是通过服务器判定的, 但是客户端的棋盘可以避免 “一个位置重复落子” 这样的情况
oneStep 函数起到的效果是在一个指定的位置上绘制一个棋子. 可以区分出绘制白字还是黑子. 参数是横坐标和纵坐标, 分别对应列和行.
用 onclick 来处理用户点击事件 . 当用户点击的时候通过这个函数来控制绘制棋子 .
me 变量用来表示当前是否轮到我落子. over 变量用来表示游戏结束.
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 //初始化一局游戏 function initGame(){ //根据服务器分配的先后手情况决定谁先下 let me=gameInfo.isWhite; //游戏是否结束 let over=false; let chessBoard=[]; //初始化chessBoard数组(表示棋盘的数组) 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/ee.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, "#"); } 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 websocket=new WebSocket("ws://127.0.0.1:8080/game"); websocket.onopen=function(){ console.log("连接游戏房间成功!"); } websocket.onclose=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.message!='gameReady'){ console.log("响应类型错误!"); return; } if(!resp.ok){ alert("游戏连接失败!reason="+resp.reason); //如果出现连接失败的情况,回到游戏大厅 location.assign("/game_hall.html"); return; } //初始化游戏信息 gameInfo.roomId=resp.roomId; gameInfo.thisUserId=resp.thisUserId; gameInfo.thatUserId=resp.thatUserId; gameInfo.isWhite=resp.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.onmessage=function(event){ console.log("[handlerPutChess]"+event.data); let resp=JSON.parse(event.data); if(resp.message!='putChess'){ console.log("响应类型错误!"); return; } //判断当前这个响应是自己落的子,还是别人落的子 if(resp.userId==gameInfo.thisUserId){ //自己落的子 oneStep(resp.col,resp.row,gameInfo.isWhite); }else if(resp.userId==gameInfo.thatUserId){ //对手落的子 oneStep(resp.col,resp.row,!gameInfo.isWhite); }else{ //响应错误,userId有问题 console.log('[handlerPutChess] resp UserId错误!'); return; } //给对应的位置设为1,方便后续逻辑判定当前位置是否已经有子 chessBoard[row][col]=1; //交换双方的落子轮次 me=!me; setScreenText(me); //判定游戏是否结束 if(resp.winner!=0){ if(resp.winner==gameInfo.thisUserId){ alert("你赢了!"); }else if(resp.winner==gameInfo.thatUserId){ alert("你输了!"); }else{ alert("winner字段错误!"+resp.winner); } //回到游戏大厅 location.assign('/game_hall.html'); } }
服务器开发
创建并注册GameAPI类
创建 api.GameAPI , 处理 websocket 请求.
@Component public class GameAPI extends TextWebSocketHandler { @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 进行注册
@Autowired private 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 中拿到当前用户信息.
然后要判定当前玩家是否是在房间中.
接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.
@Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { GameReadyResponse resp=new GameReadyResponse(); //1、先获取到用户的身份信息(从HttpSession中拿到当前用户的对象) User user=(User) session.getAttributes().get("user"); if (user==null){ resp.setOk(false); resp.setReason("用户尚未登录!"); session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); return; } //2、判定当前用户是否已经进入房间(使用房间管理器进行查询) Room room=roomManager.getRoomByUserId(user.getUserId()); if (room==null){ //如果为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的响应数据 //通知这两个玩家,游戏双方都准备好了 //通知玩家1 noticeGameReady(room,room.getUser1(),room.getUser2()); //通知玩家2 noticeGameReady(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 resp=new 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 webSocketSession=onlineUserManager.getFromGameRoom(thisUser.getUserId()); webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); }
玩家下线的处理
@Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { User user=(User) session.getAttributes().get("user"); if (user==null){ //在断开连接的时候就不给客户端返回响应了 return; } WebSocketSession exitSession=onlineUserManager.getFromGameRoom(user.getUserId()); if (session==exitSession){ //避免在多开的情况下,第二个用户退出连接动作 onlineUserManager.exitGameRoom(user.getUserId()); } System.out.println("当前用户"+user.getUsername()+"游戏房间连接异常"); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { User user=(User) session.getAttributes().get("user"); if (user==null){ //在断开连接的时候就不给客户端返回响应了 return; } WebSocketSession exitSession=onlineUserManager.getFromGameRoom(user.getUserId()); if (session==exitSession){ //避免在多开的情况下,第二个用户退出连接动作 onlineUserManager.exitGameRoom(user.getUserId()); } System.out.println("当前用户"+user.getUsername()+"离开游戏房间"); }
处理落子请求
实现 handleTextMessage
@Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { //1、先从session里拿到当前用户的身份信息 User user=(User) session.getAttributes().get("user"); if (user==null){ System.out.println("[handleTextMessage]当前玩家尚未登录!"); return; } //2、根据玩家id获取到房间对象 Room room=roomManager.getRoomByUserId(user.getUserId()); //3、通过room对象来处理这次的具体请求 room.putChess(message.getPayload()); }
修改Room类
由于我们的 Room 并没有通过 Spring 来管理. 因此内部就无法通过 @Autowired 来自动注入.
需要手动的通过 SpringBoot 的启动类来获取里面的对象.
@SpringBootApplication public class JavaGobangApplication { public static ConfigurableApplicationContext context; public static void main(String[] args) { context=SpringApplication.run(JavaGobangApplication.class, args); } }
public Room(){ //构造Room的时候生成一个唯一的字符串来表示房间id //使用UUID来作为房间id roomId= UUID.randomUUID().toString(); //通过入口类中记录的context来手动获取到前面的RoomManager和OnlineUserManager onlineUserManager= JavaGobangApplication.context.getBean(OnlineUserManager.class); roomManager=JavaGobangApplication.context.getBean(RoomManager.class); }
实现对弈功能
实现 room 中的 putChess 方法.
//二维数组用来表示棋盘 //使用0表示当前位置未落子 //使用1表示user1的落子位置 //使用2表示user2的落子位置 private int[][] board=new int[15][15]; //创建objectMapper用来转换JSON private ObjectMapper objectMapper=new ObjectMapper(); @Autowired private OnlineUserManager onlineUserManager; //引入roommanager,用于房间销毁 @Autowired private RoomManager roomManager; //通过这个方法来处理一次落子操作 public void putChess(String reqJson) throws IOException { //1、记录当前落子的情况 GameRequest request=objectMapper.readValue(reqJson,GameRequest.class); GameResponse response=new GameResponse(); //判断当前是玩家1落子还是玩家2 int chess=request.getUserId()==user1.getUserId()?1:2; int row= request.getRow(); int col= request.getCol(); if (board[row][col]!=0){ System.out.println("当前位置("+row+","+col+")已经有子了!"); return; } board[row][col]=chess; //2、进行胜负判定 int winner=checkWinner(row,col); //3、给客户端返回响应 response.setMessage("putChess"); response.setUserId(request.getUserId()); response.setRow(row); response.setCol(col); response.setWinner(winner); //要想给用户发送websocket数据,就要获得这个用户的websocketSession WebSocketSession session1=onlineUserManager.getFromGameRoom(user1.getUserId()); WebSocketSession session2=onlineUserManager.getFromGameRoom(user2.getUserId()); if (session1==null){ response.setWinner(user2.getUserId()); System.out.println("玩家1掉线!!!"); } if (session2==null){ response.setWinner(user1.getUserId()); System.out.println("玩家2掉线!!!"); } //把响应构造成Json字符串,通过session进行传输 String respJson=objectMapper.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("游戏结束!房间即将销毁!roomId="+roomId+"获胜方为"+response.getWinner()); //销毁房间 roomManager.remove(roomId,user1.getUserId(),user2.getUserId()); } }
实现打印棋盘的逻辑
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(board[r][c]+" "); } System.out.println(); } System.out.println("======================"); }
实现胜负判定
//如果玩家1获胜就返回玩家1的userId //胜负未分返回0 private int checkWinner(int row, int col,int chess) { //1、检查所有的行 //先遍历这五种情况 for (int c=col-4;c<=col;c++){ //针对其中一种情况,来判断这五个子是不是连在一起 try { if (board[row][c]==chess && board[row][c+1]==chess && board[row][c+2]==chess && board[row][c+3]==chess && board[row][c+4]==chess){ return chess==1?user1.getUserId():user2.getUserId(); } }catch (ArrayIndexOutOfBoundsException e){ continue; } } //2、判定所有列 for (int r=row-4;r<=row;r++){ //针对其中一种情况,来判断这五个子是不是连在一起 try { if (board[r][col]==chess && board[r+1][col]==chess && board[r+2][col]==chess && board[r+3][col]==chess && board[r+4][col]==chess){ return chess==1?user1.getUserId():user2.getUserId(); } }catch (ArrayIndexOutOfBoundsException e){ continue; } } //3、左对角线 for (int r=row-4,c=col-4;r<=row && c<=col;r++,c++){ //针对其中一种情况,来判断这五个子是不是连在一起 try { if (board[r][c]==chess && board[r+1][c+1]==chess && board[r+2][c+2]==chess && board[r+3][c+3]==chess && board[r+4][c+4]==chess){ return chess==1?user1.getUserId():user2.getUserId(); } }catch (ArrayIndexOutOfBoundsException e){ continue; } } //4、右对角线 for (int r=row-4,c=col+4;r<=row && c>=col;r++,c--){ //针对其中一种情况,来判断这五个子是不是连在一起 try { if (board[r][c]==chess && board[r+1][c-1]==chess && board[r+2][c-2]==chess && board[r+3][c-3]==chess && board[r+4][c-4]==chess){ return chess==1?user1.getUserId():user2.getUserId(); } }catch (ArrayIndexOutOfBoundsException e){ continue; } } return 0; }
处理途中玩家掉线
在GameAPI中的handleTransportError和afterConnectionClosed添加noticeThatUserWin()方法
private void noticeThatUserWin(User user) throws IOException { //1、根据当前玩家,找到玩家所在的房间 Room room=roomManager.getRoomByUserId(user.getUserId()); if (room==null){ //该房间已经被释放,没有“对手” System.out.println("当前房间已经被释放,无需通知对手!"); return; } //2、根据房间找对手 User thatUser=(user==room.getUser1())?room.getUser2():room.getUser1(); //3、找到对手的在线状态 WebSocketSession webSocketSession=onlineUserManager.getFromGameRoom(thatUser.getUserId()); if (webSocketSession==null){ //意味着对手掉线了 System.out.println("对手也已经掉线了,无需通知!"); return; } //4、构造一个响应,来通知对手,你是获胜方 GameResponse resp=new 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,天梯分数+30 void userWin(int userId); //总比赛场数+1,获胜场数不变,天梯分数-30 void userLose(int userId); }
<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>
修改putChess方法
//通过这个方法来处理一次落子操作 public void putChess(String reqJson) throws IOException { //1、记录当前落子的情况 GameRequest request=objectMapper.readValue(reqJson,GameRequest.class); GameResponse response=new GameResponse(); //判断当前是玩家1落子还是玩家2 int chess=request.getUserId()==user1.getUserId()?1:2; int row= request.getRow(); int col= request.getCol(); if (board[row][col]!=0){ System.out.println("当前位置("+row+","+col+")已经有子了!"); return; } board[row][col]=chess; //2、打印出当前的棋盘信息 printBoard(); //3、进行胜负判定 int winner=checkWinner(row,col,chess); //4、给客户端返回响应 response.setMessage("putChess"); response.setUserId(request.getUserId()); response.setRow(row); response.setCol(col); response.setWinner(winner); //要想给用户发送websocket数据,就要获得这个用户的websocketSession WebSocketSession session1=onlineUserManager.getFromGameRoom(user1.getUserId()); WebSocketSession session2=onlineUserManager.getFromGameRoom(user2.getUserId()); if (session1==null){ response.setWinner(user2.getUserId()); System.out.println("玩家1掉线!!!"); } if (session2==null){ response.setWinner(user1.getUserId()); System.out.println("玩家2掉线!!!"); } //把响应构造成Json字符串,通过session进行传输 String respJson=objectMapper.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("游戏结束!房间即将销毁!roomId="+roomId+"获胜方为"+response.getWinner()); int winUserId=response.getWinner(); int loseUserId=response.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 room=roomManager.getRoomByUserId(user.getUserId()); if (room==null){ //该房间已经被释放,没有“对手” System.out.println("当前房间已经被释放,无需通知对手!"); return; } //2、根据房间找对手 User thatUser=(user==room.getUser1())?room.getUser2():room.getUser1(); //3、找到对手的在线状态 WebSocketSession webSocketSession=onlineUserManager.getFromGameRoom(thatUser.getUserId()); if (webSocketSession==null){ //意味着对手掉线了 System.out.println("对手也已经掉线了,无需通知!"); return; } //4、构造一个响应,来通知对手,你是获胜方 GameResponse resp=new GameResponse(); resp.setMessage("putChess"); resp.setUserId(thatUser.getUserId()); resp.setWinner(thatUser.getUserId()); webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp))); //5、更新玩家信息 int winUserId=thatUser.getUserId(); int loseUserId=user.getUserId(); userMapper.userWin(winUserId); userMapper.userLose(loseUserId); //6、释放房间对象 roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId()); }
五、部署云服务器
构造数据库中的数据
调整websocket建立连接的url
let websocketUrl='ws://'+ location.host+'/findMatch'; let websocket=new WebSocket(websocketUrl);
打包上传
通过外网访问
五子棋实战
六、后续扩展功能
计时
一步落子过程中, 玩家能思考的时间.
保存棋谱/录像回放
首先需要在数据库中创建一个新的表, 用来表示每个玩家的游戏房间编号,服务器把每一局对局, 玩家轮流落子的位置都记录下来(比如保存到一个文本文件中),然后玩家可以选定某个曾经的比赛, 在页面上回放出对局的过程.
观战功能
在游戏大厅除了显示匹配按钮之外, 还能显示当前所有的对局房间,玩家可以选中某个房间, 以观众的形式加入到房间中. 同时能实时的看到选手的对局情况.
界面聊天
同一个房间中的选手之间可以发送文本消息,或者在对战中可接受到游戏大厅好友的消息
人机对战
支持 AI 功能, 实现人机对战.
根据以上扩展功能,后续将对此项目进行扩充,敬请期待!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/146979.html