对战五子棋——网页版

news2024/12/23 13:22:25

目录

一、项目简介

二、用户模块 

1、创建用户实体类

2、编写userMapper接口文件 

3、实现userMapper.xml文件 

4、对用户密码进行加密

5、实现用户登录功能

6、实现用户注册功能 

三、实现用户匹配模块

1、展示用户个人信息 

2、匹配请求类 

3、匹配响应类 

4、创建用户管理类 

5、创建房间类 

6、实现房间管理类 

5、实现匹配器类 

6、实现匹配处理类 

处理连接请求 

处理开始匹配/取消匹配请求

处理连接关闭

处理连接异常 

四、实现五子棋对战模块 

1、前端绘制棋盘和棋子

2、前端初始化 websocket

3、处理落子请求

4、处理落子响应 

5、定义落子请求类

6、定义落子响应类 

7、实现对战功能 

8、实现胜负判定 

9、实现游戏处理类 

处理连接成功

通知玩家就绪 

处理落子请求 

通知另一个玩家获胜

处理玩家下线 以及 连接出错

五、页面展示 


一、项目简介

实现一个网页版的五子棋对战程序,用户可以进行注册,注册完成后登录进入游戏大厅,会显示用户的天梯分数记录以及比赛场次的记录,会根据用户的天梯分数实现匹配机制,实现两个玩家在网页端进行五子棋的对战。

二、用户模块 

1、创建用户实体类

@Data
public class User {
    private int userId; //用户id
    private String username; //用户名
    private String password; //用户密码
    private int score; //用户分数
    private int totalCount; //用户的比赛场次
    private int winCount; //用户的获胜场次
}

2、编写userMapper接口文件 

此处主要提供四个方法:

  • selectByName: 根据用户名查找用户信息. 用于实现登录.
  • insert: 新增用户. 用户实现注册.
  • userWin: 用于给获胜玩家修改分数.
  • userLose: 用户给失败玩家修改分数.
@Mapper
public interface UserMapper {
    // 插入用户. 用于注册功能,返回受影响的行数
    int insert(User user);
    // 根据用户名, 来查询用户的详细信息. 用于登录功能
    User selectByName(String username);
    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    void userWin(int userId);
    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    void userLose(int userId);
}

3、实现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.gobang.mapper.UserMapper">
    <insert id="insert">
        insert into user(username,password,score,totalCount,winCount) values(#{username},#{password},1000,0,0);
    </insert>
    <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>
    <select id="selectByName" resultType="com.example.gobang.model.User">
        select * from user where username = #{username}
    </select>
</mapper>

4、对用户密码进行加密

为了保证用户登录的安全性,使用MD5加盐的方式对用户的的密码进行加密和解密操作。

public class PasswordUtil {
    /*加密操作*/
    public static String encryption(String password){
        String salt = IdUtil.simpleUUID();//生成随机的32位盐值
        String midpwd = SecureUtil.md5(salt+password);
        return salt+"#"+midpwd;//方便解密
    }
    /*解密:判断密码是否相同,并不能得到解密后的密码*/
    public static boolean decrypt(String password,String truePassword){
        if(StringUtils.hasLength(password) && StringUtils.hasLength(truePassword)){
            if(truePassword.length() == 65 && truePassword.contains("#")){
                String[] pwd = truePassword.split("#");
                String salt = pwd[0];//得到盐值
                String midPassword = pwd[1];//得到盐值+密码使用md5加密后的密码
                password = SecureUtil.md5(salt+password);
                if(password.equals(midPassword)){
                    return true;
                }
            }
        }
        return false;
    }
}

5、实现用户登录功能

实现用户登录功能,首先需要确定好前后端交互的接口:

请求:

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=Jake&password=123

响应:

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

{
    userId: 1,
    username: 'Jake',
    score: 1000,
    totalCount: 10,
    winCount: 5
}    
如果登录失败, 返回的是一个 userId 为 0 的对象

后端代码实现:

@RequestMapping("/login")
    public Object login(String username, String password, HttpServletRequest req) {
        if(!StringUtils.hasLength(username)){
            return null;
        }
        // 根据username找到匹配的用户, 并且密码也一致, 就认为登录成功
        User user = userService.selectByName(username);
        if (user == null || !PasswordUtil.decrypt(password, user.getPassword())) {
            // 登录失败;
            return null;
        }
        HttpSession httpSession = req.getSession(true);
        httpSession.setAttribute("user", user);
        return user;
    }

前端代码实现:

<script>
    function login(){
        //对登录名和密码进行非空校验
        var username = jQuery("#username");
        var password = jQuery("#password");
        //对首部去空格后进行非空校验
        if(jQuery.trim(username.val()) === ""){
            alert("请输入登录名");
            //清除原有数据,将光标定位到输入框起始位置
            username.focus();
            return;
        }
        if(jQuery.trim(password.val()) === ""){
            alert("请输入密码");
            password.focus();
            return;
        }
        jQuery.ajax({
            url:"login",
            type:"GET",
            data:{"username":username.val(),"password":password.val()},
            success:function (user){
                if(user && user.userId > 0){
                    //登录成功,进入游戏大厅页面
                    location.href = "game_hall.html";
                }else{
                    alert("用户名或密码输入错误");
                }
            }

        });
    }
</script>

6、实现用户注册功能 

 对于用户注册功能,同样也需要确定好前后端交互的接口:

请求:

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

username=zhangsan&password=123

响应:

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

{
    userId: 1,
    username: 'zhangsan',
    score: 1000,
    totalCount: 10,
    winCount: 5
}    
如果注册失败(比如用户名重复), 返回的是一个 userId 为 0 的对象.

后端代码实现: 

@RequestMapping("/register")
    public int register(String username, String password) {
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){
            return -1;
        }
        if(userService.selectByName(username) != null){
            return 0;
        }
        //将存入数据库中的密码进行加密
        password = PasswordUtil.encryption(password);
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        //将用户存入到数据库
        if(userService.insert(user) > 0){
            return 1;
        }
        return -1;
    }

前端代码实现:

<script>
    //进行注册操作
    function reg(){
        var username = jQuery("#username");
        var password = jQuery("#password");
        var password2 = jQuery("#password2");
        //非空校验
        if(jQuery.trim(username.val()) === ""){
            alert("请先输入用户名");
            username.focus();
            return false;
        }

        if(jQuery.trim(password.val()) === ""){
            alert("请先输入密码");
            password.focus();
            return false;
        }
        if(jQuery.trim(password2.val()) === ""){
            alert("请先输入确认密码");
            password2.focus();
            return false;
        }
        if(password.val() !== password2.val()){
            alert("两次密码输入不一致,请重新输入");
            password.focus();
            password2.focus();
            return false;
        }
        jQuery.ajax({
            url:"register",
            type:"POST",
            data:{
                "username":username.val(),
                "password":password.val(),
            },
            success:function(result){
                if(result === 1){
                    alert("注册成功!");
                    location.href = "login.html";
                }else if(result === 0){
                    alert("用户名已存在")
                }else if (result === -1) {
                    alert("注册失败")
                }
            }
        });
    }
</script>

三、实现用户匹配模块

对于用户匹配模块,首先也要确定好前后交互的接口:

请求:

{
    message: 'startMatch' / 'stopMatch',
}

响应: (匹配成功后的响应)

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

注意:

  • 页面这端拿到匹配响应之后, 就跳转到游戏房间.
  • 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面.

1、展示用户个人信息 

用户登录成功之后才会跳转到用户匹配模块 ,在用户匹配模块首先需要展示用户的个人信息。

前端代码实现:

<script>
    $.ajax({
        method: 'get',
        url: '/userInfo',
        success: function(data) {
            let screen = document.querySelector('#screen');
            screen.innerHTML = '玩家: ' + data.username + ', 分数: ' + data.score + "<br> 比赛场次: " + data.totalCount + ", 获胜场次: " + data.winCount;
        }
    });
</script>

后端代码实现: 

@RequestMapping("/userInfo")
    public Object getUserInfo(HttpServletRequest req) {
        HttpSession httpSession = req.getSession(false);
        User user = (User) httpSession.getAttribute("user");
        // 根据username来查询
        User newUser = userService.selectByName(user.getUsername());
        return newUser;
    }

2、匹配请求类 

@Data
public class MatchRequest {
    private String message = ""; //匹配请求信息

}

3、匹配响应类 

@Data
public class MatchResponse {
    private boolean ok; //是否响应成功
    private String reason; //响应失败原因
    private String message; //响应信息
}

4、创建用户管理类 

创建用户管理类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.

借助这个类, 一方面可以判定用户是否是在线, 同时也可以进行方便的获取到 Session 从而给客户端回话.

  • 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
  • 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
  • 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.

由于在游戏大厅以及游戏房间页面都需要用到用户管理,所以使用两个哈希表 来分别存储两部分的会话.

@Component
public class OnlineUserManager {
    // 表示当前用户在游戏大厅在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    // 表示当前用户在游戏房间的在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

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

    public void exitGameHall(int userId) {
        gameHall.remove(userId);
    }

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

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

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

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

5、创建房间类 

在匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中.

  • 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
  • 房间内要记录对弈的玩家双方信息.
  • 记录先手方的 ID
  • 记录一个 二维数组 , 作为对弈的棋盘.
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();
    }
}

6、实现房间管理类 

Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象,所以需要一个管理器对象来管理所有的 Room.

  • 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
  • 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
  • 提供增, 删, 查方法. (包含基于房间 ID 的查询和基于用户 ID 的查询).
@Component
public class RoomManager {
    // key 为 roomId, value 为一个 Room 对象
    private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();
    private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>();

    public void addRoom(Room room, int userId1, int userId2) {
        rooms.put(room.getRoomId(), room);
        userIdToRoomId.put(userId1, room.getRoomId());
        userIdToRoomId.put(userId2, room.getRoomId());
    }

    public Room getRoomByRoomId(String roomId) {
        return rooms.get(roomId);
    }

    public Room getRoomByUserId(int userId) {
        String roomId = userIdToRoomId.get(userId);
        if (roomId == null) {
            return null;
        }
        return getRoomByRoomId(roomId);
    }

    public void removeRoom(String roomId, int userId1, int userId2) {
        rooms.remove(roomId);
        userIdToRoomId.remove(userId1);
        userIdToRoomId.remove(userId2);
    }
}

5、实现匹配器类 

  • 在 Matcher 中创建三个队列 (队列中存储 User 对象), 分别表示不同的段位的玩家. (此处约定 <2000 一档, 2000-3000 一档, >3000 一档)
  • 提供 add 方法, 供 MatchController类来调用, 用来把玩家加入匹配队列.
  • 提供 remove 方法, 供 MatchController类来调用, 用来把玩家移出匹配队列.
  • 同时 Matcher 找那个要记录 OnlineUserManager, 来获取到玩家的 Session

在 Matcher 的构造方法中, 创建一个线程, 使用该线程扫描每个队列, 把每个队列的头两个元素取出来, 匹配到一组中.

@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 中!");
        }
        System.out.println(normalQueue.size());
    }

    // 当玩家点击停止匹配的时候, 就需要把玩家从匹配队列中删除
    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();
    }
}

实现 handlerMatch方法:由于 handlerMatch 在单独的线程中调用. 因此要考虑到访问队列的线程安全问题. 需要加上锁,每个队列分别使用队列对象本身作为锁即可,在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列。

private void handlerMatch(Queue<User> matchQueue) {
    synchronized (matchQueue) {
        try {
            // 保证只有一个玩家在队列的时候, 不会被出队列. 从而能支持取消功能.
            while (matchQueue.size() < 2) {
                matchQueue.wait();
            }
            // 1. 尝试获取两个元素
            User player1 = matchQueue.poll();
            User player2 = matchQueue.poll();
            System.out.println("匹配出两个玩家: " + player1.getUserId() + ", " + player2.getUserId());
            // 2. 检查玩家在线状态(可能在匹配中玩家突然关闭页面)
            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;
            }
            if (session1 == session2) {
                // 如果得到的两个 session 相同, 说明是同一个玩家两次进入匹配队列
                // 例如玩家点击开始匹配后, 刷新页面, 重新再点开始匹配
                // 此时也把玩家放回匹配队列
                matchQueue.offer(player1);
                return;
            }

            // 3. 将这两个玩家加入到游戏房间中.
              // TODO 一会再写

            // 4. 给玩家1 发回响应数据
            MatchResponse response1 = new MatchResponse();
            response1.setMessage("matchSuccess");
            session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response1)));
            // 5. 给玩家2 发回响应数据
            MatchResponse response2 = new MatchResponse();
            response2.setMessage("matchSuccess");
            session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response2)));
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
    }
}

6、实现匹配处理类 

 创建匹配处理类, 继承自 TextWebSocketHandler 作为处理 websocket 请求的入口类.

需要用到 ObjectMapper, 后续用来处理 JSON 数据.

@Component
public class MatchAPI extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

    @Component
    public class MatchAPI extends TextWebSocketHandler {
    }

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

处理连接请求 

实现 afterConnectionEstablished 方法.

  • 通过参数中的 session 对象, 拿到之前登录时设置的 User 信息.
  • 使用 onlineUserManager 来管理用户的在线状态.
  • 先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开).
  • 设置玩家的上线状态.
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    // 1. 拿到用户信息.
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
        // 拿不到用户的登录信息, 说明玩家未登录就进入游戏大厅了.
        // 则返回错误信息并关闭连接
        MatchResponse response = new MatchResponse();
        response.setOk(false);
        response.setReason("玩家尚未登录!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        return;
    }
    // 2. 检查玩家的上线状态
    if (onlineUserManager.getSessionFromGameHall(user.getUserId()) != null
        || onlineUserManager.getSessionFromGameRoom(user.getUserId()) != null) {
        MatchResponse response = new MatchResponse();
        response.setOk(false);
        response.setReason("禁止多开游戏大厅页面!");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        return;
    }
    // 3. 设置玩家上线状态
    onlineUserManager.enterGameHall(user.getUserId(), session);
    System.out.println("玩家进入匹配页面: " + user.getUserId());
}

处理开始匹配/取消匹配请求

实现 handleTextMessage

  • 先从会话中拿到当前玩家的信息.
  • 解析客户端发来的请求
  • 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
  • 此处需要实现一个 匹配器 对象, 来处理匹配的实际逻辑.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    // 1. 拿到用户信息.
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
        System.out.println("[onMessage] 玩家尚未登录!");
        return;
    }
    System.out.println("开始匹配: " + user.getUserId() + " message: " + message.toString());
    // 2. 解析读到的数据为 json 对象
    MatchRequest request = objectMapper.readValue(message.getPayload(), MatchRequest.class);
    MatchResponse response = new MatchResponse();
    if (request.getMessage().equals("startMatch")) {
        matcher.add(user);
        response.setMessage("startMatch");
    } else if (request.getMessage().equals("stopMatch")) {
        matcher.remove(user);
        response.setMessage("stopMatch");
    } else {
        // 匹配失败
        response.setOk(false);
        response.setReason("非法的匹配请求!");
    }
    session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}

处理连接关闭

实现 afterConnectionClosed

  • 主要的工作就是把玩家从 onlineUserManager 中退出.
  • 退出的时候要注意判定, 当前玩家是否是多开的情况(一个userId, 对应到两个 websocket 连接). 如果一个玩家开启了第二个 websocket 连接, 那么这第二个 websocket 连接不会影响到玩家从 OnlineUserManager 中退出.
  • 如果玩家当前在匹配队列中, 则直接从匹配队列里移除.
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
        System.out.println("[onClose] 玩家尚未登录!");
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameHall(user.getUserId());
    if (existSession != session) {
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("玩家离开匹配页面: " + user.getUserId());
    onlineUserManager.exitGameHall(user.getUserId());
    // 如果玩家在匹配中, 则关闭页面时把玩家移出匹配队列
    matcher.remove(user);
}

处理连接异常 

连接异常与连接关闭的逻辑类似。

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
        System.out.println("[onError] 玩家尚未登录!");
        return;
    }
    WebSocketSession existSession = onlineUserManager.getSessionFromGameHall(user.getUserId());
    if (existSession != session) {
        System.out.println("当前的会话不是玩家游戏中的会话, 不做任何处理!");
        return;
    }
    System.out.println("匹配页面连接出现异常! userId: " + user.getUserId() + ", message: " + exception.getMessage());
    onlineUserManager.exitGameHall(user.getUserId());
    // 如果玩家在匹配中, 则关闭页面时把玩家移出匹配队列
    matcher.remove(user);
}

前端代码实现: 

// 1. 和服务器建立连接
let websocket = new WebSocket('ws://127.0.0.1:8080/findMatch');
// 2. 点击开始匹配
let button = document.querySelector('#match-button');
button.onclick = function() {
    if (websocket.readyState == websocket.OPEN) {
        if (button.innerHTML == '开始匹配') {
            console.log('开始匹配!');
            websocket.send(JSON.stringify({
                message: 'startMatch',
            }));
        } else if (button.innerHTML == '匹配中...(点击取消)') {
            console.log('取消匹配!');
            websocket.send(JSON.stringify({
                message: 'stopMatch'
            }));
        }
    } else {
        alert('当前您连接断开! 请重新登录!');
        location.assign('/login.html');
    }
}

// 3. 处理服务器的响应
websocket.onmessage = function(e) {
    let resp = JSON.parse(e.data)
    if (!resp.ok) {
        console.log('游戏大厅中发生错误: ' + resp.reason);
        location.assign('/login.html');
        return;
    }
    if (resp.message == 'startMatch') {
        console.log('进入匹配队列成功!');
        button.innerHTML = '匹配中...(点击取消)';
    } else if (resp.message == 'stopMatch') {
        console.log('离开匹配队列成功!');
        button.innerHTML = '开始匹配';
    } else if (resp.message == 'matchSuccess') {
        console.log('匹配成功! 进入游戏页面!');
        location.assign('/game_room.html');
    } else {
        console.log('非法的 message: ' + resp.message);
    }
}

// 4. 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
    websocket.close();
}

四、实现五子棋对战模块 

首先约定好前后端交互的接口:

连接:

ws://127.0.0.1:8080/game

连接响应:当两个玩家都连接好了, 则给双方都返回一个数据表示就绪

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

落子请求:

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

落子响应:

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

1、前端绘制棋盘和棋子

这部分代码基于 canvas API,首先使用一个二维数组来表示棋盘. 虽然胜负是通过服务器判定的, 但是客户端的棋盘可以避免 "一个位置重复落子" 这样的情况;

oneStep 函数起到的效果是在一个指定的位置上绘制一个棋子. 可以区分出绘制白字还是黑子. 参数是横坐标和纵坐标, 分别对应列和行.

me 变量用来表示当前是否轮到我落子. over 变量用来表示游戏结束.

gameInfo = {
    roomId: null,
    thisUserId: null,
    thatUserId: null,
    isWhite: true,
}

function setScreenText(me) {
    let screen = document.querySelector('#screen');
    if (me) {
        screen.innerHTML = "轮到你落子了!";
    } else {
        screen.innerHTML = "轮到对方落子了!";
    }
}

//初始化游戏
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();
    }

   
}

2、前端初始化 websocket

在 前端页面中中, 加入 websocket 的连接代码, 实现前后端交互.

  • 在获取到服务器反馈的就绪响应之后, 再初始化棋盘.
  • 创建 websocket 对象, 并注册 onopen/onclose/onerror 函数. 其中在 onerror 中做一个跳转到游戏大厅的逻辑. 当网络异常断开, 则回到大厅.
  • 实现 onmessage 方法. onmessage 先处理游戏就绪响应.
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);
}

3、处理落子请求

定义 onclick 函数, 在落子操作时加入发送请求的逻辑.

  • 实现 send , 通过 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);
    }
}

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

4、处理落子响应 

在 initGame 中, 修改 websocket 的 onmessage

  • 在 initGame 之前, 处理的是游戏就绪响应, 在收到游戏响应之后, 就改为接收落子响应了.
  • 在处理落子响应中要处理比赛结果。
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);
}

5、定义落子请求类

@Data
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;
}
@Data
public class GameRequest {
    private String message = "putChess";
    private int userId;
    private int row;
    private int col;
}

6、定义落子响应类 

@Data
public class GameResponse {
    private String message = "putChess";
    private int userId;
    private int row;
    private int col;
    private int winner; // 胜利玩家的 userId
}

7、实现对战功能 

 在 room 类中定义 putChess 方法.

  • 先把请求解析成请求对象.
  • 根据请求对象中的信息, 往棋盘上落子.
  • 落子完毕之后, 为了方便调试, 可以打印出棋盘的当前状况.
  • 检查游戏是否结束.
  • 构造落子响应, 写回给每个玩家.
  • 写回的时候如果发现某个玩家掉线, 则判定另一方为获胜.
  • 如果游戏胜负已分, 则修改玩家的分数, 并销毁房间.
// 玩家落子
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、实现胜负判定 

  • 如果游戏分出胜负, 则返回玩家的 id. 如果未分出胜负,则返回 0.
  • 棋盘中值为 1 表示是玩家 1 的落子, 值为 2 表示是玩家 2 的落子.
  • 检查胜负的时候, 以当前落子位置为中心, 检查所有相关的行,列, 对角线即可. 不必遍历整个棋盘.
// 判定棋盘形式, 找出胜利的玩家.
// 如果游戏分出胜负, 则返回玩家的 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();
}

9、实现游戏处理类 

创建 gameController类 , 处理 websocket 请求.

@Component
public class GameAPI 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 {
    }
}

处理连接成功

实现 GameController类的 afterConnectionEstablished 方法.

  • 首先需要检测用户的登录状态. 从 Session 中拿到当前用户信息.
  • 然后要判定当前玩家是否是在房间中.
  • 接下来进行多开判定.如果玩家已经在游戏中, 则不能再次连接.
  • 把两个玩家放到对应的房间对象中. 当两个玩家都建立了连接, 房间就放满了.这个时候通知两个玩家双方都准备就绪.
  • 如果有第三个玩家尝试也想加入房间, 则给出一个提示, 房间已经满了.
@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);
}

通知玩家就绪 

private void noticeGameReady(Room room, int thisUserId, int thatUserId) throws IOException {
    GameReadyResponse resp = new GameReadyResponse();
    resp.setRoomId(room.getRoomId());
    resp.setThisUserId(thisUserId);
    resp.setThatUserId(thatUserId);
    resp.setWhiteUserId(room.getWhiteUserId());
    WebSocketSession session1 = onlineUserManager.getSessionFromGameRoom(thisUserId);
    session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
}

处理落子请求 

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

通知另一个玩家获胜

// 通知另外一个玩家直接获胜!
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)));
}

处理玩家下线 以及 连接出错

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

五、页面展示 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/761383.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

郭东白的架构课学习笔笔记(1)

1.架构师的品质 自信和勇气&#xff08;正确的废话&#xff09;拥有战略意图&#xff0c;所谓战略意图&#xff0c;就是拥有与其资源和能力极不相称的雄心壮志。使用演绎法寻找架构原理&#xff0c;而不是归纳法。 2.如何提升自己的架构能力 向身边比自己厉害的优秀架构师或…

【网络安全带你练爬虫-100练】第14练:文件内容的读取、取出

目录 一、目标1&#xff1a;把文件内容遍历取出 二、目标2&#xff1a;把文件内容全部取出 三、网络安全O 一、目标1&#xff1a;把文件内容遍历取出 &#xff08;1&#xff09;如果文件脚本在不同目录 file_path "path/to/your/file.txt" # 替换为你的文件路径…

【Gradle】Gradle之JVM进程详解

个人主页&#xff1a;金鳞踏雨 个人简介&#xff1a;大家好&#xff0c;我是金鳞&#xff0c;一个初出茅庐的Java小白 目前状况&#xff1a;22届普通本科毕业生&#xff0c;几经波折了&#xff0c;现在任职于一家国内大型知名日化公司&#xff0c;从事Java开发工作 我的博客&am…

【机密计算组织】机密计算联盟

一、简介 1.1 机密计算联盟与成员 2019年8月22日&#xff0c;Linux基金会宣布多家巨头企业组建“机密计算联盟”&#xff08;Confidential Computing Consortium&#xff09;&#xff0c;该基金将负责对联盟活动进行监督。机密计算联盟专门针对云服务及硬件生态&#xff0c;致…

SpringBoot中注入ServletFilterListener

1.基本介绍 文档&#xff1a;SpringBoot中注入Servlet&Filter&Listener 考虑到实际开发业务非常复杂和兼容问题&#xff0c;SpringBoot支持将Servlet、Filter、Listener注入spring容器中&#xff0c;成为Spring Bean也就是说&#xff0c;SpringBoot开放了和原生WEB组件…

关于初识MySQL数据库以及MySQL的基本使用

文章目录 什么是数据库什么是MySQL为什么要有数据库 MySQL基本使用连接mysql查看当前服务器对应的数据库创建数据库进入某个数据库建立一张表向表中插入数据查询表中的数据 服务器&#xff0c;数据库&#xff0c;表之间的关系数据逻辑存储MySQL架构SQL语句分类存储引擎 什么是数…

vue-cesium的基本使用【一】

最近的项目中用到了cesium,也了解了一点关于cesium的知识&#xff0c;打点、 标绘、等等基础的功能点&#xff0c;但是在开发过程中使用原生的cesium编写对于初学者还是有点难度&#xff0c;为此&#xff0c;找到关于对cesium进行二次封装的开源项目vue-cesium,本次文章主要记录…

Git原理与基本操作(含安装教程)

Git Git初识Git安装Linux-centosLinux-ubuntu Git基本操作git int配置Git 认识⼯作区、暂存区、版本库添加⽂件--场景⼀git add、git commitgit log 查看.git⽂件git cat-file 添加⽂件--场景⼆修改⽂件git statusgit diff [file]、git diff HEAD -- [file] 版本回退git resetg…

聊一下2023前端状况

2023已过大半&#xff0c;也是疫情后开放的第一年&#xff0c;想必大家都能感受到 各行各业一定程度上都在萎缩&#xff0c;线下一些工厂招聘&#xff0c;喊着90年以前的&#xff0c;可以回家了的 今年出现频率最高的一词就是裁员&#xff0c;失业&#xff0c;大学生慢就业 互联…

【从零开始学习Linux】背景知识与获取环境

哈喽&#xff0c;哈喽&#xff0c;大家好~ 我是你们的老朋友&#xff1a;保护小周ღ 本期给大家带来的是 Linux 操作系统的简介&#xff0c;以及如何获取一个Linux 的环境&#xff0c;作为Linux 章节起始篇&#xff0c;如果不妥之处&#xff0c;欢迎批评指正~ 本期收录于博主…

谷粒商城第二篇服务功能-商品服务-三级分类

商品服务三级分类工程初始化及查询搭建 在数据库中插入数据 1.controller类 RestController RequestMapping("product/category") public class CategoryController {Autowiredprivate CategoryService categoryService;/*** 查出所有分类以及子分类&#xff0c;…

前端Vue自定义可自由滚动新闻栏tabs选项卡标签栏标题栏组件

随着技术的发展&#xff0c;开发的复杂度也越来越高&#xff0c;传统开发方式将一个系统做成了整块应用&#xff0c;经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改&#xff0c;造成牵一发而动全身。 通过组件化开发&#xff0c;可以有效实现…

vue3.0之组合API有哪些(详解)

vue3.0之组合API有哪些 一、setup函数二、生命周期三、reactive函数四、toRef函数五、toRefs函数六、ref函数七、知识运用案例八、computed函数九、watch函数十、ref属性十一、父子通讯1.父传子2.子传父 十二、依赖注入十三、补充 v-model语法糖(简写)十四、补充 mixins语法 一…

【档案专题】三、电子档案管理系统

导读&#xff1a;主要针对电子档案管理系统相关内容介绍。对从事电子档案管理信息化的职业而言&#xff0c;不断夯实电子档案管理相关理论基础是十分重要。只有通过不断梳理相关知识体系和在实际工作当中应用实践&#xff0c;才能走出一条专业化加职业化的道路&#xff0c;从而…

el-cascader级联选择器那些事

el-cascader级联选择器那些事 1、获取选中的节点及其所有上级 vue3element-plusts 1、获取选中的节点及其所有上级 使用cascader组件提供的getCheckedNodes() <el-cascader :options"options" :show-all-levels"false" change"changeCascader&q…

cocosCreator笔记 之Spine了解

版本&#xff1a; 3.4.0 参考&#xff1a; Spine 骨骼动画资源 Spine Skeleton组件 cocosLua 之 骨骼动画 简介 使用spine动画&#xff0c;cocosCreator目前支持的版本&#xff1a; creator版本spine版本V3.0 及以上v3.8&#xff08;原生平台不支持特定版本 v3.8.75&…

北京银行发放门头沟区首笔知识产权质押贷款

6月&#xff0c;位于北京中关村门头沟科技园、专注于研制工业母机的民营企业——北京精雕科技集团有限公司&#xff08;以下简称“精雕科技集团”&#xff09;&#xff0c;因生产经营急需资金&#xff0c;但是由于缺乏抵押物而陷入了融资困境。“精雕科技集团与北京银行合作已长…

java习题3

292. Nim 游戏 难度简单 你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a; 桌子上有一堆石头。你们轮流进行自己的回合&#xff0c; 你作为先手 。每一回合&#xff0c;轮到的人拿掉 1 - 3 块石头。拿掉最后一块石头的人就是获胜者。 假设你们每一步都是最优解。…

Slf4j日志集成

Slf4j日志集成 下面就是集成步骤&#xff0c;按着做就可以了 1、logback-spring.xml 哪个服务需要记录日志就将在哪个服务的resource下新建logback-spring.xml文件&#xff0c;里面的内容如下&#xff1a; <!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 …

微软宣布Win10准备热烈的迎接Docker

在DockerCon 2017大会上&#xff0c;Docker团队今天宣布了LinuxKit&#xff0c;这是一个安全、干净和便携式的Linux子系统container容器环境。LinuxKit允许工具构建自定义的Linux子系统&#xff0c;可以仅包含完全运行时平台的组件需要。所有的系统服务都是可替换的容器&#x…