目录
一、项目背景
用户模块
用户的注册和登录
管理用户的天梯分数,比赛场数,获胜场数等信息
匹配模块
依据用户的天梯积分,实现匹配机制
对战模块
把两个匹配到的玩家放到一个游戏房间中,对方通过网页的形式来进行对战比赛
二、核心技术
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: "19930112"
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, "#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 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 功能, 实现人机对战.
根据以上扩展功能,后续将对此项目进行扩充,敬请期待!