Basado en el doble juego de backgammon SpringBoot+MyBatis

1. Funciones básicas

Tecnología:
Front-end: Back- HTML + CSS + JavaScript + AJAX
end:SpringBoot + MyBatis + WebSocket + MySQL 5.7

inserte la descripción de la imagen aquí

2. Efecto de demostración

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

3. Crea un proyecto

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

4. Diseño de base de datos

inserte la descripción de la imagen aquí

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. Archivos de configuración

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

6.1 Implementación de inicio de sesión

6.1.1 Interfaz de interacción de front-end y back-end

Cada vez que diseñamos, primero debemos diseñar las interfaces interactivas de front-end y 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 capa modelo

Crear la clase de usuario

@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 capa del mapeador

No olvides la anotación @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 capa xml

Cree el paquete mybatis en recursos y cree 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 capa de servicio

Llame al método de la capa del 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 capa de controlador

inserte la descripción de la imagen aquí
Se utiliza para almacenar cadenas de sesión

@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 Cifrado de contraseña con 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>

Agregar anotación a la clase de inicio

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

6.1.8 Añadir interceptor

Crear paquete de configuración

Clase LoginInterceptor

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

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

inserte la descripción de la imagen aquí

6.2 Implementación del Registro

6.2.1 Interfaces de interacción front-end y 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
}    

Si el registro falla (por ejemplo, el nombre de usuario ya existe), devuelve un objeto cuyo nombre de usuario es nulo

6.2.2 capa de controlador

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

inserte la descripción de la imagen aquí

No funcionará si vuelves a registrar lo mismo.
inserte la descripción de la imagen aquí

6.3 Obtención de información del usuario

6.3.1 Interfaces de interacción front-end y 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 capa de controlador

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

inserte la descripción de la imagen aquí

Porque hemos guardado este JSESSIONID antes de iniciar sesión con esta cuenta
inserte la descripción de la imagen aquí

6.4 Cerrar sesión

6.4.1 Interfaces de interacción de front-end y back-end

请求
GET /logout HTTP/1.1

响应
HTTP/1.1 200

6.4.2 capa de controlador

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

redirigió mi página de inicio de sesión
inserte la descripción de la imagen aquí

7. Módulos a juego

El cliente inicia activamente una solicitud al servidor y devuelve una respuesta. Si el cliente no inicia una solicitud, el servidor no puede contactar activamente al cliente. Aquí, necesitamos que el servidor envíe activamente un mensaje al cliente, y necesitamos use "mensaje push"
inserte la descripción de la imagen aquí
aquí. Es necesario ponerse de acuerdo sobre las interfaces de interacción front-end y back-end, que también se basan websocketen . Puede transmitir datos de texto y datos binarios. Aquí, está diseñado para transmitir datos de texto en json formato.

7.1 Interfaces de interacción front-end y back-end

conectar (URL)

ws://127.0.0.1:8080/findMatch

solicitud de coincidencia

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

La coincidencia aquí es que después de iniciar sesión, la información del usuario se ha obtenido y guardado en HttpSession

Respuesta coincidente 1 (esta es la respuesta coincidente devuelta por el servidor inmediatamente después de enviar la solicitud)

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

Respuesta de coincidencia 2 (coincidencia con el oponente, el servidor rechaza activamente el mensaje, el oponente coincidente no necesita reflejarse en la respuesta y todavía se coloca en el lado del servidor)

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

7.2 Desarrollo front-end de funciones de coincidencia

inserte la descripción de la imagen aquí

// 此处进行初始化 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 Desarrollo back-end de funciones de coincidencia

Cree la clase MatchController, la clase de entrada para procesar solicitudes 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 {
    
    
    
    }
}

Agregue la dirección de respuesta que activa la URL en 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());
    }
}

De esta forma, los datos (principalmente el objeto Usuario) almacenados en el HttpSession durante el proceso de inicio de sesión anterior se pueden poner en la sesión del WebSocket.Es conveniente obtener la información del usuario actual en el siguiente código.

7.3.1 Implementación del Administrador de usuarios

Cree la clase OnlineUserManager para administrar el estado en línea del usuario actual. Es esencialmente una estructura de tabla hash. La clave es la identificación del usuario y el valor es la WebSocketSession del usuario.

Dado que este es un estado de subprocesos múltiples, muchos usuarios tendrán problemas de seguridad de subprocesos al acceder a la misma tabla hash, por lo que aquí se usa ConcurrentHashMap para garantizar problemas de seguridad de subprocesos.

Con la ayuda de esta clase, por un lado, puede determinar si el usuario está en línea y, al mismo tiempo, puede obtener fácilmente la sesión para devolver la llamada al cliente.

Al ingresar a la sala, almacenar el usuario en la tabla hash,
al salir, eliminar al usuario de la tabla hash y
consultar la sesión correspondiente a través del ID de usuario, para devolver los datos al cliente.

También hay una interfaz de batalla en la parte posterior, todo creado primero.

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

Los jugadores en el partido se emparejan con puntajes, y todos los jugadores se dividen en tres categorías

Nomal: puntaje < 2000
Alto: puntaje >= 2000 && puntaje < 3000
Muy alto: puntaje >= 3000

De acuerdo con estos tres niveles, se asignan diferentes colas. Se requiere un hilo especial para escanear continuamente la cola coincidente. Si los jugadores coincidentes forman un par, se sacan y se colocan en una habitación.

Proporcione el método add para que la clase MatchAPI llame para agregar el jugador a la cola coincidente.
Proporcione el método remove para que la clase MatchAPI llame para eliminar al jugador de la cola coincidente.
Al mismo tiempo, Matcher encuentra que OnlineUserManager es grabado para obtener la sesión del jugador.

Debido a que handlerMatch se llama en un subproceso separado. Por lo tanto, se debe considerar la seguridad del subproceso para acceder a la cola. Se deben agregar bloqueos.
Cada cola puede usar el objeto de la cola como un bloqueo.
Use esperar en la entrada para esperar hasta que la cola llega a 2 elementos o más, se despierta la cola de consumo de subprocesos.

@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 la clase Habitación

Después de que el partido sea exitoso, los dos jugadores deben colocarse en el mismo objeto de la habitación.

  1. Una sala debe contener una ID de sala, utilizando UUID como identificador único de la sala.
  2. La información de ambos jugadores en el juego debe quedar registrada en la sala.
  3. Registre la identificación del primer motor
  4. Registre una matriz bidimensional como tablero de ajedrez para el juego.
  5. Grabe un OnlineUserManager para una posterior interacción con el cliente.
  6. Por supuesto, ObjectMapper es indispensable para procesar 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 Implementación del administrador de salas

Habrá muchos objetos Room. Cada dos jugadores en el juego corresponde a un objeto Room. Se necesita un objeto administrador para administrar todas las habitaciones.

  1. Use una tabla hash para guardar todos los objetos de la habitación, la clave es roomId, el valor es el objeto de la habitación
  2. Luego use una tabla Hash para guardar el mapeo de ID de usuario -> ID de habitación, lo cual es conveniente para encontrar la habitación de acuerdo con el jugador.
  3. Proporciona una API para agregar, eliminar y verificar (la verificación incluye dos versiones, consulta basada en ID de sala y consulta basada en ID de usuario).
// 房间管理器
// 也要唯一实例
    @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 capa de controlador

websocket tiene 4 métodos:

一: 实现 afterConnectionEstablished 方法.

  1. A través del objeto de sesión en el parámetro, obtenga la información del usuario configurada antes de iniciar sesión.
  2. Use onlineUserManager para administrar el estado en línea de los usuarios.
  3. Primero determine si el usuario ya está en línea, si está en línea, devolverá un error directamente (prohibir la misma cuenta para abrir más de una).
  4. Establece el estado en línea del jugador.
@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. Primero obtenga la información del jugador actual de la sesión.
  2. Analizar la solicitud enviada por el cliente
  3. Determine el tipo de solicitud. Si es startMatch, agregue el objeto de usuario a la cola coincidente. Si es stopMatch, elimine el objeto de usuario de la cola coincidente.
  4. Aquí se debe implementar un objeto comparador para manejar la lógica real de coincidencia.
@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. El trabajo principal es salir del reproductor desde onlineUserManager.
  2. Al salir, preste atención para determinar si el jugador actual tiene más de uno abierto (un ID de usuario, correspondiente a dos conexiones websocket). Si un jugador abre una segunda conexión websocket, entonces la segunda conexión websocket no afectará al jugador. Salir de OnlineUserManager .
  3. Si el jugador está actualmente en la cola de emparejamiento, se eliminará directamente de la cola de emparejamiento.
@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 方法

La misma lógica que afterConnectionClosed, en el caso de un apagado anormal, obtenga la información del usuario, luego establezca el estado en línea en fuera de línea y luego elimine al usuario en la cola coincidente

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

8.1 Interfaces de interacción de front-end y back-end

conectar

ws://127.0.0.1:8080/game

Respuesta de conexión (cuando ambos jugadores están conectados, devuelve datos a ambas partes para indicar que están listos)

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

soltar solicitud:

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

Respuesta de caída:

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

8.2 Desarrollo de clientes

El lienzo aquí se usa para dibujar el tablero de ajedrez,
inserte la descripción de la imagen aquí

archivo js:

  1. Esta parte del código se basa en la API de lienzo. No es necesario que entendamos esta parte. Simplemente copie y pegue el siguiente código directamente.
  2. Use una matriz bidimensional para representar el tablero de ajedrez. Aunque el servidor determina el resultado, el tablero de ajedrez del cliente puede evitar la situación de "movimientos repetidos en una posición".
  3. El efecto de la función oneStep es dibujar una pieza de ajedrez en una posición específica. Puede distinguir si dibujar un carácter blanco o una pieza negra. Los parámetros son la abscisa y la ordenada, correspondientes a la columna y la fila respectivamente.
  4. Use onclick para manejar el evento de clic del usuario. Cuando el usuario hace clic, esta función se usa para controlar el dibujo del peón.
  5. La variable me se usa para indicar si es mi turno de mover, la variable over se usa para indicar el final del juego.
  6. Este código utilizará una imagen de fondo (sky.jpg), que se puede colocar en el directorio de imágenes.
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

En el código de la interfaz:

  1. Primero elimine la llamada original a la función initGame.Después de obtener la respuesta lista del servidor, inicialice el tablero.
  2. Cree un objeto websocket y registre las funciones onopen/onclose/onerror. En caso de error, haga una lógica para saltar al lobby del juego. Cuando la red se desconecte anormalmente, volverá al lobby.
  3. Implemente el método onmessage. onmessage procesa primero la respuesta de juego listo.
// 注意, 路径要写作 /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 una solicitud de movimiento

Modifique la función onclick para agregar la lógica de enviar la solicitud cuando se coloca al niño.

  1. Comente la operación del onStep original y modifique el tablero de ajedrez, y póngalo en procesamiento cuando reciba la respuesta de movimiento.
  2. Implementar enviar, enviar solicitudes de envío a través de 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 Manejo de respuestas de movimiento

En initGame, modifique el mensaje onmessage de websocket

  1. Antes de initGame, se procesa la respuesta de juego listo y, después de recibir la respuesta del juego, se cambia para recibir la respuesta de movimiento.
  2. Al tratar con la respuesta al movimiento, es necesario tratar con la mano ganadora.
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 Desarrollo de servidores

Cree una clase GameController para manejar solicitudes 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 Crear un objeto de subsolicitud/respuesta

Clase 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;
}

clase GameRequest

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

clase 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 Manejo del éxito de la conexión

  1. En primer lugar, debe detectar el estado de inicio de sesión del usuario. Obtenga la información del usuario actual de la sesión.
  2. Luego, debemos determinar si el jugador actual está en la sala.
  3. A continuación, se realiza la determinación de apertura múltiple, si el jugador ya está en el juego, no puede volver a conectarse.
  4. Coloque a los dos jugadores en los objetos de la sala correspondiente. Cuando ambos jugadores están conectados, la sala está llena. En este momento, se notifica a los dos jugadores que ambos están listos.
  5. Si un tercer jugador también intenta unirse a la sala, se le indicará que la sala está llena.
@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 Procesamiento fuera de línea del jugador

@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 Agregar código de tablero de ajedrez a la clase de habitación

Aquí Room quiere inyectar objetos Spring, no puede usar la anotación @Autowired @Resource. Necesita usar contexto

Modificar la clase de inicio

public class JavaGobangApplication {
    
    
    public static ConfigurableApplicationContext context;

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

}

Sala de juego:

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 Manejo de solicitudes de envío

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 el método putChess

  1. Primero, analice la solicitud en un objeto de solicitud.
    2. De acuerdo con la información en el objeto de solicitud, mueva el juego en el tablero de ajedrez.
    3. Después de completar el movimiento, para facilitar la depuración, puede imprimir el estado actual de el tablero de ajedrez.
    4. Verifique si el juego ha terminado.
    5. Construya Responda al movimiento y escríbalo a cada jugador.
    6. Al escribir de nuevo, si se encuentra que un jugador está desconectado, se considera que el otro lado es el ganador
    7. Si el juego ha sido puntuado, modifique la puntuación del jugador y destruya la habitación.
// 玩家落子
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 la lógica de impresión del tablero de ajedrez

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 la victoria o la derrota

  1. Si el juego está empatado, se devuelve el id del jugador, si el juego no está empatado, se devuelve 0.
  2. Un valor de 1 en el tablero significa el movimiento del jugador 1, y un valor de 2 significa el movimiento del jugador 2.
  3. Al verificar el resultado, tome la posición de movimiento actual como el centro y verifique todas las filas, columnas y diagonales relevantes. No es necesario recorrer todo el tablero.
// 判定棋盘形式, 找出胜利的玩家.
// 如果游戏分出胜负, 则返回玩家的 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 Proceso de salida del jugador

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

Supongo que te gusta

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