Baseado no jogo duplo de gamão SpringBoot + MyBatis

1. Funções principais

Tecnologia:
Front-end: Back- HTML + CSS + JavaScript + AJAX
end:SpringBoot + MyBatis + WebSocket + MySQL 5.7

insira a descrição da imagem aqui

2. Efeito de demonstração

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui
insira a descrição da imagem aqui

3. Crie um projeto

insira a descrição da imagem aqui
insira a descrição da imagem aqui
insira a descrição da imagem aqui
insira a descrição da imagem aqui

4. Projeto de banco de dados

insira a descrição da imagem aqui

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(255),
    score int,        -- 天梯积分
    totalCount int,   -- 比赛总场数
    winCount int      -- 获胜场数
);

insert into user values(null,"cm","$2a$10$Bs4wNEkledVlGZa6wSfX7eCSD7wRMO0eUwkJH0WyhXzKQJrnk85li",1000,0,0);

5. Arquivos de configuração

application.yml

debug: true
logging:
    level:
        com:
            example: DEBUG
            example.onlinemusic.mapper: debug
        druid:
            sql:
                Statement: DEBUG
        root: INFO
spring:
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: root
        url: jdbc:mysql://localhost:3306/java_gobang?characterEncoding=utf8&serverTimezone=UTC
        username: root

mybatis:
    mapper-locations: classpath:mybatis/**Mapper.xml
server:
    port: 8081

mybatis.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.mapper.UserMapper">

   
</mapper>

6. Módulo do usuário

6.1 Implementação de Login

6.1.1 Interface de interação front-end e back-end

Toda vez que projetamos, precisamos projetar primeiro as interfaces interativas de front-end e back-end.

请求
POST /login HTTP/1.1
{
    
    username: "",password: ""}

响应
HTTP/1.1 200 OK
Content-Type: application/json

{
    
    
    userId: 1,
    username: 'cm',
    score: 1000,
    totalCount: 0,
    winCount: 0
}    

6.1.2 camada de modelo

Crie a classe de usuário

@Data
public class User {
    
    
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;

}

6.1.3 camada do mapeador

Não se esqueça da anotação @Mapper

@Mapper
public interface UserMapper {
    
    

    //往数据里插入一个用户,用于注册功能
    int insert(User user);

    //根据用户名,来查询用户的详细信息,用于登录功能
    User selectByName(String username);

    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    int userWin(int userId);

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    int userLose(int userId);

}

6.1.4 camada xml

Crie o pacote mybatis em recursos e crie 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.mapper.UserMapper">

    <!-- 新增用户 -->
    <insert id="insert">
        insert into user values (null,#{username},#{password},1000,0,0);
    </insert>


    <!-- 根据用户名查找用户用户 -->
    <select id="selectByName" resultType="com.example.java_gobang.model.User">
        select * from user where username=#{username};
    </select>

    <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>

</mapper>

6.1.5 camada de serviço

Chame o método da camada do mapeador

@Service
public class UserService {
    
    

    @Resource
    private UserMapper userMapper;

    //往数据里插入一个用户,用于注册功能
    public int insert(User user){
    
    
        return userMapper.insert(user);
    }

    //根据用户名,来查询用户的详细信息,用于登录功能
    public User selectByName(String username){
    
    
        return userMapper.selectByName(username);
    }

    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    public int userWin(int userId){
    
    
        return userMapper.userWin(userId);
    }

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    public int userLose(int userId){
    
    
        return userMapper.userLose(userId);
    }
}

6.1.6 camada controladora

insira a descrição da imagem aqui
Usado para armazenar strings de sessão

@RestController
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;


    @RequestMapping("/login")
    @ResponseBody
    public Object login(String username, String password, HttpServletRequest request){
    
    

        // 查询用户是否在数据库中存在
        User user = userService.selectByName(username);

        // 没有查到
        if(user == null) {
    
    
            System.out.println("登录失败!");
            return new User();
        }else {
    
    

            //查到了,但密码不一样
            if(!bCryptPasswordEncoder.matches(password,user.getPassword())) {
    
    
                return new User();
            }
            // 匹配成功,创建 session
            request.getSession().setAttribute(Constant.USER_SESSION_KEY,user);
            return user;
        }
    }
}

6.1.7 Criptografia de senha com BCrypt

<!-- 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>

Adicionar anotação à classe de inicialização

@SpringBootApplication(exclude = {
    
    org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})

6.1.8 Adicionar interceptador

Criar pacote de configuração

Classe Interceptor de Login

public class LoginInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(Constant.USER_SESSION_KEY) != null){
    
    
            return true;
        }
        response.sendRedirect("/login.html");
        return false;
    }
}

Classe AppConfig

@Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login.html")
                .excludePathPatterns("/**/register.html")
                .excludePathPatterns("/**/css/**.css")
                .excludePathPatterns("/**/images/**")
                .excludePathPatterns("/**/js/**.js")
                .excludePathPatterns("/**/login")
                .excludePathPatterns("/**/register")
                .excludePathPatterns("/**/logout");
    }

6.1.9 Teste

insira a descrição da imagem aqui

6.2 Implementação do Registro

6.2.1 Interfaces de interação front-end e back-end

请求
POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=cm&password=123456

响应
HTTP/1.1 200 OK
Content-Type: application/json

{
    
    
    userId: 1,
    username: 'dingding',
    score: 1000,
    totalCount: 0,
    winCount: 0
}    

Se o registro falhar (por exemplo, o nome de usuário já existe), ele retorna um objeto cujo nome de usuário é nulo

6.2.2 camada controladora

@RequestMapping("/register")
    @ResponseBody
    public Object register(String username,String password){
    
    

        User user1 = userService.selectByName(username);
        if(user1 != null){
    
    
            System.out.println("当前用户已存在");
            return new User();
        }else{
    
    
            User user2 = new User();
            user2.setUsername(username);
            String password1 = bCryptPasswordEncoder.encode(password);
            user2.setPassword(password1);
            userService.insert(user2);
            return user2;
        }
    }

6.2.3 Teste

insira a descrição da imagem aqui

Não funcionará se você registrar o mesmo novamente.
insira a descrição da imagem aqui

6.3. Obtenção de informações do usuário

6.3.1 Interfaces de interação front-end e back-end

请求
GET /userinfo HTTP/1.1

响应
HTTP/1.1 200 OK
Content-Type: application/json

{
    
    
    userId: 1,
    username: 'cm',
    score: 1000,
    totalCount: 0,
    winCount: 0
}    

6.3.2 camada controladora

@RequestMapping("/userinfo")
    @ResponseBody
    public Object getUserInfo(HttpServletRequest request){
    
    
            try{
    
    
                HttpSession session = request.getSession(false);
                User user = (User)session.getAttribute("user");
                User newUser = userService.selectByName(user.getUsername());
                return newUser;
            }catch (NullPointerException e){
    
    
                System.out.println("没有该用户");
                return new User();
            }

    }

6.3.3 Teste

insira a descrição da imagem aqui

Porque salvamos este JSESSIONID antes de fazer login com esta conta
insira a descrição da imagem aqui

6.4 Sair

6.4.1 Interfaces de interação front-end e back-end

请求
GET /logout HTTP/1.1

响应
HTTP/1.1 200

6.4.2 camada controladora

@RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
    
    HttpSession session = request.getSession(false);
    // 拦截器的拦截, 所以不可能出现session为空的情况
    session.removeAttribute(Constant.USER_SESSION_KEY);
    response.sendRedirect("login.html");

}

6.4.3 Teste

redirecionei minha página de login
insira a descrição da imagem aqui

7. Módulos correspondentes

O cliente inicia ativamente uma solicitação ao servidor e retorna uma resposta. Se o cliente não iniciar uma solicitação, o servidor não poderá contatá-lo ativamente. Aqui, precisamos que o servidor envie ativamente uma mensagem ao cliente e precisamos use "message push"
insira a descrição da imagem aqui
aqui. É necessário concordar com as interfaces de interação front-end e back-end, que também são baseadas websocketem . Ele pode transmitir dados de texto e dados binários. Aqui, ele é projetado para transmitir dados de texto em json formato.

7.1 Interfaces de interação front-end e back-end

conectar (URL)

ws://127.0.0.1:8080/findMatch

pedido de correspondência

{
    
    
    message: 'startMatch' / 'stopMatch', // 开始/结束匹配
}

A correspondência aqui é que após o login, as informações do usuário foram obtidas e salvas na HttpSession

Resposta correspondente 1 (esta é a resposta correspondente retornada pelo servidor imediatamente após o envio da solicitação)

{
    
    
    ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 false
    reason: '',                // 错误原因
    message: 'startMatch' / 'stopMatch'
}

Resposta correspondente 2 (combinada com o oponente, o servidor ativamente empurra de volta a mensagem, o oponente correspondente não precisa ser refletido na resposta e ainda é colocado no lado do servidor)

{
    
    
    ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 false
    reason: '',                // 错误原因
    message: 'matchSuccess',    
}

7.2 Desenvolvimento front-end de funções correspondentes

insira a descrição da imagem aqui

// 此处进行初始化 websocket, 并且实现前端的匹配逻辑.
    // 此处的路径必须写作 /findMatch, 千万不要写作 /findMatch/
    let websocketUrl = 'ws://' + location.host + '/findMatch';
    let websocket = new WebSocket(websocketUrl);
    websocket.onopen = function() {
    
    
        console.log("onopen");
    }
    websocket.onclose = function() {
    
    
        console.log("onclose");
    }
    websocket.onerror = function() {
    
    
        console.log("onerror");
    }
    // 监听页面关闭事件. 在页面关闭之前, 手动调用这里的 websocket 的 close 方法.
    window.onbeforeunload = function() {
    
    
        websocket.close();
    }

    // 一会重点来实现, 要处理服务器返回的响应
    websocket.onmessage = function(e) {
    
    
        // 处理服务器返回的响应数据. 这个响应就是针对 "开始匹配" / "结束匹配" 来对应的
        // 解析得到的响应对象. 返回的数据是一个 JSON 字符串, 解析成 js 对象
        let resp = JSON.parse(e.data);
        let matchButton = document.querySelector('#match-button');
        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");
            location.replace("/game_room.html");
        } else if (resp.message == 'repeatConnection') {
    
    
            alert("当前检测到多开! 请使用其他账号登录!");
            location.replace("/login.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.replace('/login.html');
        }
    }

7.3 Desenvolvimento de back-end de funções correspondentes

Crie a classe MatchController, a classe de entrada para processar solicitações de websocket

// 通过这个类来处理匹配功能中的 websocket 请求
@Component
public class MatchController extends TextWebSocketHandler {
    
    
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

    @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 {
    
    
    
    }
}

Adicione o endereço de resposta que aciona o URL no AppConfig

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer {
    
    
    @Autowired
    private MatchAPI matchAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    
    
        registry.addHandler(testAPI, "/test");
        // 通过 .addInterceptors(new HttpSessionHandshakeInterceptor() 这个操作来把 HttpSession 里的属性放到 WebSocket 的 session 中
        // 参考: https://docs.spring.io/spring-framework/docs/5.0.7.RELEASE/spring-framework-reference/web.html#websocket-server-handshake
        // 然后就可以在 WebSocket 代码中 WebSocketSession 里拿到 HttpSession 中的 attribute.
        registry.addHandler(matchAPI, "/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

Desta forma, os dados (principalmente o objeto User) armazenados na HttpSession durante o processo de login anterior podem ser colocados na sessão do WebSocket.É conveniente obter as informações do usuário atual no código a seguir.

7.3.1 Implementando o Gerenciador de Usuários

Crie a classe OnlineUserManager para gerenciar o status online do usuário atual. É essencialmente uma estrutura de tabela de hash. A chave é o ID do usuário e o valor é a WebSocketSession do usuário.

Como este é um estado multithread, muitos usuários terão problemas de segurança de thread ao acessar a mesma tabela de hash, então ConcurrentHashMap é usado aqui para garantir problemas de segurança de thread.

Com a ajuda desta classe, por um lado, você pode determinar se o usuário está online e, ao mesmo tempo, pode obter facilmente a Sessão para chamar de volta ao cliente.

Ao entrar na sala, armazene o usuário na tabela de hash,
ao sair, exclua o usuário da tabela de hash e
consulte a sessão correspondente através do userId, para retornar os dados ao cliente.

Há também uma interface de batalha na parte de trás, tudo criado primeiro

@Component
public class OnlineUserManager {
    
    
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

    public void enterGameHall(int userId, WebSocketSession session) {
    
    
        gameHall.put(userId, session);
    }

    // 只有当前页面退出的时候, 能销毁自己的 session
    // 避免当一个 userId 打开两次 游戏页面, 错误的删掉之前的会话的问题.
    public void exitGameHall(int userId) {
    
    
        gameHall.remove(userId);
    }

    public WebSocketSession getSessionFromGameHall(int userId) {
    
    
        return gameHall.get(userId);
    }

    public void enterGameRoom(int userId, WebSocketSession session) {
    
    
        gameRoom.put(userId, session);
    }

    public void exitGameRoom(int userId) {
    
    
        gameRoom.remove(userId);
    }

    public WebSocketSession getSessionFromGameRoom(int userId) {
    
    
        return gameRoom.get(userId);
    }
}

Os jogadores na partida são combinados com pontuações e os jogadores inteiros são divididos em três categorias

Nomal: pontuação < 2000
Alta: pontuação >= 2000 && pontuação < 3000
Muito Alta: pontuação >= 3000

De acordo com esses três níveis, diferentes filas são alocadas. Um thread especial é necessário para varrer continuamente a fila correspondente. Se os jogadores correspondentes formarem um par, eles serão retirados e colocados em uma sala.

Forneça o método add para a classe MatchAPI chamar para adicionar o jogador à fila correspondente.
Forneça o método remove para a classe MatchAPI chamar para remover o jogador da fila correspondente.
Ao mesmo tempo, o Matcher descobre que o OnlineUserManager é gravado para obter a Sessão do jogador.

Como handlerMatch é chamado em um encadeamento separado. Portanto, a segurança do encadeamento de acessar a fila deve ser considerada. Bloqueios precisam ser adicionados.
Cada fila pode usar o próprio objeto de fila como um bloqueio.
Use wait na entrada para aguardar até a fila atinge 2 elementos ou mais, a fila de consumo de thread é despertada.

@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;

    @Autowired
    private RoomManager roomManager;

    private ObjectMapper objectMapper = new ObjectMapper();


    // 操作匹配队列的方法
    // 把玩家放到匹配队列中
    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!");
        }

    }

    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() {
    
    
                while (true) {
    
    
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    handlerMatch(veryHighQueue);
                }
            }
        };
        t3.start();
    }

    private void handlerMatch(Queue<User> matchQueue) {
    
    
        synchronized (matchQueue){
    
    
            try{
    
    
                // 1. 检测队列中元素个数是否达到 2
                // 队列的初始情况可能是 空
                // 如果往队列中添加一个元素,这个时候,仍然是不能进行后续匹配操作的
                // 因此在这里使用 while 循环检查是更合理的
                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.getSessionFromGameHall(player1.getUserId());
                WebSocketSession session2 = onlineUserManager.getSessionFromGameHall(player2.getUserId());
                //理论上来说,匹配队列中的玩家一定是在线的状态
                // 因为前面的逻辑里进行了处理,当玩家断开连接的时候把玩家从匹配队列中移除
                // 但是此处仍然进行一次判定
                if(session1 == null){
    
    
                    // 如果玩家1 现在不在线,就把玩家2 重新放回到匹配队列中
                    matchQueue.offer(player2);
                    return;
                }
                if(session2 == null){
    
    
                    // 如果玩家2 现在下线,就把玩家1 重新放回到匹配队列
                    matchQueue.offer(player1);
                    return;
                }
                // 当前能否排到两个玩家是同一个用户的情况嘛? 一个玩家入队列了两次??
                // 理论上也不会存在~~
                // 1) 如果玩家下线, 就会对玩家移出匹配队列
                // 2) 又禁止了玩家多开.
                // 但是仍然在这里多进行一次判定, 以免前面的逻辑出现 bug 时带来严重的后果.
                if (session1 == session2) {
    
    
                    // 把其中的一个玩家放回匹配队列.
                    matchQueue.offer(player1);
                    return;
                }


                // 4. 把这两个玩家放到一个游戏房间中
                Room room = new Room();
                roomManager.add(room, player1.getUserId(), player2.getUserId());

                // 5. 给玩家反馈信息: 你匹配到对手了
                // 通过 websocket 返回一个 message 为 'matchSuccess' 这样的响应
                // 此处要给两个玩家都返回 "匹配成功" 这样的信息
                // 因此就要返回两次
                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 (IOException | InterruptedException e){
    
    
                e.printStackTrace();
            }
        }
    }

}

7.3.2 Implementando a classe Room

Depois que a partida for bem-sucedida, os dois jogadores precisam ser colocados no mesmo objeto de sala.

  1. Uma sala deve conter um ID de sala, usando UUID como identificador exclusivo da sala.
  2. As informações de ambos os jogadores no jogo devem ser registradas na sala.
  3. Registre o ID do primeiro motor
  4. Grave uma matriz bidimensional como o tabuleiro de xadrez para o jogo.
  5. Grave um OnlineUserManager para interação posterior com o cliente.
  6. Claro, ObjectMapper é indispensável para processar json
public class Room {
    
    
    private String roomId;
    // 玩家1
    private User user1;
    // 玩家2
    private User user2;
    // 先手方的用户 id
    private int whiteUserId = 0;
    // 棋盘, 数字 0 表示未落子位置. 数字 1 表示玩家 1 的落子. 数字 2 表示玩家 2 的落子
    private static final int MAX_ROW = 15;
    private static final int MAX_COL = 15;
    private int[][] chessBoard = new int[MAX_ROW][MAX_COL];

    private ObjectMapper objectMapper = new ObjectMapper();

    private OnlineUserManager onlineUserManager;

    public Room() {
    
    
        // 使用 uuid 作为唯一身份标识
        roomId = UUID.randomUUID().toString();
    }

    // getter / setter 方法略
}

7.3.3 Implementando o Gerenciador de Sala

Haverá muitos objetos de Sala. Cada dois jogadores no jogo corresponde a um objeto de Sala. Um objeto de gerente é necessário para gerenciar todas as salas.

  1. Use uma tabela de hash para salvar todos os objetos da sala, a chave é roomId, o valor é o objeto da sala
  2. Em seguida, use uma tabela de hash para salvar o mapeamento de userId -> roomId, o que é conveniente para encontrar a sala de acordo com o jogador.
  3. Fornece API para adicionar, excluir e verificar. (A verificação inclui duas versões, consulta com base no ID do quarto e consulta com base no ID do usuário).
// 房间管理器
// 也要唯一实例
    @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 映射关系不存在, 直接返回 null
            return null;
        }
        return rooms.get(roomId);
    }

}

7.3.4 camada controladora

websocket tem 4 métodos:

一: 实现 afterConnectionEstablished 方法.

  1. Por meio do objeto de sessão no parâmetro, obtenha as informações do usuário definidas antes de efetuar login.
  2. Use onlineUserManager para gerenciar o status online dos usuários.
  3. Primeiro determine se o usuário já está online, se estiver online, retorne um erro diretamente (proibir que a mesma conta abra mais de uma).
  4. Defina o status online do jogador.
@Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    
    
        // 玩家上线, 加入到 OnlineUserManager 中

        // 1. 先获取到当前用户的身份信息(谁在游戏大厅中, 建立的连接)
        try {
    
    
            User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);

            // 2. 先判定当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑.
            if (onlineUserManager.getSessionFromGameHall(user.getUserId()) != null
                    || onlineUserManager.getSessionFromGameRoom(user.getUserId()) != null) {
    
    
                // 当前用户已经登录了!!
                // 针对这个情况要告知客户端, 你这里重复登录了.
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setReason("当前禁止多开!");
                response.setMessage("repeatConnection");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                // 此处直接关闭有些太激进了, 还是返回一个特殊的 message , 供客户端来进行判定, 由客户端负责进行处理
                // session.close();
                return;
            }

            // 3. 拿到了身份信息之后, 就可以把玩家设置成在线状态了
            onlineUserManager.enterGameHall(user.getUserId(), session);
            System.out.println("玩家 " + user.getUsername() + " 进入游戏大厅!");
        } catch (NullPointerException e) {
    
    
            System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
            // e.printStackTrace();
            // 出现空指针异常, 说明当前用户的身份信息是空, 用户未登录呢.
            // 把当前用户尚未登录这个信息给返回回去~~
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录! 不能进行后续匹配功能!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }

二: 实现 handleTextMessage 方法

  1. Primeiro obtenha as informações do jogador atual da sessão.
  2. Analisar a solicitação enviada pelo cliente
  3. Determine o tipo de solicitação. Se for startMatch, adicione o objeto de usuário à fila correspondente. Se for stopMatch, exclua o objeto de usuário da fila correspondente.
  4. Um objeto de correspondência precisa ser implementado aqui para lidar com a lógica real de correspondência.
@Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    
    
        // 实现处理开始匹配请求和处理停止匹配请求.
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
    
    
            // 进入匹配队列
            matcher.add(user);
            // 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
    
    
            // 退出匹配队列
            matcher.remove(user);
            // 移除之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
    
    
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }

三: 实现 afterConnectionClosed 方法

  1. O trabalho principal é sair do player do onlineUserManager.
  2. Ao sair, preste atenção para determinar se o player atual tem mais de uma conexão aberta (um userId, correspondente a duas conexões websocket). Se um player abrir uma segunda conexão websocket, a segunda conexão websocket não afetará o player. Sair do OnlineUserManager .
  3. Se o jogador estiver atualmente na fila de matchmaking, ele será removido diretamente da fila de matchmaking.
@Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    
    
        try {
    
    
            // 玩家下线, 从 OnlineUserManager 中删除
            User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
            WebSocketSession tmpSession = onlineUserManager.getSessionFromGameHall(user.getUserId());
            if (tmpSession == session) {
    
    
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中, 而 websocket 连接断开了, 就应该移除匹配队列
            matcher.remove(user);
        } catch (NullPointerException e) {
    
    
            System.out.println("[MatchAPI.afterConnectionClosed] 当前用户未登录!");
            // e.printStackTrace();
        }
    }

四: 实现 handleTransportError 方法

A mesma lógica de afterConnectionClosed, no caso de desligamento anormal, obtenha as informações do usuário, defina o status online como offline e exclua o usuário na fila correspondente

 @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    
    
        try {
    
    
            // 玩家下线, 从 OnlineUserManager 中删除
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getSessionFromGameHall(user.getUserId());
            if (tmpSession == session) {
    
    
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中, 而 websocket 连接断开了, 就应该移除匹配队列
            matcher.remove(user);
        } catch (NullPointerException e) {
    
    
            System.out.println("[MatchAPI.handleTransportError] 当前用户未登录!");
        }
    }

8. Módulo de Batalha

8.1 Interfaces de interação front-end e back-end

conectar

ws://127.0.0.1:8080/game

Resposta de conexão (quando ambos os jogadores estão conectados, retorne dados para ambas as partes para indicar prontidão)

{
    
    
    message: 'gameReady',    // 游戏就绪
    ok: true,                // 是否成功. 
    reason: '',                // 错误原因
    roomId: 'abcdef',        // 房间号. 用来辅助调试. 
    thisUserId: 1,            // 玩家自己的 id
    thatUserId: 2,            // 对手的 id
    whiteUser: 1,            // 先手方的 id
}

soltar pedido:

{
    
    
    message: 'putChess',
    userId: 1,
    row: 0,
    col: 0
}

Resposta de queda:

{
    
    
    message: 'putChess',
    userId: 1,    
    row: 0,
    col: 0, 
    winner: 0
}

8.2 Desenvolvimento do Cliente

A tela aqui é usada para desenhar o tabuleiro de xadrez,
insira a descrição da imagem aqui

js arquivo:

  1. Esta parte do código é baseada na API de tela. Não precisamos entender essa parte. Basta copiar e colar o código a seguir diretamente.
  2. Use uma matriz bidimensional para representar o tabuleiro de xadrez. Embora o resultado seja determinado pelo servidor, o tabuleiro de xadrez do cliente pode evitar a situação de "movimentos repetidos em uma posição"
  3. O efeito da função oneStep é desenhar uma peça de xadrez em uma posição especificada. Ela pode distinguir se desenha um personagem branco ou uma peça preta. Os parâmetros são a abcissa e a ordenada, correspondentes à coluna e linha respectivamente.
  4. Use onclick para manipular o evento click do usuário. Quando o usuário clica, esta função é usada para controlar o desenho do peão.
  5. A variável me é usada para indicar se é minha vez de me mover, a variável over é usada para indicar o fim do jogo.
  6. Este código usará uma imagem de fundo (sky.jpg), que pode ser colocada no diretório de imagens.
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
//
// TODO

//
// 初始化一局游戏
//
function initGame() {
    
    
    // 是我下还是对方下. 根据服务器分配的先后手情况决定
    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 = new Image();
    logo.src = "image/sky.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;
        }
    }

    // TODO 实现发送落子请求逻辑, 和处理落子响应逻辑. 
}

initGame();

8.2.1 Inicializar websocket

No código do front-end:

  1. Primeiro exclua a chamada da função initGame original. Após obter a resposta pronta do servidor, inicialize a placa.
  2. Crie um objeto websocket, e registre as funções onopen/onclose/onerror. Em onerror, faça uma lógica para pular para o lobby do jogo. Quando a rede for desconectada anormalmente, ela retornará ao lobby.
  3. Implemente o método onmessage. O onmessage processa primeiro a resposta pronta para o jogo.
// 注意, 路径要写作 /game 不要写作 /game/
websocket = new WebSocket("ws://127.0.0.1:8080/game");
//连接成功建立的回调方法
websocket.onopen = function (event) {
    
    
    console.log("open");
}
//连接关闭的回调方法
websocket.onclose = function () {
    
    
    console.log("close");
}
//连接发生错误的回调方法
websocket.onerror = function () {
    
    
    console.log("error");
    alert('和服务器连接断开! 返回游戏大厅!')
    location.assign('/game_hall.html')
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
    
    
    websocket.close();
}

websocket.onmessage = function (event) {
    
    
    console.log('handlerGameReady: ' + event.data);

    let response = JSON.parse(event.data);
    if (response.message != 'gameReady') {
    
    
        console.log('响应类型错误!');
        return;
    }
    if (!response.ok) {
    
    
        alert('连接游戏失败! reason: ' + response.reason);
        location.assign('/game_hall.html')
        return;
    }
    // 初始化游戏信息
    gameInfo.roomId = response.roomId;
    gameInfo.thisUserId = response.thisUserId;
    gameInfo.thatUserId = response.thatUserId;
    gameInfo.isWhite = (response.whiteUserId == gameInfo.thisUserId);
    console.log('[gameReady] ' + JSON.stringify(gameInfo));
    // 初始化棋盘
    initGame();
    // 设置 #screen 的显示
    setScreenText(gameInfo.isWhite);
}

8.2.2 Enviar uma solicitação de movimentação

Modifique a função onclick para adicionar a lógica de envio da solicitação quando o filho for colocado.

  1. Comente a operação do onStep original e modifique o tabuleiro de xadrez, e coloque-o em processamento ao receber a resposta do movimento.
  2. Implemente send , envie solicitações de descarte por meio do websocket.
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) {
    
    
        // 发送坐标给服务器, 服务器要返回结果
        send(row, col);

        // oneStep(col, row, gameInfo.isWhite);
        // chessBoard[row][col] = 1;
        // me = !me; 
    }
}

function send(row, col) {
    
    
    console.log("send");
    let request = {
    
    
        message: "putChess",
        userId: gameInfo.thisUserId,
        row: row,
        col: col,
    }
    websocket.send(JSON.stringify(request));
}

8.2.3 Manipulação de respostas de movimento

No initGame, modifique a mensagem do websocket

  1. Antes do initGame, a resposta do jogo pronto é processada e, após o recebimento da resposta do jogo, ela é alterada para receber a resposta do movimento.
  2. Ao lidar com a resposta do movimento, é necessário lidar com a mão vencedora.
websocket.onmessage = function (event) {
    
    
    console.log('handlerPutChess: ' + event.data);

    let response = JSON.parse(event.data);
    if (response.message != 'putChess') {
    
    
        console.log('响应类型错误!');
        return;
    }

    // 1. 判断 userId 是自己的响应还是对方的响应, 
    //    以此决定当前这个子该画啥颜色的
    if (response.userId == gameInfo.thisUserId) {
    
    
        oneStep(response.col, response.row, gameInfo.isWhite);
    } else if (response.userId == gameInfo.thatUserId) {
    
    
        oneStep(response.col, response.row, !gameInfo.isWhite);
    } else {
    
    
        console.log('[putChess] response userId 错误! response=' + JSON.stringify(response));
        return;
    }
    chessBoard[response.row][response.col] = 1;
    me = !me; // 接下来该下个人落子了. 

    // 2. 判断游戏是否结束
    if (response.winner != 0) {
    
    
        // 胜负已分
        if (response.winner == gameInfo.thisUserId) {
    
    
            alert("你赢了!");
        } else {
    
    
            alert("你输了");
        }
        // 如果游戏结束, 则关闭房间, 回到游戏大厅. 
        location.assign('/game_hall.html')
    }

    // 3. 更新界面显示
    setScreenText(me);
}

8.3 Desenvolvimento de Servidor

Crie uma classe GameController para lidar com solicitações de websocket

@Component
public class GameController extends TextWebSocketHandler {
    
    
    private ObjectMapper objectMapper = new ObjectMapper();
    @Autowired
    private RoomManager roomManager;
    // 这个是管理 game 页面的会话
    @Autowired
    private OnlineUserManager onlineUserManager;

    @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 {
    
    
    }
}

8.3.1 Criar um objeto de subsolicitação/resposta

Classe GameReadyResponse

public class GameReadyResponse {
    
    
    private String message = "gameReady";
    private boolean ok = true;
    private String reason = "";
    private String roomId = "";
    private int thisUserId = 0;
    private int thatUserId = 0;
    private int whiteUserId = 0;
}

Classe GameRequest

public class GameRequest {
    
    
    // 如果不给 message 设置 getter / setter, 则不会被 jackson 序列化
    private String message = "putChess";
    private int userId;
    private int row;
    private int col;
}

Classe GameResponse

public class GameResponse {
    
    
    // 如果不给 message 设置 getter / setter, 则不会被 jackson 序列化
    private String message = "putChess";
    private int userId;
    private int row;
    private int col;
    private int winner; // 胜利玩家的 userId
}

8.3.2 Lidando com o Sucesso da Conexão

  1. Primeiro, você precisa detectar o status de login do usuário. Obtenha as informações do usuário atual da Sessão.
  2. Então precisamos determinar se o jogador atual está na sala.
  3. Em seguida, é feita a determinação de abertura múltipla. Se o jogador já estiver no jogo, ele não poderá se conectar novamente.
  4. Coloque os dois jogadores nos objetos da sala correspondente. Quando ambos os jogadores estiverem conectados, a sala estará cheia. Neste momento, os dois jogadores são notificados de que ambos estão prontos.
  5. Se um terceiro jogador tentar entrar na sala também, é dado um aviso de que a sala está cheia.
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    
    
    GameReadyResponse resp = new GameReadyResponse();

    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        resp.setOk(false);
        resp.setReason("用户尚未登录!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
        return;
    }
    Room room = roomManager.getRoomByUserId(user.getUserId());
    if (room == null) {
    
    
        resp.setOk(false);
        resp.setReason("用户并未匹配成功! 不能开始游戏!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
        return;
    }
    System.out.println("连接游戏! roomId=" + room.getRoomId() + ", userId=" + user.getUserId());

    // 先判定用户是不是已经在游戏中了.
    if (onlineUserManager.getSessionFromGameHall(user.getUserId()) != null
        || onlineUserManager.getSessionFromGameRoom(user.getUserId()) != null) {
    
    
        resp.setOk(false);
        resp.setReason("禁止多开游戏页面!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
        return;
    }
    // 更新会话
    onlineUserManager.enterGameRoom(user.getUserId(), session);

    // 同一个房间的两个玩家, 同时连接时要考虑线程安全问题.
    synchronized (room) {
    
    
        if (room.getUser1() == null) {
    
    
            room.setUser1(user);
            // 设置 userId1 为先手方
            room.setWhiteUserId(user.getUserId());
            System.out.println("userId=" + user.getUserId() + " 玩家1准备就绪!");
            return;
        }
        if (room.getUser2() == null) {
    
    
            room.setUser2(user);
            System.out.println("userId=" + user.getUserId() + " 玩家2准备就绪!");

            // 通知玩家1 就绪
            noticeGameReady(room, room.getUser1().getUserId(), room.getUser2().getUserId());
            // 通知玩家2 就绪
            noticeGameReady(room, room.getUser2().getUserId(), room.getUser1().getUserId());
            return;
        }
    }
    // 房间已经满了!
    resp.setOk(false);
    String log = "roomId=" + room.getRoomId() + " 已经满了! 连接游戏失败!";
    resp.setReason(log);
    session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    System.out.println(log);
}

8.3.3 Processamento offline do jogador

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    
    
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId());
    if (existSession != session) {
    
    
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("连接出错! userId=" + user.getUserId());
    onlineUserManager.exitGameRoom(user.getUserId());
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    
    
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId());
    if (existSession != session) {
    
    
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("用户退出! userId=" + user.getUserId());
    onlineUserManager.exitGameRoom(user.getUserId());
}

8.3.4 Adicionar código quadriculado à classe de quarto

Aqui o Room quer injetar objetos Spring, você não pode usar a anotação @Autowired @Resource. Você precisa usar o contexto

Modificar a classe de inicialização

public class JavaGobangApplication {
    
    
    public static ConfigurableApplicationContext context;

    public static void main(String[] args) {
    
    
        context = SpringApplication.run(JavaGobangApplication.class, args);
    }

}

Sala de jogos:

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);
      userService = JavaGobangApplication.context.getBean(UserService.class);
    }

8.3.5 Tratamento de solicitações de envio

Implementar handleTextMessage

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    
    
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        return;
    }
    Room room = roomManager.getRoomByUserId(user.getUserId());
    room.putChess(message.getPayload());
}

8.3.6 Implementar o método putChess

  1. Primeiro, analise a solicitação em um objeto de solicitação.
    2. De acordo com as informações no objeto de solicitação, mova o jogo no tabuleiro de xadrez.
    3. Após a conclusão do movimento, para facilitar a depuração, você pode imprimir o estado atual do
    4. Verifique se o jogo acabou. 5.
    Construa Responda ao lance e escreva-o de volta para cada jogador.
    6. Ao escrever de volta, se um jogador for desconectado, o outro lado é considerado o 7.
    Se o jogo foi pontuado, modifique a pontuação do jogador e destrua a sala.
// 玩家落子
public void putChess(String message) throws IOException {
    
    
    GameRequest req = objectMapper.readValue(message, GameRequest.class);
    GameResponse response = new GameResponse();
    // 1. 进行落子
    int chess = req.getUserId() == user1.getUserId() ? 1 : 2;
    int row = req.getRow();
    int col = req.getCol();
    if (chessBoard[row][col] != 0) {
    
    
        System.out.println("落子位置有误! " + req);
        return;
    }
    chessBoard[row][col] = chess;
    printChessBoard();
    // 2. 检查游戏结束
    //    返回的 winner 为玩家的 userId
    int winner = checkWinner(chess, row, col);
    // 3. 把响应写回给玩家
    response.setUserId(req.getUserId());
    response.setRow(row);
    response.setCol(col);
    response.setWinner(winner);
    WebSocketSession session1 = onlineUserManager.getSessionFromGameRoom(user1.getUserId());
    WebSocketSession session2 = onlineUserManager.getSessionFromGameRoom(user2.getUserId());
    if (session1 == null) {
    
    
        // 玩家1 掉线, 直接认为玩家2 获胜
        response.setWinner(user2.getUserId());
        System.out.println("玩家1 掉线!");
    }
    if (session2 == null) {
    
    
        // 玩家2 掉线, 直接认为玩家1 获胜
        response.setWinner(user1.getUserId());
        System.out.println("玩家2 掉线!");
    }
    String responseJson = objectMapper.writeValueAsString(response);
    if (session1 != null) {
    
    
        session1.sendMessage(new TextMessage(responseJson));
    }
    if (session2 != null) {
    
    
        session2.sendMessage(new TextMessage(responseJson));
    }
    // 4. 如果玩家胜负已分, 就把 room 从管理器中销毁
    if (response.getWinner() != 0) {
    
    
        userMapper.userWin(response.getWinner() == user1.getUserId() ? user1 : user2);
        userMapper.userLose(response.getWinner() == user1.getUserId() ? user2 : user1);
        roomManager.removeRoom(roomId, user1.getUserId(), user2.getUserId());
        System.out.println("游戏结束, 房间已经销毁! roomId: " + roomId + " 获胜方为: " + response.getWinner());
    }
}

8.3.7 Implementando a lógica de impressão do tabuleiro de xadrez

private void printChessBoard() {
    
    
    System.out.println("打印棋盘信息: ");
    System.out.println("===========================");
    for (int r = 0; r < MAX_ROW; r++) {
    
    
        for (int c = 0; c < MAX_COL; c++) {
    
    
            System.out.print(chessBoard[r][c] + " ");
        }
        System.out.println();
    }
    System.out.println("===========================");
}

8.3.8 Realizar vitória ou derrota

  1. Se o jogo estiver empatado, o id do jogador será retornado. Se o jogo não estiver empatado, será retornado 0.
  2. Um valor de 1 no tabuleiro significa o movimento do jogador 1, e um valor de 2 significa o movimento do jogador 2.
  3. Ao verificar o resultado, tome a posição de movimento atual como centro e verifique todas as linhas, colunas e diagonais relevantes. Não é necessário percorrer todo o tabuleiro.
// 判定棋盘形式, 找出胜利的玩家.
// 如果游戏分出胜负, 则返回玩家的 id.
// 如果未分出胜负, 则返回 0
// chess 值为 1 表示玩家1 的落子. 为 2 表示玩家2 的落子
private int checkWinner(int chess, int row, int col) {
    
    
    // 以 row, col 为中心
    boolean done = false;
    // 1. 检查所有的行(循环五次)
    for (int c = col - 4; c <= col; c++) {
    
    
        if (c < 0 || c >= MAX_COL) {
    
    
            continue;
        }
        if (chessBoard[row][c] == chess
            && chessBoard[row][c + 1] == chess
            && chessBoard[row][c + 2] == chess
            && chessBoard[row][c + 3] == chess
            && chessBoard[row][c + 4] == chess) {
    
    
            done = true;
        }
    }
    // 2. 检查所有的列(循环五次)
    for (int r = row - 4; r <= row; r++) {
    
    
        if (r < 0 || r >= MAX_ROW) {
    
    
            continue;
        }
        if (chessBoard[r][col] == chess
            && chessBoard[r + 1][col] == chess
            && chessBoard[r + 2][col] == chess
            && chessBoard[r + 3][col] == chess
            && chessBoard[r + 4][col] == chess) {
    
    
            done = true;
        }
    }
    // 3. 检查左对角线
    for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {
    
    
        if (r < 0 || r >= MAX_ROW || c < 0 || c >= MAX_COL) {
    
    
            continue;
        }
        if (chessBoard[r][c] == chess
            && chessBoard[r + 1][c + 1] == chess
            && chessBoard[r + 2][c + 2] == chess
            && chessBoard[r + 3][c + 3] == chess
            && chessBoard[r + 4][c + 4] == chess) {
    
    
            done = true;
        }
    }
    // 4. 检查右对角线
    for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {
    
    
        if (r < 0 || r >= MAX_ROW || c < 0 || c >= MAX_COL) {
    
    
            continue;
        }
        if (chessBoard[r][c] == chess
            && chessBoard[r + 1][c - 1] == chess
            && chessBoard[r + 2][c - 2] == chess
            && chessBoard[r + 3][c - 3] == chess
            && chessBoard[r + 4][c - 4] == chess) {
    
    
            done = true;
        }
    }
    if (!done) {
    
    
        return 0;
    }
    return chess == 1 ? user1.getUserId() : user2.getUserId();
}

8.3.9 Processo de saída do jogador

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    
    
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId());
    if (existSession != session) {
    
    
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("连接出错! userId=" + user.getUserId());
    onlineUserManager.exitGameRoom(user.getUserId());
    
    // [代码加在这里]
    noticeThatUserWin(user);
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    
    
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
    
    
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameRoom(user.getUserId());
    if (existSession != session) {
    
    
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("用户退出! userId=" + user.getUserId());
    onlineUserManager.exitGameRoom(user.getUserId());
    
    // [代码加在这里]
    noticeThatUserWin(user);
}
// 通知另外一个玩家直接获胜!
private void noticeThatUserWin(User user) throws IOException {
    
    
    Room room = roomManager.getRoomByUserId(user.getUserId());
    if (room == null) {
    
    
        System.out.println("房间已经释放, 无需通知!");
        return;
    }
    User thatUser = (user == room.getUser1() ? room.getUser2() : room.getUser1());
    WebSocketSession session = onlineUserManager.getSessionFromGameRoom(thatUser.getUserId());
    if (session == null) {
    
    
        System.out.println(thatUser.getUserId() + " 该玩家已经下线, 无需通知!");
        return;
    }
    GameResponse resp = new GameResponse();
    resp.setUserId(thatUser.getUserId());
    resp.setWinner(thatUser.getUserId());
    session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
}

Acho que você gosta

Origin blog.csdn.net/chenbaifan/article/details/126527270
Recomendado
Clasificación