笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接AcWing SpringBoot 框架课

1. 同步玩家位置

1.1 游戏信息记录

两名玩家初始位置需要服务器确定,且之后的每次移动需要服务器上判断我们需要Game 类中添加 Player用来记录玩家信息,在 consumer.utils 包下创建 Player 类:

package com.kob.backend.consumer.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
    private Integer id;
    private Integer sx;
    private Integer sy;
    private List<Integer> steps;  // 记录历史走过的每一步方向
}

然后可以Game创建玩家

package com.kob.backend.consumer.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

public class Game {
    ...
    private final Player playerA, playerB;

    public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {
        ...
        playerA = new Player(idA, rows - 2, 1, new ArrayList<>());  // 默认A在左下角B在右上角
        playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
    }

    public Player getPlayerA() {
        return playerA;
    }

    public Player getPlayerB() {
        return playerB;
    }
    ...
}

WebSocketServer创建 Game 时传入两名玩家的 ID,并且我们将与游戏内容相关信息全部包装一个 JSONObject 类中:

package com.kob.backend.consumer;

import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    ...

    private void startMatching() {
        System.out.println("Start matching!");
        matchPool.add(this.user);

        while (matchPool.size() >= 2) {  // 临时调试用的,未来要替换成微服务
            Iterator<User> it = matchPool.iterator();
            User a = it.next(), b = it.next();
            matchPool.remove(a);
            matchPool.remove(b);

            Game game = new Game(13, 14, 20, a.getId(), b.getId());
            game.createMap();

            JSONObject respGame = new JSONObject();
            respGame.put("a_id", game.getPlayerA().getId());
            respGame.put("a_sx", game.getPlayerA().getSx());
            respGame.put("a_sy", game.getPlayerA().getSy());
            respGame.put("b_id", game.getPlayerB().getId());
            respGame.put("b_sx", game.getPlayerB().getSx());
            respGame.put("b_sy", game.getPlayerB().getSy());
            respGame.put("map", game.getG());

            JSONObject respA = new JSONObject();  // 发送给A的信息
            respA.put("event", "match_success");
            respA.put("opponent_username", b.getUsername());
            respA.put("opponent_photo", b.getPhoto());
            respA.put("game", respGame);
            users.get(a.getId()).sendMessage(respA.toJSONString());  // A不一定是当前链接,因此要在users中获取

            JSONObject respB = new JSONObject();  // 发送给B的信息
            respB.put("event", "match_success");
            respB.put("opponent_username", a.getUsername());
            respB.put("opponent_photo", a.getPhoto());
            respB.put("game", respGame);
            users.get(b.getId()).sendMessage(respB.toJSONString());
        }
    }

    ...
}

前端需要进行相应的修改,在 store/pk.js 中创建两名玩家信息

export default {
  state: {
    ...
    a_id: 0,
    a_sx: 0,
    a_sy: 0,
    b_id: 0,
    b_sx: 0,
    b_sy: 0,
    gameObject: null,  // 整个GameMap对象
  },
  getters: {},
  mutations: {
    ...

    updateGame(state, game) {
      state.game_map = game.map;
      state.a_id = game.a_id;
      state.a_sx = game.a_sx;
      state.a_sy = game.a_sy;
      state.b_id = game.b_id;
      state.b_sx = game.b_sx;
      state.b_sy = game.b_sy;
    },

    updateGameObject(state, gameObject) {
      state.gameObject = gameObject;
    },
  },
  actions: {},
  modules: {},
};

GameMap.vue需要先将 GameMap 对象存下来,之后会在 PKIndexView用到

...

<script>
import { ref, onMounted } from "vue";
import { GameMap } from "@/assets/scripts/GameMap";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    let parent = ref(null);
    let canvas = ref(null);

    onMounted(() => {
      store.commit(
        "updateGameObject",
        new GameMap(canvas.value.getContext("2d"), parent.value, store));
    });

    return {
      parent,
      canvas,
    };
  },
};
</script>

...

PKIndexView 中要传入从后端获取到的 game 数据

...

<script>
...

export default {
  ...
  setup() {
    ...

    onMounted(() => {
      ...

      socket.onmessage = (msg) => {  // 接收到后端消息时会执行
        ...

        if (data.event === "match_success") {  // 匹配成功
          ...

          store.commit("updateGame", data.game);  // 更新游戏内容

          ...
        }
      };

      ...
    });

    ...
  },
};
</script>

<style scoped></style>

1.2 实现多线程同步移动

我们需要实现两名玩家客户端以及服务器端的移动同步,假如 Client1 发出了移动指令,那么就会将这个消息发送服务器,同理另一个客户端 Client2 发出移动指令时也会将消息发送服务器,服务器在接收到两名玩家的消息后再将消息同步给两名玩家。

我们WebSocketServer 中会维护一个游戏 Game,这个 Game 也有自己执行流程,它会先创建地图 creatMap,接着会一步一步执行,即 nextStep,每一步等待两名玩家的操作,这个操作可以键盘输入,也可以是由 Bot 代码执行的微服务返回回来的结果获取输入后会将结果发送一个评判系统 judge,来判断两名玩家下一步是不是合法的,如果有一方不合法就游戏结束

等待用户输入时会有一个时间限制比如5秒,如果有一方还没有输入则表示输了,同样也是游戏结束。否则如果两方输入的下一步都是合法的则继续循环 nextStep。这个 nextStep 流程比较独立的,而且每个游戏对局都有这个独立的过程,如果 Game 是单线程的,那么在等待用户输入时这个线程就会卡死,如果有多个游戏对局的话那么只能先卡死在某个对局中,其他对局的玩家体验就会很差,因此 Game 不能作为一个单线程处理,每次在等待用户输入时都需要另起一个新的线程,这就涉及到了多线程通信以及加锁问题

我们Game继承Thread即可转为多线程然后需要实现 Thread 的入口函数使用快捷键 Alt + Insert选择重写方法,需要重写的是 run() 方法,这是新线程的入口函数

package com.kob.backend.consumer.utils;

import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class Game extends Thread {
    private final Integer rows;
    private final Integer cols;
    private final Integer inner_walls_count;
    private final boolean[][] g;
    private static final int[] dx = { -1, 0, 1, 0 }, dy = { 0, 1, 0, -1 };
    private final Player playerA, playerB;
    private Integer nextStepA = null;  // 下一步操作,0、1、2、3分别表示四个方向,null表示还没有获取
    private Integer nextStepB = null;
    private ReentrantLock lock = new ReentrantLock();  // 需要给nextStep变量上锁防止读写冲突
    private String status = "playing";  // 整局游戏的状态结束后为finished
    private String loser = "";  // 输的一方是谁,all表示平局

    public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {
        this.rows = rows;
        this.cols = cols;
        this.inner_walls_count = inner_walls_count;
        this.g = new boolean[rows][cols];
        playerA = new Player(idA, rows - 2, 1, new ArrayList<>());  // 默认A在左下角B在右上角
        playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
    }

    public Player getPlayerA() {
        return playerA;
    }

    public Player getPlayerB() {
        return playerB;
    }

    public void setNextStepA(Integer nextStepA) {  // 未来会在另一个线程中调用
        lock.lock();  // 操作nextStep变量前先上锁
        try {
            this.nextStepA = nextStepA;
        } finally {
            lock.unlock();  // 操作完后无论是否异常解锁
        }
    }

    public void setNextStepB(Integer nextStepB) {
        lock.lock();
        try {
            this.nextStepB = nextStepB;
        } finally {
            lock.unlock();
        }
    }

    public boolean[][] getG() {
        return g;
    }

    private boolean check_connectivity(int sx, int sy, int tx, int ty) {
        if (sx == tx &amp;&amp; sy == ty) return true;
        g[sx][sy] = true;
        for (int i = 0; i < 4; i++) {
            int nx = sx + dx[i], ny = sy + dy[i];
            if (!g[nx][ny] &amp;&amp; check_connectivity(nx, ny, tx, ty)) {
                g[sx][sy] = false;  // 注意在这里我们用的g就是原始数组,因此修改后要记得还原
                return true;
            }
        }
        g[sx][sy] = false;  // 记得还原
        return false;
    }

    private boolean drawMap() {
        // 初始化障碍物标记数组
        for (int i = 0; i < this.rows; i++) {
            Arrays.fill(g[i], false);
        }

        // 给地图四周加上障碍物
        for (int r = 0; r < this.rows; r++) {
            g[r][0] = g[r][this.cols - 1] = true;
        }
        for (int c = 0; c < this.cols; c++) {
            g[0][c] = g[this.rows - 1][c] = true;
        }

        // 添加地图内部随机障碍物,需要有对称性因此枚举一半即可,另一半对称生成
        Random random = new Random();
        for (int i = 0; i < this.inner_walls_count / 2; i++) {
            for (int j = 0; j < 10000; j++) {
                int r = random.nextInt(this.rows);  // 返回0~this.rows-1的随机整数
                int c = random.nextInt(this.cols);
                if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;
                if (r == this.rows - 2 &amp;& c == 1 || r == 1 && c == this.cols - 2) continue;
                g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
                break;
            }
        }

        return check_connectivity(this.rows - 2, 1, 1, this.cols - 2);
    }

    public void createMap() {
        for (int i = 0; i < 10000; i++) {
            if (drawMap()) {
                break;
            }
        }
    }

    private boolean nextStep() {  // 等待两名玩家的下一步操作,在该方法中也会操作nextStep变量
        try {
            Thread.sleep(500);  // 前端的蛇每秒走2格,因此走一格需要500ms,每次后端执行下一步时需要先sleep,否则快速多次输入将会覆盖掉之前输入的信息
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 50; i++) {
            try {
                Thread.sleep(100);  // 每回合循环50次,每次睡眠100ms,即一回合等待用户输入的时间为5s
                lock.lock();
                try {
                    if (nextStepA != null && nextStepB != null) {  // 两名玩家的下一步操作都读到了
                        playerA.getSteps().add(nextStepA);
                        playerB.getSteps().add(nextStepB);
                        return true;
                    }
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    private void judge() {  // 判断两名玩家下一步操作是否合法
    }

    private void sendAllMessage(String message) {  // 向两个Client发送消息
        WebSocketServer.users.get(playerA.getId()).sendMessage(message);
        WebSocketServer.users.get(playerB.getId()).sendMessage(message);
    }

    private void sendMove() {  // 向两个Client发送移动消息
        lock.lock();
        try {
            JSONObject resp = new JSONObject();
            resp.put("event", "move");
            resp.put("a_direction", nextStepA);
            resp.put("b_direction", nextStepB);
            sendAllMessage(resp.toJSONString());
            nextStepA = nextStepB = null;
        } finally {
            lock.unlock();
        }
    }

    private void sendResult() {  // 向两个Client公布结果
        JSONObject resp = new JSONObject();
        resp.put("event", "result");
        resp.put("loser", loser);
        sendAllMessage(resp.toJSONString());
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {  // 游戏最多走的步数不会超过1000
            if (nextStep()) {  // 是否获取了两条蛇的下一步操作
                judge();
                if ("playing".equals(status)) {  // 如果游戏还在进行中则需要将两名玩家的操作广播两个Client
                    sendMove();
                } else {
                    sendResult();
                    break;
                }
            } else {
                status = "finished";
                lock.lock();
                try {
                    if (nextStepA == null && nextStepB == null) {
                        loser = "all";
                    } else if (nextStepA == null) {
                        loser = "A";
                    } else {
                        loser = "B";
                    }
                } finally {
                    lock.unlock();
                }
                sendResult();  // 这一步结束后需要给两个Client发送消息
                break;
            }
        }
    }
}

然后前端 GameMap.js 中在移动时需要向后端通信,现在两名玩家的键盘输入操作就只需要 W/S/A/D 了:

import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObject {
  ...

  add_listening_events() {
    this.ctx.canvas.focus();  // 使Canvas聚焦

    this.ctx.canvas.addEventListener("keydown", e => {
      let d = -1;
      if (e.key === "w") d = 0;
      else if (e.key === "d") d = 1;
      else if (e.key === "s") d = 2;
      else if (e.key === "a") d = 3;

      if (d !== -1) {
        this.store.state.pk.socket.send(JSON.stringify({
          event: "move",
          direction: d,
        }));
      }
    });
  }

  ...
}

WebSocketServer 对于每局游戏对局都会创建一个 Game 类,通过 start() 方法可以新开一个线程运行 Game 中的 run() 方法,由于我们需要在 Game使用 WebSocketServerusers,还需要将 users 修改public然后需要接收前端的移动请求

package com.kob.backend.consumer;

import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    // ConcurrentHashMap是一个线程安全哈希表,用于用户ID映射到WS实例
    public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
    // CopyOnWriteArraySet也是线程安全
    private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>();  // 匹配池
    private User user;
    private Session session = null;
    private Game game = null;

    private static UserMapper userMapper;

    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        WebSocketServer.userMapper = userMapper;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) throws IOException {
        this.session = session;
        System.out.println("Connected!");

        Integer userId = JwtAuthentication.getUserId(token);
        this.user = userMapper.selectById(userId);

        if (user != null) {
            users.put(userId, this);
        } else {
            this.session.close();
        }
    }

    @OnClose
    public void onClose() {
        System.out.println("Disconnected!");
        if (this.user != null) {
            users.remove(this.user.getId());
            matchPool.remove(this.user);
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {  // 一般会把onMessage()当作路由
        System.out.println("Receive message!");
        JSONObject data = JSONObject.parseObject(message);
        String event = data.getString("event");  // 取出event的内容

        if ("start_match".equals(event)) {  // 开始匹配
            this.startMatching();
        } else if ("stop_match".equals(event)) {  // 取消匹配
            this.stopMatching();
        } else if ("move".equals(event)) {  // 移动
            move(data.getInteger("direction"));
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    public void sendMessage(String message) {  // 从后端向当前链接发送消息
        synchronized (this.session) {  // 由于是异步通信,需要加一个锁
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void startMatching() {
        System.out.println("Start matching!");
        matchPool.add(this.user);

        while (matchPool.size() >= 2) {  // 临时调试用的,未来要替换成微服务
            Iterator<User> it = matchPool.iterator();
            User a = it.next(), b = it.next();
            matchPool.remove(a);
            matchPool.remove(b);

            game = new Game(13, 14, 20, a.getId(), b.getId());
            game.createMap();
            users.get(a.getId()).game = game;
            users.get(b.getId()).game = game;

            game.start();  // 开一个新的线程

            JSONObject respGame = new JSONObject();
            respGame.put("a_id", game.getPlayerA().getId());
            respGame.put("a_sx", game.getPlayerA().getSx());
            respGame.put("a_sy", game.getPlayerA().getSy());
            respGame.put("b_id", game.getPlayerB().getId());
            respGame.put("b_sx", game.getPlayerB().getSx());
            respGame.put("b_sy", game.getPlayerB().getSy());
            respGame.put("map", game.getG());

            JSONObject respA = new JSONObject();  // 发送给A的信息
            respA.put("event", "match_success");
            respA.put("opponent_username", b.getUsername());
            respA.put("opponent_photo", b.getPhoto());
            respA.put("game", respGame);
            users.get(a.getId()).sendMessage(respA.toJSONString());  // A不一定是当前链接,因此要在users中获取

            JSONObject respB = new JSONObject();  // 发送给B的信息
            respB.put("event", "match_success");
            respB.put("opponent_username", a.getUsername());
            respB.put("opponent_photo", a.getPhoto());
            respB.put("game", respGame);
            users.get(b.getId()).sendMessage(respB.toJSONString());
        }
    }

    private void stopMatching() {
        System.out.println("Stop matching!");
        matchPool.remove(this.user);
    }

    private void move(Integer direction) {
        if (game.getPlayerA().getId().equals(user.getId())) {
            game.setNextStepA(direction);
        } else if (game.getPlayerB().getId().equals(user.getId())) {
            game.setNextStepB(direction);
        }
    }
}

最后PKIndexView处理接收到后端发来的移动消息以及游戏结束消息:

<template>
  <PlayGround v-if="$store.state.pk.status === 'playing'" />
  <MatchGround v-else />
</template>

<script>
import PlayGround from "@/components/PlayGround.vue";
import MatchGround from "@/components/MatchGround.vue";
import { onMounted, onUnmounted } from "vue";
import { useStore } from "vuex";

export default {
  components: {
    PlayGround,
    MatchGround,
  },
  setup() {
    const store = useStore();

    let socket = null;
    let socket_url = `ws://localhost:3000/websocket/${store.state.user.jwt_token}/`;

    onMounted(() => {
      socket = new WebSocket(socket_url);

      store.commit("updateOpponent", {
        username: "我的对手",
        photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
      });

      socket.onopen = () => {  // 链接成功建立后会执行
        console.log("Connected!");
        store.commit("updateSocket", socket);
      };

      socket.onmessage = (msg) => {  // 接收到后端消息时会执行
        const data = JSON.parse(msg.data);  // Spring传过来数据是放在消息的data中
        console.log(data);

        if (data.event === "match_success") {  // 匹配成功
          store.commit("updateOpponent", {  // 更新对手信息
            username: data.opponent_username,
            photo: data.opponent_photo,
          });
          store.commit("updateGame", data.game);  // 更新游戏内容

          setTimeout(() => {  // 3秒后再进入游戏地图界面
            store.commit("updateStatus", "playing");
          }, 3000);
        } else if (data.event === "move") {  // 两名玩家的移动
          const gameObject = store.state.pk.gameObject;
          const [snake0, snake1] = gameObject.snakes;
          snake0.set_direction(data.a_direction);
          snake1.set_direction(data.b_direction);
        } else if (data.event === "result") {  // 游戏结束
          const gameObject = store.state.pk.gameObject;
          const [snake0, snake1] = gameObject.snakes;
          if (data.loser === "all" || data.loser === "A") {
            snake0.status = "die";
          }
          if (data.loser === "all" || data.loser === "B") {
            snake1.status = "die";
          }
        }
      };

      socket.onclose = () => {  // 关闭链接后会执行
        console.log("Disconnected!");
        store.commit("updateStatus", "matching");  // 进入游戏地图后玩家点击其他页面应该默认退出游戏
      };
    });

    onUnmounted(() => {
      socket.close();  // 如果不断开链接每次切换页面都会创建新链接,就会导致有很多冗余链接
    });
  },
};
</script>

<style scoped></style>

2. 同步碰撞检测

现在还需要将碰撞检测放到后端进行判断,先将 Snake.js 中的碰撞检测判断代码删掉,并将死后变白的逻辑放到 render() 函数中:

...

export class Snake extends AcGameObject {
  ...

  next_step() {  // 将蛇的状态变为走下一步
    ...

    // if (!this.gamemap.check_next_valid(this.next_cell)) {  // 下一步不合法
    //   this.status = "die";
    // }
  }

  ...

  render() {
    ...

    ctx.fillStyle = this.color;
    if (this.status === "die") {
      ctx.fillStyle = "white";
    }

    ...
  }
}

接下来需要实现后端中的 judge() 方法,在判断的时候需要知道当前蛇的身体有哪些,先在 comsumer.utils 包下创建 Cell 类表示身体的每一格:

package com.kob.backend.consumer.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {
    int x;
    int y;
}

然后Player 类中创建一个方法能够根据玩家历史走过的路径找出当前这条蛇身体的每一格:

package com.kob.backend.consumer.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
    private Integer id;
    private Integer sx;
    private Integer sy;
    private List<Integer> steps;  // 记录历史走过的每一步方向

    private boolean check_tail_increasing(int step) {  // 检测当前回合蛇的长度是否增加
        if (step <= 7) return true;  // 前7回合每一回合长度都增加
        return step % 3 == 1;  // 之后每3回合增加一次长度
    }

    public List<Cell> getCells() {  // 返回蛇的身体,每次都根据蛇历史走的方向将其每一格找出来
        List<Cell> cells = new ArrayList<>();
        int[] dx = { -1, 0, 1, 0 }, dy = { 0, 1, 0, -1 };
        int x = sx, y = sy;
        int step = 0;
        cells.add(new Cell(x, y));
        for (int d: steps) {
            x += dx[d];
            y += dy[d];
            cells.add(new Cell(x, y));
            if (!check_tail_increasing(++step)) {
                cells.remove(0);  // 删掉蛇尾,即第一个起始的位置
            }
        }
        return cells;
    }
}

最后即可Game 类中实现 judge() 方法:

...

public class Game extends Thread {
    ...

    private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {  // 判断A是否合法
        int n = cellsA.size();
        Cell headCellA = cellsA.get(n - 1);  // A的头,也就是最后一个Cell
        if (g[headCellA.x][headCellA.y]) {
            return false;
        }
        for (int i = 0; i < n - 1; i++) {  // 判断除了头以外的其他身体部分
            if (cellsA.get(i).x == headCellA.x && cellsA.get(i).y == headCellA.y) {
                return false;
            }
            if (cellsB.get(i).x == headCellA.x && cellsB.get(i).y == headCellA.y) {
                return false;
            }
        }
        return true;
    }

    private void judge() {  // 判断两名玩家下一步操作是否合法
        List<Cell> cellsA = playerA.getCells();
        List<Cell> cellsB = playerB.getCells();

        boolean validA = check_valid(cellsA, cellsB);
        boolean validB = check_valid(cellsB, cellsA);
        if (!validA || !validB) {
            status = "finished";
            if (!validA && !validB) {
                loser = "all";
            } else if (!validA) {
                loser = "A";
            } else {
                loser = "B";
            }
        }
    }

    ...
}

3. 实现游戏结束界面

首先我们需要将输的玩家记录前端全局变量中,在 store/pk.js添加 loser 变量

export default {
  state: {
    ...
    loser: "none",  // none表示没人输,all表示平局,A/B表示A/B赢
  },
  getters: {},
  mutations: {
    ...
    updateLoser(state, loser) {
      state.loser = loser;
    },
  },
  actions: {},
  modules: {},
};

然后PKIndexView 组件的游戏结束处理语句块中添加更新 loser语句

store.commit("updateLoser", data.loser);

游戏结束后需要给用户用户展示谁赢谁输的界面,并提供一个重开按钮,在 components 目录下创建 ResultBoard.vue

<template>
  <div class="card text-bg-secondary text-center">
    <div class="card-header" style="font-size: 26px;">
      游戏结束
    </div>
    <div class="card-body" style="background-color: rgba(255, 255, 255, 0.4);">
      <div class="result_board_text" v-if="$store.state.pk.loser === 'all'">
        Draw
      </div>
      <div class="result_board_text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id.toString() === $store.state.user.id">
        Lose
      </div>
      <div class="result_board_text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id.toString() === $store.state.user.id">
        Lose
      </div>
      <div class="result_board_text" v-else>
        Win
      </div>
      <div class="result_board_btn">
          <button @click="returnHome" type="button" class="btn btn-info btn-lg">
            返回主页
          </button>
      </div>
    </div>
  </div>
</template>

<script>
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();

    const returnHome = () => {  // 需要复原一些全局变量
      store.commit("updateStatus", "matching");
      store.commit("updateLoser", "none");
      store.commit("updateOpponent", {
        username: "我的对手",
        photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
      });
    };

    return {
      returnHome,
    };
  },
};
</script>

<style scoped>
.card {
  width: 30vw;
  position: absolute;
  top: 25vh;
  left: 35vw;
}

.result_board_text {
    color: white;
    font-size: 50px;
    font-weight: bold;
    font-style: italic;
    padding: 5vh 0;
}

.result_board_btn {
    padding: 3vh 0;
}
</style>

4. 持久化游戏状态

4.1 创建数据库

最后我们还需要将游戏过程存到数据库中,方便用户之后回看游戏录像,在数据库中创建 record 表用来记录每局对战的信息:

创建该数据库表的 SQL 语句如下

CREATE TABLE `kob`.`record` (
    `id` int NOT NULL AUTO_INCREMENT,
    `a_id` int NULL,
    `a_sx` int NULL,
    `a_sy` int NULL,
    `b_id` int NULL,
    `b_sx` int NULL,
    `b_sy` int NULL,
    `a_steps` varchar(1000) NULL,
    `b_steps` varchar(1000) NULL,
    `map` varchar(1000) NULL,
    `loser` varchar(10) NULL,
    `createtime` datetime NULL,
    PRIMARY KEY (`id`)
);

pojo 包下创建 Record如下

package com.kob.backend.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Record {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private Integer aId;
    private Integer aSx;  // 注意别忘了驼峰命名
    private Integer aSy;
    private Integer bId;
    private Integer bSx;
    private Integer bSy;
    private String aSteps;
    private String bSteps;
    private String map;
    private String loser;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private Date createtime;
}

mapper 包下创建 RecordMapper如下

package com.kob.backend.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kob.backend.pojo.Record;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface RecordMapper extends BaseMapper<Record> {
}

4.2 保存游戏对局信息

可以在向前端发送游戏结果消息之前将对局信息存下来,首先需要在 WebSocketServer 中将 RecordMapper 创建出来:

...

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    ...

    public static RecordMapper recordMapper;  // 要在Game中调用

    @Autowired
    public void setRecordMapper(RecordMapper recordMapper) {
        WebSocketServer.recordMapper = recordMapper;
    }

    ...
}

然后Player 中创建辅助函数用来返回 steps字符串形式:

...

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
    ...

    private List<Integer> steps;  // 记录历史走过的每一步方向

    ...

    public String getStringSteps() {  // 将steps转换字符串
        StringBuilder res = new StringBuilder();
        for (int d: steps) {
            res.append(d);
        }
        return res.toString();
    }
}

最后可以Game 中将游戏记录保存数据库中:

...

public class Game extends Thread {
    ...

    private String getStringMap() {  // 将g转换成01字符串
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                res.append(g[i][j] ? 1 : 0);
            }
        }
        return res.toString();
    }

    private void saveRecord() {  // 将对局信息存到数据库中
        Record record = new Record(
                null,
                playerA.getId(),
                playerA.getSx(),
                playerA.getSy(),
                playerB.getId(),
                playerB.getSx(),
                playerB.getSy(),
                playerA.getStringSteps(),
                playerB.getStringSteps(),
                getStringMap(),
                loser,
                new Date()
        );
        WebSocketServer.recordMapper.insert(record);
    }

    private void sendResult() {  // 向两个Client公布结果
        JSONObject resp = new JSONObject();
        resp.put("event", "result");
        resp.put("loser", loser);
        saveRecord();  // 在发送结束消息给前端之前先将游戏记录存下来
        sendAllMessage(resp.toJSONString());
    }

    ...
}

原文地址:https://blog.csdn.net/m0_51755720/article/details/134632884

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_18111.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注