文章目录
1. 项目设计
前端 :
HTML + CSS + JavaScript + Jquery + AJAX
后端 :
Spring MVC + Spring Boot + MyBatis
2. 效果图展示
3. 创建项目以及配置文件
3.1 创建项目
3.2 配置文件
3.2.1 在 application.properties 中添加配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/onlineGobang?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
3.2.2 在 resources 目录下创建mapper
mapper下添加 目录 **.xml 并添加代码
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.onlinemusicserver.mapper."对应的Mapper""></mapper>
4. 数据库设计与实现
这里使用数据库存储每一个用户的信息, 初始的时候, 天梯分和场次都是默认的.
createdatabaseifnotexists onlineGobang;use onlineGobang;droptableifexistsuser;createtableuser(
userId intprimarykeyauto_increment,
username varchar(20)unique,
password varchar(255)notnull,
score int,
totalCount int,
winCount int);
5. 登录注册模块
5.1 设计登录注册交互接口
登录功能
请求
POST /user/login HTTP/1.1
{username: "",password: ""}
响应
{
status: 1/-1,
message: "",
data: ""
}
注销功能
请求
GET /user/logout HTTP/1.1
响应
HTTP/1.1 200
注册功能
请求
POST /user/register HTTP/1.1
{username: "",password: ""}
响应
{
status: 1/-1,
message: "",
data: ""
}
5.2 设置登录注册功能返回的响应类
通过这个类, 方便前端接收内容
@DatapublicclassResponseBodyMessage<T>{privateint status;privateString message;privateT data;publicResponseBodyMessage(int status,String message,T data){this.status = status;this.message = message;this.data = data;}}
5.3 使用 BCrypt 对密码进行加密
在 pom.xml中添加依赖
<!-- security依赖包 (加密)--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency>
在启动类中添加注解
@SpringBootApplication(exclude ={org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
在 cofig 包下, 创建一个类 AppConfig.
@ConfigurationpublicclassAppConfigimplementsWebMvcConfigurer{@BeanpublicBCryptPasswordEncodergetBCryptPasswordEncoder(){returnnewBCryptPasswordEncoder();}}
5.4 完成 MyBatis 操作
在 model 包中, 创建 User 实体类
@DatapublicclassUser{privateint userId;privateString username;privateString password;privateint score;privateint totalCount;privateint winCount;}
在 mapper 包中, 创建 UserMapper 接口
这个接口中 主要是完成
- 注册, 插入一个用户
- 登录的时候, 通过名字查询当前用户是否存在.
@MapperpublicinterfaceUserMapper{// 注册一个用户, 初始的天梯积分默认为1000, 场次默认为0intinsert(User user);// 通过username查询当前用户是否存在UserselectByName(String username);}
在 resources 目录下, 创建一个目录 mapper, 在目录下创建 UserMapper.xml
在 UserMapper.xml 中写对应UserMapper接口中对应的操作
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.example.gobang.mapper.UserMapper"><insertid="insert">
insert into user values(null,#{username},#{password},1000,0,0)
</insert><selectid="selectByName"resultType="com.example.gobang.model.User">
select * from user where username = #{username}
</select></mapper>
创建 service 包, 在包下创建 UserService 类, 这个类调用 Mapper接口中的方法
@ServicepublicclassUserService{@AutowiredprivateUserMapper userMapper;publicintinsert(User user){return userMapper.insert(user);}publicUserselectByName(String username){return userMapper.selectByName(username);}}
5.5 后端的实现
创建 controller包, 在包下创建一个 UserController 类
这个类是实现登录模块的功能的
- 这里需要注入 UserService, 调用数据库中的方法
- 还需要注入 BCryptPasswordEncoder, 对密码进行加密和比较
5.5.1 登录功能后端实现
注意这里的登录.
- 首先去数据库根据用户名查询是否存在当前用户.
- 如果不存在, 登录失败.
- 如果存在, 用输入的密码, 和数据库中的密码进行比较, 看是否相等. (注: 数据中的密码是加密的)
- 如果不相等, 登录失败.
- 如果相等, 创建 session, 并登录成功.
@RequestMapping("/login")publicResponseBodyMessage<User>login(@RequestBodyUser user,HttpServletRequest request){User truUser = userService.selectByName(user.getUsername());if(truUser ==null){System.out.println("登录失败!");returnnewResponseBodyMessage<>(-1,"用户名密码错误!",user);}else{boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());if(!flg){returnnewResponseBodyMessage<>(-1,"用户名密码错误!",user);}System.out.println("登录成功!");HttpSession session = request.getSession(true);
session.setAttribute(Constant.USER_SESSION_KEY,truUser);returnnewResponseBodyMessage<>(1,"登录成功!",truUser);}}
5.5.2 注册功能后端实现
- 首先查看是否该用户是否存在
- 存在, 就注册失败
- 不存在, 就进行注册, 首先对当前密码进行加密.
- 加密之后对这个用户添加到数据库中.
@RequestMapping("/register")publicResponseBodyMessage<User>register(@RequestBodyUser user){User truUser = userService.selectByName(user.getUsername());if(truUser !=null){returnnewResponseBodyMessage<>(-1,"当前用户名已经存在!",user);}else{String password = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(password);
userService.insert(user);returnnewResponseBodyMessage<>(1,"注册成功!",user);}}
5.5.3 注销功能
直接删除对应session 为
Constant.USER_SESSION_KEY
, 然后跳转到
login.html
@RequestMapping("/logout")publicvoiduserLogout(HttpServletRequest request,HttpServletResponse response)throwsIOException,IOException{HttpSession session = request.getSession(false);// 拦截器的拦截, 所以不可能出现session为空的情况
session.removeAttribute(Constant.USER_SESSION_KEY);
response.sendRedirect("login.html");}
注意: 这里的
Constant.USER_SESSION_KEY
是存储的 session 字符串, 由于该 字符串是不变的, 所以存入 Constant 类中.
5.6 前端的实现
5.6.1 登录前端实现
let loginButton = document.querySelector('#loginButton');
loginButton.onclick=function(){let username = document.querySelector('#loginUsername');let password = document.querySelector('#loginPassword');if(username.value.trim()==""){alert('请先输入用户名!');
username.focus();return;}if(password.value.trim()==""){alert('请先输入密码!');
password.focus();return;}
$.ajax({url:"user/login",method:"POST",data:JSON.stringify({username: username.value.trim(),password: password.value.trim()}),contentType:"application/json;charset=utf-8",success:function(data, status){if(data.status ==1){
location.assign("index.html");}else{alert(data.message);
username.value="";
password.value="";
username.focus();}}})}
5.6.2 注册前端实现
let Reg = document.querySelector('#Reg');
Reg.onclick=function(){let username = document.querySelector('#RegUsername');let password1 = document.querySelector('#RegPassword1');let password2 = document.querySelector('#RegPassword2');if(!$('#checkbox').is(':checked')){alert("请勾选条款");return;}if(username.value.trim()==""){alert("请先输入用户名!");
username.focus();return;}if(password1.value.trim()==""){alert('请先输入密码!');
password1.focus();return;}if(password2.value.trim()==""){alert('请再次输入密码!');
password2.focus();return;}if(username.value.trim().length >20){alert("用户名长度过长");
username.value="";
username.focus();return;}if(password1.value.trim()!= password2.value.trim()){alert('两次输入的密码不同!');
passwrod1.value="";
password2.value="";return;}if(password1.value.trim().length >255){alert("当前密码长度过长!");
password1.value="";
password2.value="";
password1.focus();return;}
$.ajax({url:"user/register",method:"POST",data:JSON.stringify({username: username.value.trim(),password: password1.value.trim()}),contentType:"application/json;charset=utf-8",success:function(data,status){if(data.status ==1){alert(data.message);
location.assign("login.html");}else{alert(data.message);
username.value="";
password1.value="";
password2.value="";
username.focus();}}})}
5.7 添加拦截器
LoginIntercepter
publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{HttpSession session = request.getSession(false);if(session !=null&& session.getAttribute(Constant.USER_SESSION_KEY)!=null){returntrue;}
response.sendRedirect("/login.html");returnfalse;}}
AppConfig
@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer,WebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){LoginInterceptor loginInterceptor =newLoginInterceptor();
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/**/login.html").excludePathPatterns("/**/css/**.css").excludePathPatterns("/**/images/**").excludePathPatterns("/**/fonts/**").excludePathPatterns("/**/js/**.js").excludePathPatterns("/**/scss/**").excludePathPatterns("/**/user/login").excludePathPatterns("/**/user/register").excludePathPatterns("/**/user/logout");}}
6. 大厅界面
6.1 交互接口设计
这里客户端1, 点击匹配发送消息给服务器, 客户端2, 也点击匹配发送消息给服务器, 当服务器收到两个人的请求之后, 就需要服务器主动向客户端发送消息, 这里就需要用到 websocket
URL: ws://127.0.0.1:8080/findMatch
匹配请求
{
message: ' startMatch ' / ' stopMatch'
}
匹配响应1 这个响应是点击匹配之后, 立刻返回的响应
{
status: '1' / '-1'
message: ' startMatch ' / ' stopMatch '
}
匹配响应2 这个响应是匹配成功之后的响应
{
status: '1' / '-1'
message: 'matchSuccess'
}
6.2 用户加载前后交互接口
请求
GET /user/userInfo HTTP/1.1
响应
{
status: 1/-1 (1 为成功, -1 为失败),
message: "对应信息",
data: "内容", (用户信息)
}
6.3 前端和后端实现用户信息加载
6.3.1 后端的实现
根据当前存储的session对象, 来查找对应的用户
@RequestMapping("/userInfo")publicResponseBodyMessage<User>getUserInfo(HttpServletRequest request){HttpSession session = request.getSession(false);User user =(User) session.getAttribute(Constant.USER_SESSION_KEY);if(user ==null){returnnewResponseBodyMessage<>(-1,"当前用户不存在",null);}else{returnnewResponseBodyMessage<>(1,"查找成功!", newUser);}}
6.3.2 前端的实现
load();functionload(){
$.ajax({url:"user/userInfo",method:"GET",success:function(data){if(data.status ==1){let h2 = document.querySelector('#myname');
h2.innerHTML ="你好! "+ data.data.username;let game = document.querySelector('#gameMes');
game.innerHTML ="天梯分数: "+ data.data.score +" | "+"场数: "+ data.data.totalCount +" | "+"获胜场数: "+ data.data.winCount;}else{alert(data.message);
location.assign("login.html");}}})}
6.4 实现匹配功能的前端代码
let websocketUrl ='ws://'+ location.host +'/findMatch';let websocket =newWebSocket(websocketUrl);// 连接成功的时候调用的方法
websocket.onopen=function(){
console.log("onopen");}// 连接关闭的时候调用的方法
websocket.onclose=function(){
console.log("onclose");}// 连接异常的时候调用的方法
websocket.onerror=function(){
console.log("onerrot");}// 监听整个窗口关闭的事件, 当窗口关闭, 主动的去关闭websocket连接
window.onbeforeunload=function(){
websocket.close();}// 连接成功收到的响应
websocket.onmessage=function(e){// 先将Json格式 e 化为 响应对象let resp =JSON.parse(e.data);// 获取到 匹配按钮let play = document.querySelector('#beginPlay');// 等于-1是错误的起来, 打印错误的信息, 并跳转到登录页面if(resp.status ==-1){alert(resp.message);
location.assign("login.html");return;}// 这里就都是正常的响应, 那么就判断是开始匹配, 还是结束匹配if(resp.message =='startMatch'){//开始匹配
console.log("开始匹配");
play.innerHTML ='匹配中...(点击停止)';}elseif(resp.message =='stopMatch'){//结束匹配
console.log("结束匹配");
play.innerHTML ='开始匹配';}elseif(resp.message =='matchSuccess'){//匹配成功
console.log("匹配成功");
location.assign('room.html');}else{// 按理不会触发这个elsealert(resp.message);
console.log("收到非法响应");}}// 获取到匹配按钮let play = document.querySelector('#beginPlay');// 匹配按钮点击事件
play.onclick=function(){// 判断当前 readyState 是否是OPEN状态的if(websocket.readyState == websocket.OPEN){// 当前 readyState 处于OPEN 状态, 说明链接是好的if(play.innerHTML =='开始匹配'){// 发送开始匹配的请求
websocket.send(JSON.stringify({message:'startMatch',}))}elseif(play.innerHTML =='匹配中...(点击停止)'){// 发送停止匹配的请求
websocket.send(JSON.stringify({message:'stopMatch',}))}}else{// 这里就是链接异常的情况alert('当前您的链接已经断开, 请重新登录');
location.assign("login.html");}}
6.5 实现匹配功能的后端代码
这里是触发url的响应地址
@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer{@AutowiredprivateMatchController matchController;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry){
registry.addHandler(matchController,"/findMatch").addInterceptors(newHttpSessionHandshakeInterceptor());}}
6.5.1 创建在线状态
当用户登录的时候, 就让用户状态添加到哈希表中
由于这里是 多线程的状态下, 很多用户访问同一个哈希表就会出现线程安全的问题, 所以这里就使用 ConcurrentHashMap, 确保了线程安全问题.
- 这里存储的, key是用户的Id, value是对应的WebSocketSession的信息.
- 提供三个方法 - 进入房间的时候, 将用户的状态存入哈希表中- 退出房间的时候, 将用户的状态从哈希表中删除- 获取当前用户的 WebSocketSession 信息
@ComponentpublicclassOnlineUserManager{// 这个哈希表是表示当前用户在游戏大厅的在线状态privateConcurrentHashMap<Integer,WebSocketSession> gameState =newConcurrentHashMap<>();publicvoidenterGameIndex(int userId,WebSocketSession webSocketSession){
gameState.put(userId,webSocketSession);}publicvoidexitGameHall(int userId){
gameState.remove(userId);}publicWebSocketSessiongetState(int userId){return gameState.get(userId);}}
6.5.2 创建房间对象
房间对象, 每一房间中, 会有RoomId, 和2个用户信息.
所以这里需要有一个完全不可重复的RoomId, 这里就使用Java中的 UUID来解决
// 游戏房间@DatapublicclassRoom{privateString roomId;privateUser user1;privateUser user2;publicRoom(){this.roomId = UUID.randomUUID().toString();}}
6.5.3 创建房间管理器
按理 也是使用哈希表存储, 也有线程安全问题, 所以也使用ConcurrentHashMap
提供3个方法
- 添加用户进入到房间
- 删除房间中的用户
- 提供房间Id得到房间对象
// 房间管理器@ComponentpublicclassRoomManager{privateConcurrentHashMap<String,Room> rooms =newConcurrentHashMap<>();publicvoidinsert(Room room){
rooms.put(room.getRoomId(),room);}publicvoidremove(String roomId){
rooms.remove(roomId);}publicRoomfindRoomByRoomId(String roomId){return rooms.get(roomId);}
6.5.4 创建匹配队列
匹配队列, 首先按照分数将用户分为三个等级.
<2000
, 属于简单用户>= 2000 && < 3000
, 属于普通用户>=3000
, 属于高级用户
// 创建匹配队列 按等级划分// 1. < 2000privateQueue<User> simpleQueue =newLinkedList<>();// 2. >= 2000 && < 3000privateQueue<User> normalQueue =newLinkedList<>();// 3. >= 3000privateQueue<User> highQueue =newLinkedList<>();
这里就通过队列来分为为三个等级, 来完成匹配和退出
- 点击匹配的时候, 按照用户当前的等级, 将用户入队
- 取消匹配的时候, 按照用户当前的等级, 将用户从队列中删除
- 创建三个线程, 一直循环的去对应等级队列中进行获取用户, 如果当前队列中的用户有2个以上的时候, 就进行匹配.
这里也有线程安全的问题, 这里同一个队列中, 用户并发的入队, 和删除用户操作, 就会产生线程安全的问题. 如果是不同的队列, 就不涉及线程安全的问题
解决办法: 对于同一个队列中的操作进行加锁.
问题2: 这里的三个线程, 是循环的去等待, 如果当前队列中迟迟没有人进来, 而线程还是循环的执行下去, 这样的资源消耗就非常的大.
所以在进行判断当前用户是否有2个以上的时候, 如果当前用户小于2个, 就将当前的队列进行wait(), 直到再次有用户加入进来的时候,就解锁, 再去判断当前用户是否有2个以上的用户.
// 匹配器, 这个类是用来完成匹配功能的@ComponentpublicclassMatcher{// 创建匹配队列 按等级划分// 1. < 2000privateQueue<User> simpleQueue =newLinkedList<>();// 2. >= 2000 && < 3000privateQueue<User> normalQueue =newLinkedList<>();// 3. >= 3000privateQueue<User> highQueue =newLinkedList<>();@AutowiredprivateOnlineUserManager onlineUserManager;@AutowiredprivateObjectMapper objectMapper;@AutowiredprivateRoomManager roomManager;/**
* 将当前玩家添加到匹配队列中
* @param user
*/publicvoidinsert(User user){// 按等级加入队列中if(user.getScore()<2000){synchronized(simpleQueue){
simpleQueue.offer(user);// 只要有用户进入了, 就进行唤醒
simpleQueue.notify();}}elseif(user.getScore()>=2000&& user.getScore()<3000){synchronized(normalQueue){
normalQueue.offer(user);
normalQueue.notify();}}else{synchronized(highQueue){
highQueue.offer(user);
highQueue.notify();}}}/**
* 将当前玩家匹配队列中删除
* @param user
*/publicvoidremove(User user){// 按照当前等级去对应匹配队列中删除if(user.getScore()<2000){synchronized(simpleQueue){
simpleQueue.remove(user);}}elseif(user.getScore()>=2000&& user.getScore()<3000){synchronized(normalQueue){
normalQueue.remove(user);}}else{synchronized(highQueue){
highQueue.remove(user);}}}/**
* 这里使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配
*/publicMatcher(){// 创建三个线程, 操作三个匹配队列Thread t1 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(simpleQueue);}}};
t1.start();Thread t2 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(normalQueue);}}};
t2.start();Thread t3 =newThread(){@Overridepublicvoidrun(){while(true){handlerMatch(highQueue);}}};
t3.start();}privatevoidhandlerMatch(Queue<User> matchQueue){synchronized(matchQueue){try{// 1. 先查看当前队列中的元素个数, 是否满足两个// 这里使用while, 以防为0的时候, 被唤醒,然后没有再次判断导致进入下面操作.while(matchQueue.size()<2){// 用户小于2个的时候, 就进行等待, 以免浪费资源
matchQueue.wait();}// 2. 尝试从队列中取出两个玩家User player1 = matchQueue.poll();User player2 = matchQueue.poll();// 打印日志System.out.println("匹配到的两个玩家: "+ player1.getUsername()+" , "+ player2.getUsername());// 3. 获取到玩家的 websocket 的会话.WebSocketSession session1 = onlineUserManager.getState(player1.getUserId());WebSocketSession session2 = onlineUserManager.getState(player2.getUserId());// 再次判断是否为空if(session1 ==null&& session2 !=null){
matchQueue.offer(player2);return;}if(session1 !=null&& session2 ==null){
matchQueue.offer(player1);return;}if(session1 ==null&& session2 ==null){return;}if(session1 == session2){
matchQueue.offer(player1);return;}// 4. 把两个玩家放入一个游戏房间中Room room =newRoom();
roomManager.insert(room,player1.getUserId(),player2.getUserId());// 5. 给玩家反馈信息, 通知匹配到了对手MatchResponse response1 =newMatchResponse();
response1.setStatus(1);
response1.setMessage("matchSuccess");String json1 = objectMapper.writeValueAsString(response1);
session1.sendMessage(newTextMessage(json1));MatchResponse response2 =newMatchResponse();
response2.setMessage("matchSuccess");
response2.setStatus(1);String json2 = objectMapper.writeValueAsString(response2);
session2.sendMessage(newTextMessage(json2));}catch(IOException|InterruptedException e){
e.printStackTrace();}}}}
6.5.5 写完 MatchController
websocket 有4个方法.
- 连接成功的时候调用的方法, 这里需要去判断多开的问题, 由于用户同时登录一个账号的时候, 就会出现多开, 解决办法就是查询当前用户的在线状态, 如果当前用户在线, 就退出当前登录. 如果没有多开就设置登陆状态
- 异常关闭的情况, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
- 退出的时候调用的方法, 获取用户的信息, 然后设置在线状态为不在线, 然后删除匹配队列中的用户
- 处理收到请求的方法, 通过前端发来的请求, 判断是否是开始匹配还是停止匹配.
@ComponentpublicclassMatchControllerextendsTextWebSocketHandler{privateObjectMapper objectMapper =newObjectMapper();@AutowiredprivateOnlineUserManager onlineUserManager;@AutowiredprivateMatcher matcher;// 连接成功的时候就会调用该方法@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{// 玩家上线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 2. 判断当前用户是否已经登录if(onlineUserManager.getState(user.getUserId())!=null){// 当前用户已经登录MatchResponse message =newMatchResponse();
message.setMessage("当前用户已经登录!");
message.setStatus(-1);
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(message)));
session.close();return;}// 3. 设置在线状态
onlineUserManager.enterGameIndex(user.getUserId(),session);}@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{// 处理开始匹配 和 停止匹配User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);String payload = message.getPayload();MatchRequest matchRequest = objectMapper.readValue(payload,MatchRequest.class);MatchResponse matchResponse =newMatchResponse();if(matchRequest.getMessage().equals("startMatch")){// 进入匹配队列// 创建匹配队列, 加入用户
matcher.insert(user);// 返回响应给前端
matchResponse.setStatus(1);
matchResponse.setMessage("startMatch");}elseif(matchRequest.getMessage().equals("stopMatch")){// 退出匹配队列// 创建匹配队列, 将用户移除
matcher.remove(user);
matchResponse.setMessage("stopMatch");
matchResponse.setStatus(1);}else{
matchResponse.setStatus(-1);
matchRequest.setMessage("非法匹配");// 非法情况}
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(matchResponse)));}// 异常情况@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{// 玩家下线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session){// 2. 设置在线状态
onlineUserManager.exitGameHall(user.getUserId());}
matcher.remove(user);}// 关闭情况@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{// 玩家下线// 1. 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());if(webSocketSession == session){// 2. 设置在线状态
onlineUserManager.exitGameHall(user.getUserId());}
matcher.remove(user);}}
6.6 大厅界面总结
- 这里要注意多线程环境下, 多个用户同时使用同一个哈希表的时候, 进行添加和删除的时候, 会有线程安全的问题, 那么这里就需要使用 ConcurrentHashMap
- 在多线程环境下, 按照等级分的队列, 在多线程环境下, 并发的进行入队的时候, 删除的队列中用户的时候, 也会有线程安全问题, 这里针对同一个队列就可以进行加锁.
- 由于创建3个线程循环的进入队列中查看是否满足2个用户, 如果当前的环境下, 用户特别少, 一直去循环的进入, 会造成CPU占用率特别高, 所以这里就使用wai()等待, 在有用户进入匹配队列的时候,再去唤醒notify().
- 防止多开, 多个地方登录同一个账号就会出现很多问题, 这里在进行连接的时候判断, 如果用户已经在线, 就不让该地方用户登录.
- 要想让 房间是第一无二, 就需要使用 UUID, 那么roomId也要使用 字符串的格式.
7. 房间界面
7.1 交互接口设计
连接URL
ws://127.0.0.1:8080/game
当双方玩家都已经连接好了 发送响应
{
message: 'gameReady'
status: '1 / -1' (1是正常响应, -1 是错误响应)
roomId: ' '
thisUserId: ' ' (自己用户Id)
thatUserId: ' ' (对方用户Id)
whiteUser: ' ' (先手方)
}
落子的时候的请求
{
message: ' putChess '
userId: ' ' (落子的用户Id)
row: ' ' (落子的第几行)
col: ' ' (落子的第几列)
}
落子的时候的响应
{
message: 'putChess;
userId: ' '
row: ' '
col: ' '
winner: ' ' (获胜者, 和用户Id一致, 如果没有获胜, 就是0)
}
7.2 实现房间界面前端代码
7.2.1 设置棋盘界面, 以及显示框.
这里的 canvas 是用来绘制棋盘的,
room.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>游戏房间</title><linkhref="css/game_room.css"rel="stylesheet"type="text/css"media="all"/></head><body><divclass="container"><divclass="one"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvasid="chess"width="450px"height="450px"></canvas><!-- 显示区域 --><divid="screen"> 等待玩家连接中... </div></div></div><scriptsrc="js/script.js"></script></body></html>
game_room.css
*{margin: 0;padding: 0;box-sizing: border-box;}html, body{height: 100%;background-image:url(../images/bg.jpg);background-repeat: no-repeat;background-position: center;background-size: cover;}.container{height: 100%;display: flex;justify-content: center;align-items: center;}#screen{width: 450px;height: 50px;margin-top: 10px;background-color: #fff;font-size: 22px;line-height: 50px;text-align: center;}.backButton{width: 450px;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;}.backButton:active{background-color: gray;}
7.2.2 对应的js文件
- 这里的
setScreenText
这个方法是用来将显示框中的内容, 根据当前是谁下棋来改变内容.- 这里的
initGame
这个方法是用来初始画棋盘的, 棋盘大小为 15 * 15- 内部的
oneStep
是当点击下子之后, 会绘制对应颜色的棋子.- 注意这里的棋盘数组, 为0是没有落子, 为1是落子了.
- 这里的gameInfo, 内部内容是全局的.用来接收传过来的响应
let gameInfo ={roomId:null,thisUserId:null,thatUserId:null,isWhite:true,}//// 设定界面显示相关操作//functionsetScreenText(me){let screen = document.querySelector('#screen');if(me){
screen.innerHTML ="轮到你落子了!";}else{
screen.innerHTML ="轮到对方落子了!";}}//// 初始化 websocket//// TODO//// 初始化一局游戏//functioninitGame(){// 是我下还是对方下. 根据服务器分配的先后手情况决定let me = gameInfo.isWhite;// 游戏是否结束let over =false;let chessBoard =[];//初始化chessBord数组(表示棋盘的数组)for(let i =0; i <15; i++){
chessBoard[i]=[];for(let j =0; j <15; j++){
chessBoard[i][j]=0;}}let chess = document.querySelector('#chess');let context = chess.getContext('2d');
context.strokeStyle ="#BFBFBF";// 背景图片let logo =newImage();
logo.src ="image/sky.jpeg";
logo.onload=function(){
context.drawImage(logo,0,0,450,450);initChessBoard();}// 绘制棋盘网格functioninitChessBoard(){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 为 truefunctiononeStep(i, j, isWhite){
context.beginPath();
context.arc(15+ i *30,15+ j *30,13,0,2* Math.PI);
context.closePath();var gradient = context.createRadialGradient(15+ i *30+2,15+ j *30-2,13,15+ i *30+2,15+ j *30-2,0);if(!isWhite){
gradient.addColorStop(0,"#0A0A0A");
gradient.addColorStop(1,"#636766");}else{
gradient.addColorStop(0,"#D1D1D1");
gradient.addColorStop(1,"#F9F9F9");}
context.fillStyle = gradient;
context.fill();}
chess.onclick=function(e){if(over){return;}if(!me){return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行let col = Math.floor(x /30);let row = Math.floor(y /30);if(chessBoard[row][col]==0){// TODO 发送坐标给服务器, 服务器要返回结果oneStep(col, row, gameInfo.isWhite);
chessBoard[row][col]=1;}}// TODO 实现发送落子请求逻辑, 和处理落子响应逻辑. }initGame();
7.2.3 初始化 websocket
- 在服务器传过来请求的时候, 两个用户都已经准备好了, 首先判断是否是正确的请求.
- 在请求是正确的时候, 将传过来的信息存入到
gameInfo
中, 注意这里的isWhite 是判断是否是先手方.- 注意只有2个人都建立连接了, 才初始画棋盘, 所以在这里初始化棋盘为好.
- 棋盘绘制好之后, 在显示框中, 显示对应的信息, 调用对应的
setScreenText
方法
let websocketUrl ='ws://'+ location.host +'/game';let websocket =newWebSocket(websocketUrl);
websocket.onopen=function(){
console.log("房间链接成功!");}
websocket.onclose=function(){
console.log("房间断开链接");}
websocket.onerror=function(){
console.log("房间出现异常");}
window.onbeforeunload=function(){
websocket.close();}
websocket.onmessage=function(e){
console.log(e.data);let resp =JSON.parse(e.data);if(resp.message !='gameReady'){
console.log("响应类型错误");
location.assign("index.html");return;}if(resp.status ==-1){alert("游戏链接失败!");
location.assign("index.html");return;}
gameInfo.roomId == resp.roomId;
gameInfo.thisUserId = resp.thisUserId;
gameInfo.thatUserId = resp.thatUserId;
gameInfo.isWhite = resp.whiteUser == resp.thisUserId;// 初始化棋盘initGame();// 设置显示内容setScreenText(gameInfo.isWhite);}
7.2.4 落子时,发送落子请求
在初始化棋盘之后, 在点击的时候, 发送落子请求
注意发送的对应的格式
functionsend(row,col){let req ={message:'putChess',userId: gameInfo.thisUserId,row: row,col: col
};
websocket.send(JSON.stringify(req));}
7.2.5 落子时, 发送落子响应
- 注意这里的响应是在落子之后, 所以要写在initGame() 中
- 在接收的时候, 首先将JSON格式响应转成可以接收的格式
- 判断响应是否正常, 排除响应错误的情况
- 判断当前是自己落子还是对方落子, 然后根据落子绘制棋子
- 落子之后, 交换落子的权利, 然后将显示的内容改变.
- 再次去判断是否游戏结束. 结束的时候,在显示框显示获胜信息, 并添加一个返回大厅的按钮, 以免直接返回了(用户看不到失败的信息.
websocket.onmessage=function(e){
console.log(e.data);let resp =JSON.parse(e.data);if(resp.message !='putChess'){
console.log("响应类型错误!");
location.assign("index.html")return;}if(resp.userId == gameInfo.thisUserId){// 自己落子oneStep(resp.col, resp.row, gameInfo.isWhite);
chessBoard[resp.row][resp.col]=1;}elseif(resp.userId == gameInfo.thatUserId){// 别人落子oneStep(resp.col, resp.row,!gameInfo.isWhite);
chessBoard[resp.row][resp.col]=1;}else{// 落子异常
console.log("userId 异常");return;}// 交换落子
me =!me;setScreenText(me);// 判断游戏是否结束let screenDiv = document.querySelector('#screen');if(resp.winner !=0){
console.log(resp.winner+" "+ gameInfo.thisUserId+" "+ gameInfo.thatUserId);if(resp.winner == gameInfo.thisUserId){
screenDiv.innerHTML ="恭喜你, 获胜了!";}elseif(resp.winner == gameInfo.thatUserId){
screenDiv.innerHTML ="游戏结束, 失败了!";}else{
console.log("winner 错误");alert("当前 winner字段错误 winner = "+ resp.winner);}// location.assign('index.html');// 增加一个按钮, 返回游戏大厅let backBtn = document.createElement('button');
backBtn.innerHTML ="返回游戏大厅";
backBtn.className ="backButton";let one = document.querySelector('.one');
backBtn.onclick=function(){
location.assign("index.html");}
one.appendChild(backBtn);}
7.3 实现房间界面后端代码
7.3.1 注册GameController
@Configuration@EnableWebSocketpublicclassAppConfigimplementsWebSocketConfigurer,WebMvcConfigurer{@AutowiredprivateGameController gameController;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry){
registry.addHandler(gameController,"/game").addInterceptors(newHttpSessionHandshakeInterceptor());}
7.3.2 创建GameController
afterConnectionEstablished
这个方法是在建立连接时候的方法.handleTextMessage
这个方法是接收发送的响应handleTransportError
这个方法是出现异常的时候执行的afterConnectionClosed
这个方法是关闭websocket的时候执行的
@ComponentpublicclassGameControllerextendsTextWebSocketHandler{@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{}@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{}@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{}@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{}}
7.3.3 创建对应的响应类和请求类
双方进入房间准备就绪的响应
// 客户端链接成功后, 返回的响应@DatapublicclassGameReadyResponse{privateString message;privateint status;privateString roomId;privateint thisUserId;privateint thatUserId;privateint whiteUser;}
落子请求
// 落子的请求@DatapublicclassGameRequest{privateString message;privateint userId;privateint row;privateint col;}
落子响应
//落子响应@DatapublicclassGameResponse{privateString message;privateint userId;privateint row;privateint col;privateint winner;}
7.3.4 完成用户房间在线状态管理
在之前的 OnlineUserManager 中添加代码
- enterGameRoom, 进入房间添加到哈希表中(上线)
- exitGameRoom, 退出房间从哈希表中删除(下线)
- getRoomState, 获取当前用户的websocketsession信息
// 这个哈希表是表示当前用户在游戏房间的在线状态privateConcurrentHashMap<Integer,WebSocketSession> roomState =newConcurrentHashMap<>();publicvoidenterGameRoom(int userId,WebSocketSession webSocketSession){
roomState.put(userId,webSocketSession);}publicvoidexitGameRoom(int userId){
roomState.remove(userId);}publicWebSocketSessiongetRoomState(int userId){return roomState.get(userId);}
7.3.5 添加 MyBatis 用来更新玩家积分
UserMapper
// 总场数 + 1, 获胜场数+1, 天梯分数 + 50voiduserWin(int userId);// 总场数 + 1, 天梯分数 -50voiduserLose(int userId);
UserMapper.xml
<updateid="userWin">
update user set totalCount = totalCount+1 , winCount = winCount+1, score = score + 50 where userId = #{userId}
</update><updateid="userLose">
update user set totalCount = totalCount+1, score = score - 50 where userId = #{userId}
</update>
UserService
// 总场数 + 1, 获胜场数+1, 天梯分数 + 50publicvoiduserWin(int userId){
userMapper.userWin(userId);}// 总场数 + 1, 天梯分数 -50publicvoiduserLose(int userId){
userMapper.userLose(userId);}
7.3.6 完成处理连接方法
- 首先获取用户的信息
- 判断当前是否已经进入房间了, 防止未匹配成功
- 判断是否多开, 这里要查询房间在线情况, 和大厅在线情况.
- 然后让用户房间的在线状态处于在线.
- 首先判断用户1是否上线, 上线就添加到当前房间来, 用户2再上线的时候也添加房间来, 这里可以设置谁是先手方, 根据自己设定的规则.我这里是随机取0~9的数字, 如果是偶数用户1就是先手, 如果是奇数用户2就是先手
- 当用户都进入房间的时候, 通知玩家准备就绪了
- 注意这里的线程安全问题. 多个用户进入同一个方法,就有可能出现线程安全问题, 由于是同一个房间的用户进行, 只需要对房间对象加锁就可以了.
@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{GameReadyResponse readyResponse =newGameReadyResponse();// 获取用户信息User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 判断当前是否已经进入房间Room room = roomManager.findRoomByUserId(user.getUserId());if(room ==null){
readyResponse.setStatus(-1);
readyResponse.setMessage("用户尚未匹配到!");
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 判断当前是否多开if(onlineUserManager.getRoomState(user.getUserId())!=null|| onlineUserManager.getState(user.getUserId())!=null){
readyResponse.setMessage("当前用户已经登录!");
readyResponse.setStatus(-1);
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));return;}// 上线
onlineUserManager.enterGameRoom(user.getUserId(), session);synchronized(room){if(room.getUser1()==null){
room.setUser1(user);System.out.println("玩家1 "+ user.getUsername()+" 已经准备好了");return;}if(room.getUser2()==null){
room.setUser2(user);System.out.println("玩家2 "+ user.getUsername()+" 已经准备好了");Random random =newRandom();int num = random.nextInt(10);if(num %2==0){
room.setWhiteUser(room.getUser1().getUserId());}else{
room.setWhiteUser(room.getUser2().getUserId());}// 通知玩家1noticeGameReady(room,room.getUser1(),room.getUser2());// 通知玩家2noticeGameReady(room,room.getUser2(),room.getUser1());return;}}
readyResponse.setStatus(-1);
readyResponse.setMessage("房间已经满了");
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(readyResponse)));}
privatevoidnoticeGameReady(Room room,User user1,User user2)throwsIOException{GameReadyResponse resp =newGameReadyResponse();
resp.setStatus(1);
resp.setMessage("gameReady");
resp.setRoomId(room.getRoomId());
resp.setThisUserId(user1.getUserId());
resp.setThatUserId(user2.getUserId());
resp.setWhiteUser(room.getWhiteUser());WebSocketSession webSocketSession = onlineUserManager.getRoomState(user1.getUserId());
webSocketSession.sendMessage(newTextMessage(objectMapper.writeValueAsString(resp)));}
7.3.7 完成处理连接断开的方法和连接异常的方法
- 首先获取用户的信息
- 然后设置用户房间状态为下线
- 注意这里掉线了, 就需要判断对方赢了. - 判断对方是否掉线, 如果对方也掉线了, 就无需通知谁赢了- 如果对方没有掉线, 就通知对方赢了- 获胜之后, 要对玩家的信息, 场次, 胜场进行更新. 然后关闭房间
@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{// 异常下线// 下线User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session){
onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: "+ user.getUsername()+" 异常下线了");noticeThatUserWin(user);}
@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{// 下线User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());if(exitSession == session){
onlineUserManager.exitGameRoom(user.getUserId());}System.out.println("当前用户: "+ user.getUsername()+" 离开房间");noticeThatUserWin(user);}
privatevoidnoticeThatUserWin(User user)throwsIOException{Room room = roomManager.findRoomByUserId(user.getUserId());if(room ==null){System.out.println("房间已经关闭");return;}// 找到对手User thatUser =(user == room.getUser1())? room.getUser2(): room.getUser1();// 找到对手的状态WebSocketSession session = onlineUserManager.getRoomState(thatUser.getUserId());if(session ==null){// 都掉线了System.out.println("都掉线了, 无需通知");return;}// 这里通知对手获胜GameResponse gameResponse =newGameResponse();
gameResponse.setMessage("putChess");
gameResponse.setUserId(thatUser.getUserId());
gameResponse.setWinner(thatUser.getUserId());
session.sendMessage(newTextMessage(objectMapper.writeValueAsBytes(gameResponse)));// 更新玩家分数信息int winId = thatUser.getUserId();int loseId = user.getUserId();
userService.userWin(winId);
userService.userLose(loseId);// 释放房间对象
roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());}
7.3.8 在房间管理器中添加代码
- 添加哈希表, 管理用户对应的房间号
- key为用户的Id, value为用户的对应房间号
privateConcurrentHashMap<Integer,String>Ids=newConcurrentHashMap<>();publicvoidinsert(Room room,int userId1,int userId2){Ids.put(userId1,room.getRoomId());Ids.put(userId2,room.getRoomId());}publicvoidremove(String roomId,int userId1,int userId2){Ids.remove(userId1);Ids.remove(userId2);}publicRoomfindRoomByUserId(int userId){String roomId =Ids.get(userId);if(roomId ==null){returnnull;}return rooms.get(roomId);}
7.3.9 Room类添加棋盘代码
- 这里的 Constant.ROW 和 Constant.COL 都是不变的常量. 放到 Constant类中. 这里初始化的棋盘数组也是15 * 15的
- 这里Room要注入Spring对象, 不能使用@Autowired @Resource注解. 需要使用context
修改启动类
publicclassGobangApplication{publicstaticConfigurableApplicationContext context;publicstaticvoidmain(String[] args){
context =SpringApplication.run(GobangApplication.class, args);}}
// 游戏房间@DatapublicclassRoom{privateString roomId;privateUser user1;privateUser user2;privateint whiteUser;privateOnlineUserManager onlineUserManager;privateRoomManager roomManager;privateUserService userService;publicRoom(){this.roomId = UUID.randomUUID().toString();
onlineUserManager =GobangApplication.context.getBean(OnlineUserManager.class);
roomManager =GobangApplication.context.getBean(RoomManager.class);
userService =GobangApplication.context.getBean(UserService.class);}// 为0就是为落子, 为1就是用户1落子, 为2就是用户2落子privateint[][] board=newint[Constant.ROW][Constant.COL];privateObjectMapper objectMapper =newObjectMapper();}
7.3.10 实现handleTextMessage方法
落子请求
@OverrideprotectedvoidhandleTextMessage(WebSocketSession session,TextMessage message)throwsException{// 获取用户对象User user =(User) session.getAttributes().get(Constant.USER_SESSION_KEY);// 根据 玩家 Id 获取房间对象Room room = roomManager.findRoomByUserId(user.getUserId());// 通过room对象处理这次请求
room.putChess(message.getPayload());}
7.3.11 实现putChess方法
- 注意这里的落子, 与前端不同, 这里的棋盘数组, 为0就是没落子, 为1就是用户1落得子, 为2就是用户2落得子
- 每次落子都要进行胜负判断, 使用checkWinner方法来实现
- 给房间中的用户返回响应
- 注意这里的玩家掉线的情况
- 如果胜负已分, 更新玩家获胜的信息, 并销毁房间
// 这个方法是用来处理一次落子的操作publicvoidputChess(String reqJson)throwsIOException{// 1. 记录当前落子的位子GameRequest request = objectMapper.readValue(reqJson,GameRequest.class);GameResponse response =newGameResponse();// 1.1 判断当前落子是谁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,chess);// 3. 给房间中所有的客户端返回响应
response.setMessage("putChess");
response.setRow(row);
response.setCol(col);
response.setWinner(winner);
response.setUserId(request.getUserId());WebSocketSession session1 = onlineUserManager.getRoomState(user1.getUserId());WebSocketSession session2 = onlineUserManager.getRoomState(user2.getUserId());// 这里对下线进行判断if(session1 ==null){// 玩家1下线
response.setWinner(user2.getUserId());System.out.println("玩家1掉线");}if(session2 ==null){// 玩家2下线, 就认为玩家1获胜System.out.println("玩家2掉线");}String respJson = objectMapper.writeValueAsString(response);if(session1 !=null){
session1.sendMessage(newTextMessage(respJson));}if(session2 !=null){
session2.sendMessage(newTextMessage(respJson));}// 4. 如果当前获胜, 销毁房间if(response.getWinner()!=0){System.out.println("游戏结束, 房间即将销毁");// 更新获胜方的信息int winId = response.getWinner();intLoseId= response.getWinner()== user1.getUserId()? user2.getUserId(): user1.getUserId();
userService.userLose(LoseId);
userService.userWin(winId);// 销毁房间
roomManager.remove(roomId,user1.getUserId(),user2.getUserId());}}
7.3.12 完成用户胜负判断
这里要判断四种情况
- 一行有五个子连珠
- 一列有五个子连珠
- 从左到右的斜着的五子连珠
- 从右到左的斜着的五子连珠
完成
checkWinner
方法
// 谁获胜就返回谁的Id, 如果还没有获胜者, 就返回0privateintcheckWinner(int row,int col,int chess){// 判断当前是谁获胜// 1. 一行五子连珠for(int i = col -4;i >=0&& i <= col && i <=Constant.COL-5; i++){if(board[row][i]== chess
&& board[row][i+1]== chess
&& board[row][i+2]== chess
&& board[row][i+3]== chess
&& board[row][i+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}// 2. 一列五子连珠for(int i = row -4; i >=0&& i <= row && i <=Constant.ROW-5; i++){if(board[i][col]== chess
&& board[i+1][col]== chess
&& board[i+2][col]== chess
&& board[i+3][col]== chess
&& board[i+4][col]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}// 3. 斜着五子连珠 -> 左上到右下for(int i = row -4, j = col -4; i <= row && j <= col;j++,i++){try{if(board[i][j]== chess
&& board[i+1][j+1]== chess
&& board[i+2][j+2]== chess
&& board[i+3][j+3]== chess
&& board[i+4][j+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}catch(ArrayIndexOutOfBoundsException e){continue;}}// 4. 斜着五子连珠 -> 右上到左下for(int i = row+4,j=col-4; i>=row && j <= col; i--,j++){try{if(board[i][j]== chess
&& board[i-1][j+1]== chess
&& board[i-2][j+2]== chess
&& board[i-3][j+3]== chess
&& board[i-4][j+4]== chess){return chess ==1? user1.getUserId(): user2.getUserId();}}catch(ArrayIndexOutOfBoundsException e){continue;}}return0;}
版权归原作者 独一无二的哈密瓜 所有, 如有侵权,请联系我们删除。