1.用户模块
用户的注册和登录
管理用户的天梯分数、比赛场数、获胜场数等信息
2.匹配模块
依据用户的天梯积分,实现匹配机制
3.对战模块
把两个匹配到的玩家放到一个游戏房间中,双方通过网页的形式来进行对战比赛
用到的关键技术点:
Java、Spring/Spring Boot/Spring MVC、HTML/CSS/AJAX、MySQL/MyBatis、WebSocket
我们之前学习过的服务器开发,主要是这样的模型:
客户端主动向服务器发起请求,服务器收到之后,返回一个响应。
如果客户端不主动发起请求,服务器是不能主动联系客户端的
我们是否需要,服务器主动给客户端发消息这样的场景呢?
需要!!“消息推送”
当前已有的知识,主要是HTTP.HTTP自身难以实现这种消息推送效果的
HTTP要想实现类似的效果,就需要基于“轮询”的机制
很明显,像这样的轮询操作,开销是比较大的,成本也是比较高的
如果轮询间隔时间长,玩家1落子之后,玩家2不能及时拿到结果
如果轮询间隔时间短,虽然即使性得到改善,但是玩家2 不得不浪费更多的机器资源(尤其是带宽)
因此,websocket就是一个消息推送机制
websocket也是一个应用层的协议,下层是基于TCP的~
opcode描述了当前这个websocket报文是啥类型
表示当前这是一个文本帧还是一个二进制帧
表示当前这是一个ping帧,还是一个pong帧
payload len含义表示的是当前数据报携带的数据载荷的长度。这个字段本身就是一个变长的,一个websocket数据报能承载的载荷长度是非常长的
使用网页端,尝试和服务器建立websocket连接
网页端就会先给服务器发起一个HTTP请求 这个HTTP请求中会带有特殊的Header
Connection:Upgrade
Upgrade:Websocket
这两个header其实就是告知服务器,我们要进行协议升级
如果服务器支持websocket,就会返回一个特殊的HTTP响应 这个响应的状态码是101(切换协议)
客户端和服务器之间就开始使用websocket来进行通信了
编写服务器端(Java)
编写客户端(JS)
通过TestAPI重写了几个类,
光有这几个类还不够 需要把这几个类关联到路径
完成注册登录,以及用户分数管理
使用数据库来保存上述用户信息
使用MyBatis来连接并操作数据库
1.修改Spring的配置文件,使数据库可以被连接上(application.yml)
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false
username: root
password: zy19991227
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
2.创建实体类。用户 User
3.创建Mapper接口
针对数据库进行哪些具体的操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvvSwfi5-1688815713045)(C:\Users\zyx\AppData\Roaming\Typora\typora-user-images\image-20230329102600202.png)]
4.实现MyBatis的相关xml配置文件,来自动实现数据库操作
请求
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&password=123
响应
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username:'zhangsan',
score: 1000,
totalCount: 0,
winCount: 0
}
如果登录失败,就返回一个无效的user对象。
如果这里的每个属性都是空的,像userId => 0
请求
POST/register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=zhangsan&password=123
响应
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username:'zhangsan',
score: 1000,
totalCount: 0,
winCount: 0
}
这个前后端交互的接口,在约定的时候,是有很多种交互方式的
这里约定好了之后,后续的前端或后端代码,都要严格地遵守这个约定来写代码
程序运行过程中,用户登陆了之后,让客户端随时通过这个接口,来访问服务器,获取自身的信息
请求
GET/userInfo HTTP/1.1
响应
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username:'zhangsan',
score: 1000,
totalCount: 0,
winCount: 0
}
package com.example.java_gobang.api;
import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
public class UserAPI {
@Resource
private UserMapper userMapper;
@PostMapping("/login") //请求使用的是POST
@ResponseBody //将java对象转为json格式的数据
public Object login(String username, String password, HttpServletRequest req){
//关键操作:根据username去数据库中进行查询
//如果能找到匹配的用户,并且密码也一致,就认为登陆成功
User user = userMapper.selectByName(username);
System.out.println("[login] user=" + username);
if(user == null || !user.getPassword().equals(password)){
//登陆失败
System.out.println("登陆失败");
return new User();//无效对象
}
HttpSession httpSession = req.getSession(true);
//参数true的含义:会话存在直接返回,会话不存在就创建一个
//参数false的含义:会话存在直接返回,会话不存在就返回空
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 e){
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();
}
}
}
使用Postman测试登录:
用户名和密码都正确
用户名和密码不正确(返回空值)
使用Postman测试注册:
使用Postman测试用户信息:
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/cat.jpg);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.nav {
height: 50px;
background-color: rgba(5, 5, 28,0.7);
color: white;
line-height: 50px;
padding-left: 20px;
}
login.css
.login-container {
height: calc(100% - 50px);
display: flex;
justify-content: center;
align-items: center;
}
.login-dialog {
width: 400px;
height: 400px;
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;
align-items: center;
justify-content: center;
}
.login-dialog .row span {
width: 100px;
font-weight: 700;
}
#username,#password {
width: 200px;
height: 40px;
font-size: 20px;
line-height: 40px;
padding-left: 10px;
border: none;
outline: none;
border-radius: 10px;
}
#submit {
width: 300px;
height: 50px;
background-color: rgb(0,129,0);
color: whitesmoke;
border: none;
outline: none;
border-radius: 10px;
margin-top: 20px;
}
#submit:active {
background-color: rgb(6,6,6);
}
实现登录的具体过程
使用ajax,使页面和服务器之间进行交互
<script src="./js/jquery.min.js">script>
<script>
// 通过 ajax 的方式实现登录过程
let submitButton = document.querySelector('#submit');
submitButton.onclick = function() {
// 1. 先获取到用户名和密码
let username = document.querySelector('#username').value;
let password = document.querySelector('#password').value;
$.ajax({
method: 'post',
url: '/login',
data: {
username: username,
password: password
},
success: function(data) {
console.log(JSON.stringify(data));
if (data && data.userId > 0) {
// 登录成功, 跳转到游戏大厅
alert("登录成功!")
location.assign('/game_hall.html');
} else {
alert("登录失败! 用户名密码错误! 或者该账号正在游戏中!");
}
}
});
}
script>
实现注册的具体过程
使用ajax,使页面和服务器之间进行交互
<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: '/register',
data: {
username: usernameInput.value,
password: passwordInput.value,
},
success: function(body){
//如果注册成功,就会返回一个新注册好的用户对象
if(body && body.username) {
//注册成功!
alert("注册成功!");
location.assign('/login.html');
}else {
alert("注册失败!");
}
},
error: function() {
alert("注册失败!");
}
});
}
script>
让多个用户,在游戏大厅中能够进行匹配,系统会把实力相近的两个玩家凑成一桌,进行对战
约定前后端交互接口
玩家发送匹配请求,这个事情是确定(点击了匹配按钮,就会发送匹配请求)服务器啥时候告知玩家匹配结果(到底排到了谁)
需要等待匹配结束的时候才告知
正因为服务器自己也不知道啥时候能够告知玩家匹配的结果,因此就需要依赖消息推送机制当服务器这里匹配成功之后,就主动的告诉当前排到的玩家‘你排到了
接下来约定的前后端交互接口,也是基于websocket来展开的
websocket可以传输文本数据,也能传输二进制数据
此处就直接设计成让websocket传输json格式的方式即可
客户端通过websocket给服务器发送一个json格式的文本数据
ws://127.0.0.1:8080/findMatch
{
message:'startMatch' / 'stopMatch', //开始或结束匹配
}
在通过websocket传输请求数据的时候,数据中是不必带有用户身份信息的
当前用户的身份信息,在前面登陆完成后,就已经保存到HttpSession中了
websocket里,也是能拿到之前登录好的HttpSession中的信息的
ws://127.0.0.1:8080/findMatch
{
ok:true, //匹配成功
reason:'',//匹配如果失败,失败原因的信息
message: 'startMatch' / 'stopMatch',
}
这个响应是客户端给服务器发送匹配请求之后,服务器立即返回的匹配响应
ws://127.0.0.1:8080/findMatch
{
ok:true, //匹配成功
reason:'',//匹配如果失败,失败原因的信息
message: 'matchSuccess',
}
这个响应是真正匹配到对手之后,服务器主动推送回来的消息
匹配到的对手不需要在这个响应中体现,仍然都放到服务器这边来保存即可
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>
<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
+ "
比赛场次:" + body.totalCount + " 获胜场数:" + body.winCount
},
error: function() {
alert("获取用户信息失败!");
}
});
script>
body>
html>
.container {
width: 100%;
height: calc(100% - 50px);
display: flex;
align-items: center;
justify-content: center;
}
#screen {
width: 400px;
height: 200px;
font-size: 20px;
background-color: gray;
color: white;
border-radius: 10px;
text-align: center;
line-height: 100px;
}
#match-button {
width: 400px;
height: 50px;
font-size: 20px;
color: white;
background-color: orange;
border: none;
outline: none;
border-radius: 10px;
text-align: center;
line-height: 50px;
margin-top: 20px;
}
#match-button:active {
background-color: bisque;
}
![在这里插入图片描述\
JSON字符串和JS对象的转换
JSON字符串转成JS对象
JSON.parse
JS对象转成JSON字符串
JSON.stringify
JSON字符串和Java对象的转换
JSON字符串转成Java对象
ObjectMapper.readValue
Java对象转成JSON字符串
ObjectMapper.writerValueAsString
在注册websocket API的时候,就需要把前面准备好的HttpSession给搞过来(搞到Websocket的Session中!)
用户登录就会给HttpSession中保存用户的信息
此处需要能够保存和表示用户上线和下线的状态
之所以要维护用户的在线状态,目的就是为了能够在代码中比较方便的获取到某个用户
当前的websocket会话,从而可以通过这个会话来给这个客户端发送消息,同时也可以感知到他的在线/离线状态
使用哈希表来保存当前用户的在线状态
key就是用户id
value就是用户当前使用的websocket会话
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
先通过ObjectMapper
把MatchResponse
对象转成JSON
字符串,然后再包装上一层TextMessage
再进行传输
其中TextMessage
就表示一个文本格式的websocket
数据包
当前是使用HashMap来存储用户的在线状态
如果是多线程访问同一个HashMap就容易出现线程安全问题
如果同时有多个用户和服务器连接/断开连接,此时服务器就是并发的针对HashMap进行修改
private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
当浏览器1建立websocket请求时,服务器这边会在OnlineUserManager中保存键值对:userId=1,WebSocketSession=session1
当浏览器2建立websocket请求时,服务器这边会在OnlineUserManager中保存键值对:userId=1,WebSocketSession=session2
这两次连接,尝试往哈希表中存储两个键值对,这两个键值对的key是一样的 后来的value会覆盖之前value
上述这种覆盖,就会导致第一个浏览器的连接“名存实亡”已经拿不到对应的WebSocketSession了,也就无法给这个浏览器推送数据了
多开会产生上述问题,我们的程序是否应该允许多开呢?
对于大部分游戏来说,都是不行的!都是禁止多开的,禁止同一个账号在不同的主机上登录!
因此我们呢要做的,不是直接解决会话覆盖的问题,而是从源头上禁止游戏多开!
1)账号登陆成功之后,禁止在其他地方再登录(采用这种方法)
2)账号登陆之后,后续其他位置的登录会把前面的登录给踢掉
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//玩家上线,加入到OnlineUserManager中
//1.先获取到当前用户的身份信息(谁在游戏大厅中建立连接)
// 此处的代码,之所以能够getAttributes 全靠了在注册Websocket的时候
// 加上的 .addInterceptors(new HttpSessionHandshakeInterceptor());
// 这个逻辑 就把 HttpSession 中的 Attribute 都给拿到 webSocketSession 中了
// 在Http登录逻辑中,往HttpSession中存了User数据 httpSession.setAttribute("user",user);
// 此时就可以在webSocketSession中把之前 HttpSession中存的User对象给拿到了
// 注意。此处的user是有可能为空的!!
// 如果之前用户压根就没有通过HTTP来进行登录,直接就通过/game_hall.html这个url来访问游戏大厅页面
// 此时就会出现user为null的情况
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)));
}
}
在连接建立逻辑这里,做出了判定;如果玩家已经登陆过,就不能再登录,同时关闭websocket
连接
websocket
连接关闭的过程中,也会触发afterConnectionClosed
在这个方法里,会有一个exitGameHall
从带匹配的玩家中,选出分数尽量相近的的玩家
把所有玩家按照分数,分为三类:
Normal score<2000
High score>=2000 &&score<3000
VeryHigh score>=3000
给这三个等级,分配三个不同的队列
根据当前玩家的分数,来把这个玩家的用户信息,放到对应的队列里
接下来在搞一个专门的线程,去不停的扫描这个匹配队列
只要说队列里的元素(匹配中的玩家)凑成了一对,把这一对玩家取出来,放到一个游戏房间中
入队列:
取元素:
删除元素:
使用到多线程的代码时,一定要时刻注意“线程安全”问题
使用sunchronized
进行加锁
需要指定一个锁对象,到底针对谁进行加锁?只有多个线程在尝试针对同一个锁对象进行加锁的时候,才会有互斥效果
此处我们进行加锁的时候,如果多个线程访问的是不同的队列,不涉及线程安全问题。必须得是多个线程操作同一个队列,才需要加锁
因此在加锁的时候选取的锁对象,就是normalQueue,highQueue,veryHighQueue这三个队列对象本身
如果当前匹配队列中,就只有一个元素,或者没有元素,会出现什么效果?
在这个代码中,就会出现handlerMatch
一进入方法就会快速返回,然后再次进入方法 循环速度飞快 但是却没有实质的意义。这个过程中CPU占用率会非常高(忙等)
在调用完handlerMatch
之后,加上sleep(500)
这个方案确实可以,但是当有玩家匹配到之后,可能要500ms之后才能正在得到匹配的返回结果
通过sleep
难以两全齐美,要么让玩家多等,要么让CPU多转
因此我们使用wait/notify
当真正有玩家进入匹配队列之后,就调用notify
来唤醒线程
一个游戏服务器上,又同时存在了多个游戏房间~
需要一个“游戏房间管理器”管理多个游戏房间~
键值对,给每个room也生成一个唯一的roomId~
以键值对(哈希表)在room manager中进行管理
UUID
表示“世界上唯一的身份标识”
通过一系列的算法,能够生成一串字符串(一组十六进制表示的数字)
两次调用这个算法,生成的这个字符串都是不相同的
任意次调用,每次得到的结果都不相同
UUID
内部具体如何实现的(算法实现细节)不去深究 Java中有现成的类
关于RoomManager
希望能够根据房间id找到房间对象,也希望能够根据玩家id,找到玩家所属的房间
通过调试目前代码 发现问题:
问题1:
当前发现玩家点击匹配之后,匹配按钮的文本不发生改变
分析之前写过的代码,点击按钮的时候,仅仅是给服务器发送了websocket请求,告诉服务器我要开始匹配了~
服务器会立即返回一个响应,“进入匹配队列成功”,然后页面再修改按钮的文本
出现问题的原因:
服务器这边在处理匹配请求时,按理说,要立即返回一个**websocket
**响应
实际上在服务器代码这里构造了响应对象,但是忘记sendMessage
给发回去了
解决方法:
在MatchAPI
中加入如下两行代码
String jsonString = objectMapper.writeValueAsString(response);
session.sendMessage(new TextMessage(jsonString));
验证匹配功能的时候,模拟多个用户登录的情况,最好使用多个浏览器,避免同一个浏览器中的cookie/session信息干扰
问题2:
出现异常
检查发现原因:
在创建objectMapper时,未对其进行实例化
更改之后 出现404:
正常情况 因此我们目前还未创建 127.0.0.1:8080/game_room.html
可以看到我们使用两个浏览器登录zhangsan的账号后,页面上并没有什么提示"多开"响应 仅仅是打开控制台才能看到
但是在第二个浏览器窗口点击”开始匹配“按钮时,会显示 连接已断开请重新登录
当前我们虽然能够禁止一个账户的多开效果(主要是禁止在多个客户端进行匹配),但是在界面上没有一个明确的提示
此处需要调整前端代码,出现多开时,给客户一个明显的提示
更改之后,在第二次登录zhangsan账户时,会立马显示”当前和服务器的连接已经断开!请重新登录!“,并跳转回登录界面
另外,这里修改了js代码,再刷新页面的时候要使用ctrl+f5 强制刷新,否则应用的还是旧版本的js代码
点击开始匹配之后
1)先触发js中的按钮的点击事件回调
这里会发送一个websocket
的请求给服务器
2)服务器处理这个匹配请求
此处的payload数据就是上面的websocket
发送的JSON
数据,将客户端发送的数据,服务端读了出来,然后对其进行一个解析,解析为一个MatchRequest
对象,这个对象中就包含了一个关键的字段
拿message里面的内容进行判断,看是startMatch
还是stopMatch
使用matcher.add(user)
把玩家加入到匹配队列中
服务器立即给客户端返回一个响应,告知客户端,已经把用户加入到匹配队列中
3)客户端收到服务器返回的响应之后,就会立即进行处理
其中resp
就是上面我们所受到的response
对象
4)匹配器的处理
由于当前只有一个玩家,点击了开始匹配,此时队列中也就只有一个元素 因此扫描线程 会在wait处堵塞
5)此时又有一个玩家也点击了匹配操作
这里的流程同刚才的123一样
当又有一个玩家点击匹配之后,就会从匹配队列中的wait中返回 于是继续执行匹配逻辑
匹配器匹配到多个玩家就会创建一个房间 把房间加入到房间管理器中
给两个哈希表中都去添加键值对的内容
添加三组映射:
1.房间ID到房间对象的映射
2.玩家1的ID到房间ID的映射
对战模块和匹配模块使用的是两套逻辑,使用的是不同的websocket的路径进行处理,可以做到更好的解耦合~
ws://127.0.0.1:8080/game
建立连接响应
服务器要生成一些游戏的初始信息,通过这个响应告诉客户端
{
message: 'gameReady', //消息的类别是游戏就绪
ok: true,
reason:'',
roomId:'12345678', //玩家所处在的房间id
thisUserId: 1, //玩家自己的id
thatUserId: 2 //玩家对手的id
whiteUser:1 //那个玩家执白子(先手)
}
这些都是玩家匹配成功之后,要有服务器生成的内容,把这个内容返回到浏览器中
请求
此处更建议大家使用行和列 而不是坐标x和y
后面的代码中需要使用二维数组来表示这个棋盘 通过下表取二维数组元素[row][col]
如果使用x,y [y][x]
感觉比较奇怪
{
message: 'putChess',
userId:1,
row:0,
col:0, //落子的坐标,往哪一行,哪一列来落子
}
响应
{
message: 'putChess',
userId: 1,
row: 0,
col: 0,
winner:0 //winner表示当前是否分出胜负 如果winner为0,表示胜负未分,还需要继续对战,如果winner非0,则表示当前的获胜方的id
}
以上交互接口的设计,其实也不一定非得按照我们写的这种格式约定,我们也就而已使用其他的约定方式
不管是哪种格式,只要能够解决我们的问题,只要简约方便就可以了
这个页面就是匹配成功之后,要跳转到的新页面
canvas
是HTML5引入的一个标签 “画布” 可以在画布上画画
此处的棋盘和棋子都是画上去的
canvas
这个标签有一组配套的js的canvas api,通过这个api就可以实现一些“画画”的效果
例如,展示一个棋盘,就画很多的直线,就能构成棋盘的网格
表示一个棋子,就画一个圆圈,并且填充上颜色
还需要响应点击事件,在鼠标落子的地方来画圆圈
阅读一下script.js
表示当前游戏中的棋盘,通过这个棋盘来表示当前哪个位置有子了
当前玩家点击的时候,如果有子的位置就不能继续落子了
0表示空闲位置,非0表示有子了
drawImage()
是指把图片画上去
initChessBoard()
绘制棋盘
针对chess(棋盘canvas)设定了点击回调
点击回调中的事件参数 这里就会记录点击的实际位置(坐标)
match.floor()
这里是为了让点击操作能够对应到网格线上~
总体的棋盘尺寸是450px*450px 整个棋盘上是15行,15列 每一行每一列占用的尺寸就是30px
oneStep()
走一步(里面会绘制一个棋子)
最终实现的页面结果:
之前已经写了一个OnlineUserManager
对象了 也确实能够管理用户的在线状态 但是这个状态仅仅是局限于game_hall
这个页面中 现在是在game_room
中
之前在退出game_hall
页面的时候,就会断开
的连接 也就会在服务器的OnlineUserManager
中删除对应的元素
但凡是服务器端得开发,尤其是多个客户端来并发访问服务器,访问同一个数据的时候,就可能引发线程安全问题
这一段逻辑就可以视为多线程环境(两个客户端是并发连入的)
如果恰好是两个客户端同时执行到这个逻辑if(room.getUser1() == null)
,此时就会出现问题,玩家1和玩家2都会认为自己是先手方
因此就需要把这里的逻辑判定 使用锁保护起来 避免多个客户端都认为自己是玩家1
接下来需要考虑加锁对象是谁??
原则是,要竞争的资源是什么,就对谁加锁
(对谁加锁 针对这个对象访问的时候才有互斥效果)
在这个逻辑里是多个玩家/线程,在同时 访问/修改 同一个room
对象~就需要针对room对象来加锁
客户端代码中尝试获取响应中的isWhite
字段
实际的响应数据中,根本就没有isWhite
字段 有的只是whiteUser
字段
在script.js
里增加代码
function send(row, col) {
let req = {
message: 'putChess',
userId: gameInfo.thisUserId,
row: row,
col: col
};
websocket.send(JSON.stringify(req));
}
注意:客户端和服务器两边的二维数组的区别
服务器这边的数组元素有三种状态:
服务器这边的二维数组,要起到的效果是进行判定胜负,要知道玩家1和玩家2的子落在哪里
客户端这边的数组元素只有两种状态:
客户端的二维数组只是用来判定这个位置有没有子 无0有1,只是为了避免出现重复落子的情况 一个位置落子多次
如果直接在客户端来判定胜负关系,是否可行呢?
不太可行 游戏中的关键逻辑一般还是要交给服务器来进行(防止外挂)
外挂的工作过程就是 篡改客户端这边的逻辑
发现此处有一个问题,空指针异常。深深的怀疑onlineUserManager
为空
//要想给用户发送 websocket 数据,就需要获取到这个用户 WebSocketSession
WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
当前这两个属性在Room
这个类里面
//引入OnlineUserManager
@Autowired
private OnlineUserManager onlineUserManager;
//引入RoomManager,用来房间销毁
private RoomManager roomManager;
但是Room
类自身不是Spring
组件,没有被注册进去,自然@Autowired
无法生效
如果这么写,就成了单例了,Room
显然不应该是单例,应该是多例
此处Room
是已经被我们手动管理起来了:RoomManager
当前显然,Room
不应该作为Spring
中的组件~又希望能够从Spring
中拿到对应的onlineUerManager
和roomManager
就需要通过手动注入的方式来获取到实例了
判定棋面上是否出现五子连珠
一行,一列,一个对角线
因此我们不需要判定整个棋盘,只需要以row
、col
这个位置为中心,判定周围若干个格子
如果棋盘上出现了五子连珠,一定是和新落子的位置是相关的
五种一行五子连珠的情况:假设x是我们新落的子
假设这一行中最左侧的点,
第一个点 r,c
第二个点 r,c+1
第三个点 r,c+2
第四个点 r,c+3
第五个点 r,c+4
最左边第一个点的运动范围,此处(row,col)
这个位置是玩家这次的落子位置
第一种情况:
最左边点:r=row,c=col-4
第二种情况:
最左边点:r=row,c=col-3
第三种情况:
最左边点:r=row,c=col-2
第四种情况:
最左边点:r=row,c=col-1
第五种情况:
最左边点:r=row,c=col
//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;
}
}
坐标假设为 row,col
,五种情况如下:
一 二 三 四 五
a a a a x
b b b x a
c c x b b
d x c c c
x d d d d
假设这一列中最上面的点,坐标为
第一个点:r,c
第二个点:r+1,c
第三个点:r+2,c
第四个点:r+3,c
第五个点:r+4,c
最上边第一个点的运动范围,此处(row,col)
这个位置是玩家这次的落子位置
第一种情况:
最左边点:r=row-4,c=col
第二种情况:
最左边点:r=row-3,c=col
第三种情况:
最左边点:r=row-2,c=col
第四种情况:
最左边点:r=row-1,c=col
第五种情况:
最左边点:r=row,c=col
//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;
}
}
胜负未分 直接返回0return 0;
问题:
1.玩家比赛完成之后,比赛的胜负场数和分数没有改变
2.玩家掉线的情况,需要通知对手,你自动获胜
落子这里,对对方是否在线做过检测 但是这个检测是在落子的时候做的检测
刚才发现,当进行完一局游戏之后,分数没有顺利的被更新
刚才的逻辑中,主要是两部分:
1.一局游戏进行完之后,需要把信息写入数据库
2.返回到游戏大厅之后,要重新从数据库来获取
此处的关键点,就是数据库里面的内容对不对~
客户端的这个代码,实现了从服务器获取玩家信息的操作
分析到这里,就知道了~当前从服务器拿信息的这个接口,获取到的user对象不是数据库中的最新对象,而是之前在登录过程中,往session里存的user对象
后续我们已经更新了数据库的内容,但是session里的user没有发生改变
此处的解决方案:
根据当前的session中拿到的user对象,重新查询数据库
获取到的user对象才返回给客户端
更改:
在进行了一局之后
assign
更换为replace
游戏界面中 点击回退直接回退到登陆界面
1.先把数据库中的数据给构造好
2.微调页面(websocket建立连接的url进行调整)
如果服务器就在浏览的本机上,可以这么写~
如果服务器程序部署到其他机器上,此时就不能使用127.0.0.1了,而需要指定不同机器的ip
服务器部署到那个机器上,就需要制定哪个ip
这个ip就要写成云服务器的外网ip
此处我们要修改代码,让这个ip能够适应不同的主机
3.打包,并进行上传
借助maven来打包
命名为 zyx_gobang
4.运行程序,通过外网进行访问
使用命令 java -jar zyx-gobang.jar
启动java包