目录
除了地图同步以外、我们还需要同步两个玩家的位置
同步玩家的位置我们可以标记一下、至于谁在A谁在B我们需要在云端确定
确定完之后我们会把每一个玩家的位置传给前端,我们可以傻瓜式的确定a在左下角b在
右上角、我们在存地图的时候需要存一下玩家的id和位置
在game这个类里我们需要加一个player类来维护玩家的位置信息
一般开发思路需要用什么定义什么、先定义需要用到的各种函数
有参构造函数无参构造函数、存一下每个玩家每一次的指令是什么
现在有三个棋盘、还有一个在云端
有两个浏览器就是有两个client、状态同步的机制
client向云端发送消息表示这个蛇动了一下、当服务器接收到两个蛇的移动之后
服务器就会把两个蛇移动的信息分别返回给Client1client2
同步给两名玩家、这样我们就实现了三个棋盘的同步
首先我们构造map的时候传入两名玩家的userid、初始化一下我们的playerAplayerB
为了需要访问到我们的player、我们需要写两个函数
后端就可以把两个玩家的信息传过去、前端做出相应修改
Game不能作为单线程来处理、线程:一个人干就是单线程,两个人干就是多线程
涉及到两个线程之间的通信以及加锁的问题
我们需要先把game变成一个支持多线程的类
就变成多线程了、我们需要实现thread类的一个入口函数
alt+insert就可以实现、重载run函数
start函数就是thread函数的一个api、可以另起一个线程来执行这个函数
为了方便我们需要先把我们的game存放到这个类里面
我们的线程就要一步一步等待下一步操作的操作
这里设计到两个线程同时读写一个变量、这样就会有读写冲突、涉及到顺序问题
两名玩家都输入我们就进行下一步
如果超过一定时间之后两名玩家还没有输入的话
我们要结束这个操作、告诉我们哪个玩家没有输入
就输了、可以用sleep函数、如果是正在进行中的话
我们应该将这一步操作广播给两位玩家、需要同步一下
我们从服务器分别接收到两名玩家的输入之后、需要将两名玩家的输入分别广
播给两个人、比如说我们两个玩家,同时都向服务器发送了请求
c1不知道c2的操作s向c1c2广播操作
当我们移动的时候、之前我们是在gamemap里面判断的
两个线程同时操纵一个变量、至少有一个变量是写的话那就需要加锁子
前端写完之后后端需要接收到这个请求
gameobject需要存下来才能访问到蛇、每一个新的游戏都会new一个新的类
都会开一个新的线程
record表用来记录每局对战的信息
表中的列:
id: int
a_id: int
a_sx: int
a_sy: int
b_id: int
b_sx: int
b_sy: int
a_steps: varchar(1000)
b_steps: varchar(1000)
map: varchar(1000)
loser: varchar(10)
createtime: datetime
联机对战:同步玩家的操作
文件结构
backend
consumer
utils
Cell.java
Player.javasrc
components
ResultBoard.vue
1.后端:同步玩家位置信息
玩家类:需要联机对战肯定就有玩家,因此建立玩家类
- package com.kill9.backend.consumer.utils;
-
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- import java.util.List;
-
- @NoArgsConstructor
- @AllArgsConstructor
- @Data
- public class Player {
- private Integer id;
- private Integer sx;
- private Integer sy;
- //每一次的指令
- private List
steps; - }
游戏类
一场游戏包含两名玩家
- private final Player playerA,playerB;
-
-
- final int[][] g;
-
- public Game(
- Integer rows,
- Integer cols,
- Integer innerWallsCount,
- Integer idA,
- Integer idB)
- {
- this.rows = rows;
- this.cols = cols;
- this.innerWallsCount = innerWallsCount;
- this.g = new int[rows][cols];
- playerA = new Player(idA,rows-2,1,new ArrayList<>());
- playerB = new Player(idB,1,cols-2,new ArrayList<>());
-
- }
-
- public Player getPlayerA() {
- return playerA;
- }
-
- public Player getPlayerB() {
- return playerB;
- }
websocket操作类
在匹配完之后,初始化地图时,将玩家传给Game构造函数
- 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());
- respA.put("game",respGame);
- respB.put("game",respGame);
2.前端:接收两名玩家位置以及地图信息
pk类的存储
- import ModuleUser from './user'
- export default {
- state: {
- socket:null ,//ws连接
- opponent_username:"",
- opponent_photo:"",
- status:"matching",matching表示匹配界面,playing表示对战界面
- gamemap:null,
- a_id:0,
- a_sx:0,
- a_sy:0,
- b_id:0,
- b_sx:0,
- b_sy:0,
- },
- getters: {
-
- },
- mutations: {
- updateSocket(state,socket){
- state.socket = socket;
- },
- updateOpponent(state,opponent){
- state.opponent_username = opponent.username,
- state.opponent_photo = opponent.photo;
- },
- updateStatus(state,status){
- state.status = status;
- },
- updateGame(state,game){
- state.gamemap = 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;
- }
- },
- actions: {
-
-
- },
- modules: {
- user: ModuleUser,
- }
- }
pk界面
修改pk界面的接收地图相关参数
- export default {
- ...
-
- setup() {
- ...
-
- onMounted(() => {
- ...
-
- // 回调函数:接收到后端信息调用
- socket.onmessage = msg => {
- // 返回的信息格式由后端框架定义,django与spring定义的不一样
- const data = JSON.parse(msg.data);
- if(data.event === "start-matching") {
- ...
-
- setTimeout(() => {
- store.commit("updateStatus", "playing");
- }, 200);
-
- store.commit("updateGame", data.game);
- }
- }
-
- ...
- });
-
- ...
- }
- }



3.后端:一局游戏的逻辑
Webscoket操作类
将Game作为一局游戏的一个线程,使Game线程启动
- private Game game = null;
-
- ...
-
- private void startMatching() {
- System.out.println("start matching!");
- matchPool.add(this.user);
-
- while(matchPool.size() >= 2) {
- ...
-
- game.createMap();
- // 一局游戏一个线程,会执行game类的run方法
- game.start();
-
- users.get(a.getId()).game = game;
- users.get(b .getId()).game = game;
-
- ...
- }
- }
-
游戏类
一局游戏的操作,首先执行run方法。只有读写、写写有冲突,此处关于nextStep,我们会接收前端的nextStep输入 或 bots代码的输入,而且会频繁的读,因此需要加锁。
Game.java
- package com.kill9.backend.consumer.utils;
-
- import java.util.ArrayList;
- 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 innerWallsCount;
-
- private final static int[] dx = {-1,0,1,0},dy={0,1,0,-1};
-
- private final Player playerA,playerB;
- private Integer nextStepA = null;
- private Integer nextStepB = null;
- //定义锁
- private ReentrantLock lock = new ReentrantLock();
-
-
- final int[][] g;
-
- public Game(
- Integer rows,
- Integer cols,
- Integer innerWallsCount,
- Integer idA,
- Integer idB)
- {
- this.rows = rows;
- this.cols = cols;
- this.innerWallsCount = innerWallsCount;
- this.g = new int[rows][cols];
- playerA = new Player(idA,rows-2,1,new ArrayList<>());
- playerB = new Player(idB,1,cols-2,new ArrayList<>());
-
- }
- public int[][] getG(){
- return g;
- }
-
- public Player getPlayerA(){
- return playerA;
- }
- public Player getPlayerB(){
- return playerB;
- }
-
- public void setNextStepA(Integer nextStepA){
- //上锁
- lock.lock();
- try{
- this.nextStepA = nextStepA;
- }finally {
- lock.unlock();
- }
-
- }
- public void setNextStepB(Integer nextStepB){
- //上锁
- lock.lock();
- try{
- this.nextStepB = nextStepB;
- }finally {
- lock.unlock();
- }
- }
- //判断连通性 flood fill
- private boolean check_connectivity(int sx,int sy,int tx,int ty){
- if(sx==tx&&sy==ty) return true;
- g[sx][sy] = 1;
- for(int i = 0;i<4;i++){
- int x = sx+dx[i],y = sy+dy[i];
- if(x<this.rows && x>=0 && y<this.cols && y>=0 && g[x][y]==0){
- if(check_connectivity(x,y,tx,ty)){
- //恢复原来的数组
- g[sx][sy] = 0;
- return true;
- }
- }
- }
- //恢复现场
- g[sx][sy] = 0;
- return false;
- }
- //画地图
- private boolean draw(){
- //初始化
- for(int i = 0;i<this.rows;i++){
- for(int j = 0;j<this.cols;j++){
- g[i][j] = 0;
- }
- }
-
- //给四周加墙
- for(int r = 0;r<this.rows;r++){
- g[r][0] = g[r][this.cols-1] = 1;
- }
- for(int c = 0;c<this.cols;c++){
- g[0][c] = g[this.rows-1][c] = 1;
- }
- Random random = new Random();
- for(int i = 0;i<this.innerWallsCount/2;i++){
- for(int j = 0;j<1000;j++){
- int r = random.nextInt(this.rows);
- int c = random.nextInt(this.cols);
-
- //画过的不画
- if(g[r][c]==1||g[this.rows-1-r][this.cols-1-c]==1) continue;
- g[r][c] = g[this.rows-1-r][this.cols-1-c] = 1;
- break;
- }
- }
- return check_connectivity(this.rows-2,1,1,this.cols-2);
- }
-
- public void createMap(){
- for(int i = 0;i<1000;i++){
- if(draw()) break;
- }
- }
-
- private boolean nextStep(){
- //每秒五步操作,因此第一步操作是在200ms后判断是否接收到输入。并给地图初始化时间
- //如果两名玩家操作非常快 比如1s操作了50次,但是返回前端,每动完一格之后才会去判断下一步,
- //如果我们在动一格的期间获取了很多下一步的操作的话,我们就会把中间结果都覆盖掉,在前端渲染的时候就会遗漏一些步数
- //所以在进行下一步操作时先睡200ms ,因为200ms才能走一格,如果200ms内多输入了很多,它之只会留最后一步,就会 覆盖掉
- try {
- Thread.sleep(200);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- //等待两名玩家的下一步操作,上锁
- for(int i = 0;i<5;i++){
- //此循环循环了5000ms,也就是5s,前端是一秒移动5步,
- //后端接收玩家键盘输入是5s内玩家的一个输入,若在一方先输入,
- //一方还未输入,输入的一方多此操作,以最后一次为准。
- try {
- Thread.sleep(1000);
- //等待玩家输入
- lock.lock();//读玩家输入要上锁
- try{
- if(nextStepA != null && nextStepB != null){
- playerA.getSteps().add(nextStepA);
- playerB.getSteps().add(nextStepB);
- return true;
- }
- }finally {
- //由于报异常的话 可能不会解锁,所以要在finally解锁
- lock.unlock();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return false;
- }
- //入口函数
- @Override
- public void run() {
- super.run();
- }
- }
先搭好整体框架
- private void sendResult(){ //向两名玩家返回结果
-
- }
- private void judge(){//判断两名玩家是否合法操作
-
- }
- private void sendMove(){ //向两个client公布信息
-
- }
- //入口函数
- @Override
- public void run() {
- for(int i = 0;i<1000;i++){
- //先判断是否获取两条蛇的下一步操作
- if(nextStep()){ // 是否获取了两条蛇的下一步的操作
- judge();
- if(status.equals("playing")){
- //将每名玩家的操作广播给两名玩家,实现同步
- sendMove();
- }else{
- //向前端返回结果
- sendResult();
- break;
- }
- }else{
- //这里也要加锁 涉及到nextStep的读 防止边界情况 ,在超时的边界输入
- lock.lock();
- status = "finished";
- try{
- if(nextStepA == null && nextStepB == null){
- loser = "all";
- }else if(nextStepA == null){
- loser = "A";
- }else{
- loser = "B";
- }
- }finally {
- lock.unlock();
- }
- sendResult();
- break;// 表示整个游戏结束
- }
- }
- }
- }
- private void sendAllMessage(String message){
- WebSocketServer.users.get(playerA.getId()).sendMessage(message);
- WebSocketServer.users.get(playerB.getId()).sendMessage(message);
-
- }
- private void sendResult(){ //向两名玩家返回结果
- //向本局的两名玩家分别广播
- JSONObject resp = new JSONObject();
- resp.put("event","result");
- resp.put("loser",loser);
- sendAllMessage(resp.toJSONString());
- }
- private void judge(){//判断两名玩家是否合法操作
-
- }
- 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);
- //清空操作
- nextStepA = nextStepB = null;
- }finally {
- lock.unlock();
- }
-
- }
4.前端: 发送移动指令给后端
GameMap.js
- add_listening_events() {
- this.ctx.canvas.focus();
- 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;
- // else if(e.key === 'ArrowUp') snake1.set_direction(0);
- // else if(e.key === 'ArrowRight') snake1.set_direction(1);
- // else if(e.key === 'ArrowDown') snake1.set_direction(2);
- // else if(e.key === 'ArrowLeft') snake1.set_direction(3);
-
- // 若移动了,发送给后端
- if(d >= 0) {
- this.store.state.pk.socket.send(JSON.stringify({
- event: "move",
- direction: d,
- }));
- }
- });
- }
5.后端处理接收移动的事件
- private void move(int d){
-
- if(game.getPlayerA().getId().equals(user.getId())) {
- game.setNextStepA(d);
- }else if(game.getPlayerB().getId().equals(user.getId())){
- game.setNextStepB(d);
- }
-
- }
-
- //路由
- @OnMessage
- public void onMessage(String message, Session session) {
- // 从Client接收消息
- System.out.println("receive message!");
- //将字符串转换成json对象
- JSONObject data = JSONObject.parseObject(message);
- String event = data.getString("event");
- //防止空指针
- if("start-matching".equals(event)){
- startMatching();
- }else if("stop-matching".equals(event)){
- stopMatching();
- }else if("move".equals(event)){
- move(data.getInteger("direction"));
- }
- }
6前端接收后端发送过来的移动事件
设立GameMap的存储
- src/store/pk.js
-
- export default {
- state: {
- ...
-
- gameObject: null,
- },
- getters: {
- },
- mutations: {
- ...
-
- updateGameObject(state, gameobject) {
- state.gameObject = gameobject;
- }
- },
- actions: {
-
- },
- modules: {
- }
- }
GameMap存储
- src/components/GameMap.vue
-
- ...
-
- ...
-
- export default {
- setup() {
- ...
-
- onMounted(() => {
- store.commit(
- "updateGameObject",
- new GameMap(canvas.value.getContext('2d'), parent.value, store)
- );
- });
-
- ...
- }
- }
-
-
- ...
-
Pk页面:接收后端的移动事件
- src/views/pk/PkIndexView.vue
-
- ...
-
- ...
-
- export default {
- components: {
- PlayGround,
- MatchGround,
- },
- setup() {
- ...
-
- onMounted(() => {
- ...
-
- // 回调函数:接收到后端信息调用
- socket.onmessage = msg => {
- // 返回的信息格式由后端框架定义,django与spring定义的不一样
- const data = JSON.parse(msg.data);
- if(data.event === "start-matching") {
- ...
- } else if(data.event === "move") {
- console.log(data);
- const game = store.state.pk.gameObject;
- const [snake0, snake1] = game.snakes;
- snake0.set_direction(data.a_direction);
- snake1.set_direction(data.b_direction);
- } else if(data.event === "result") {
- console.log(data);
- }
- }
-
- ...
- });
- onUnmounted(() => {
- ...
- });
- }
- }
-
-
-
7.前端:根据后端返回结果将死了的蛇变白
Snake.js
删除前端判断蛇的操作是否有效
- export class Snake extends AcGameObject {
- ...
-
- // 将蛇状态变为走下一步
- next_step() {
- ...
-
- // 让蛇在下一回合长一个格子
- const k = this.cells.length;
- for(let i = k; i > 0; i--) {
- this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
- }
-
- // 删除前端判断蛇的操作是否有效
- }
-
- ...
- }
PkIndexView.vue
根据后端结果将失败的一方颜色变白
- // 回调函数:接收到后端信息调用
- socket.onmessage = msg => {
- // 返回的信息格式由后端框架定义,django与spring定义的不一样
- const data = JSON.parse(msg.data);
- if(data.event === "start-matching") {
- ...
- } else if(data.event === "move") {
- ...
- } else if(data.event === "result") {
- console.log(data);
- const game = store.state.pk.gameObject;
- const [snake0, snake1] = game.snakes;
-
- if(data.loser === "all" || data.loser === "A") {
- snake0.status = "die";
- }
- if(data.loser === "all" || data.loser === "B") {
- snake1.status = "die";
- }
- }
- }
8.后端:将前端的裁判程序移到后端
注意,此地方有两处代码有迷惑性,分别是:
1. 在Player.java的getCells函数里为什么删除的是第0个元素
2. 在Game.java的check_valid函数里为什么不判断最后一个元素是否重合
以上两个答案在下方代码的关键处有解释
Cell.java
- package com.kill9.backend.consumer.utils;
-
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Cell {
- private Integer x;
- private Integer y;
- }
Player.java
- package com.kill9.backend.consumer.utils;
-
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- import java.util.ArrayList;
- import java.util.List;
-
- @NoArgsConstructor
- @AllArgsConstructor
- @Data
- public class Player {
- private Integer id;
- private Integer sx;
- private Integer sy;
- //每一次的指令
- private List
steps; - private boolean check_tail_increasing(int step){//检验当前回合蛇的长度是否增加
- if(step<=10) return true;
- return step % 3 == 1;
- }
-
- public List
| getCells(){ | - List
| res = new ArrayList<>(); | - int[] dx={-1,0,1,0},dy = {0,1,0,-1};
- int x = sx,y = sy;
- int step = 0;
- /**
- * 每一步移动都会把蛇头移动到下一个格子(注:蛇头有两个cell,详看前端Snake.js的next_step()与update_move()逻辑),
- * 若当前长度增加,蛇头正好移到新的一个格子,剩下的蛇身长度不变,因此长度 + 1;若长度不增加,则删除蛇尾
- */
- res.add(new Cell(x,y));
- for(int d: steps){
- x += dx[d];
- y += dy[d];
- res.add(new Cell(x,y));
- if(!check_tail_increasing(++step)){
- /**
- * 关键:
- * 为什么此处删除0呢,首先存储蛇身、且判定是否增加、且画蛇的逻辑此时还是在前端,我们只是将
- * 判断蛇是否撞到 墙和蛇身 移到后端。并且我们在后端保存的是是蛇头的x、y坐标和蛇身相对
- * 于上一步操作的方向,但是在我们做了第一个操作后蛇尾才是蛇头,意思就是res逆序才是蛇
- * 头到蛇尾的位置!
- */
- res.remove(0);//删掉蛇尾
- }
- return res;
- }
- }
-
- }
Game.java
- public class Game extends Thread {
- ...
-
- private boolean check_valid(List
cellsA, List| cellsB) | | { - int n = cellsA.size();
- Cell cell = cellsA.get(n - 1);
- // 如果是墙,则非法
- if(g[cell.x][cell.y] == 1) return false;
-
- // 遍历A除最后一个Cell
- /**
- * 关键:
- * 首先我在Player中已经解释getCells的函数返回的res是蛇尾到蛇头的位置。
- * 因此以下两个for循环分别判断的是蛇头是否和两条蛇的蛇身重合!
- * 那么为什么不用判断两个蛇头是否重合呢?可能是地图大小为13 * 14,
- * 两个蛇头的位置初始为(1, 1)和(11, 12),两个蛇头的位置横纵之和分别为偶数
- * 和奇数,因此两个蛇头永远不会走到同一个格子!
- */
- for(int i = 0; i < n - 1; i++) {
- // 和蛇身是否重合
- if(cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) {
- return false;
- }
- }
-
- // 遍历B除最后一个Cell
- for(int i = 0; i < n - 1; i++) {
- // 和B蛇身是否重合
- if(cellsB.get(i).x == cell.x && cellsB.get(i).y == cell.y) {
- return false;
- }
- }
- return true;
- }
-
- private void judge() { // 判断两名玩家操作是否合法
- List
| cellsA = playerA.getCells(); | - List
| cellsB = playerB.getCells(); | -
- boolean validA = check_valid(cellsA, cellsB);
- boolean valibB = check_valid(cellsB, cellsA);
-
- if(!validA || !valibB) {
- status = "finished";
- if(!validA && !valibB) {
- loser = "all";
- } else if(!validA) {
- loser = "A";
- } else {
- loser = "B";
- }
- }
- }
-
- private void senAllMessage(String message) {
- ...
- }
-
- private void sendMove() { // 向两名玩家传递移动信息
- ...
- }
-
- private void sendResult() { // 向两名玩家发送游戏结果
- ...
- }
-
- @Override
- public void run() {
- ...
- }
- }
-
9.结果板的实现
pk信息存储类
- src/store/pk.js
-
- export default {
- state: {
- ...
- loser: "none", // all、A、B
- },
- getters: {
- },
- mutations: {
- ...
- updateLoser(state, loser) {
- state.loser = loser;
- }
- },
- actions: {
-
- },
- modules: {
- }
- }
ResultBoard.vue
- src/components/ResultBoard.vue
- "result-board">
- "result-board-text" v-if="$store.state.pk.loser == 'all'">
- Draw
-
- "result-board-text" v-else-if="$store.state.pk.loser == 'A' && $store.state.pk.a_id == $store.state.user.id">
- Lose
-
- "result-board-text" v-else-if="$store.state.pk.loser == 'B' && $store.state.pk.b_id == $store.state.user.id">
- Lose
-
- "result-board-text" v-else>
- Win
-
-
- "result-board-btn">
-
- 再来一局
-
-
-
-
-
- import {useStore} from 'vuex';
-
- export default{
-
- setup(){
- const store = useStore();
- const restart = () =>{
- store.commit("updateStatus","matching");
- store.commit("updateOpponent",{
- username:"我的对手",
- photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
- })
-
- store.commit("updateLoser","none");
- }
- return {
- restart,
- }
- }
- }
- div.result-board{
- height: 30vh;
- width: 30vw;
- background-color:rgba(191, 227, 241,0.5);
- position: absolute;
- top: 40vh;
- left: 35vw;
-
- }
- div.result-board-text{
- text-align: center;
- color: white;
- font-size: 50px;
- font-weight: 600;
- font-style: italic;
- padding-top: 5vh;
-
- }
- div.result-board-btn{
- text-align: center;
- padding-top: 7vh;
- }
Pk页面
- src/views/pk/PkIndexView.vue
-
- ...
-
-
if="$store.state.pk.loser != 'none'" /> -
-
- ...
-
- import ResultBoard from '../../components/ResultBoard.vue';
-
- ...
-
- export default {
- components: {
- ...
-
- ResultBoard,
- },
- setup() {
-
- ...
-
- onMounted(() => {
-
- ...
-
- // 回调函数:接收到后端信息调用
- socket.onmessage = msg => {
- // 返回的信息格式由后端框架定义,django与spring定义的不一样
- const data = JSON.parse(msg.data);
- if(data.event === "start-matching") {
- ...
- } else if(data.event === "move") {
- ...
- } else if(data.event === "result") {
- ...
-
- store.commit("updateLoser", data.loser);
- }
- }
-
- ...
- });
- ...
- }
- }
-
-
完善pk页面 对局中添加玩家头像,确认自己是哪条蛇
PlayGround.vue
- "playground">
- "user_b" v-if="$store.state.pk.b_id == $store.state.user.id">
-
"$store.state.user.photo" alt=""> -
{{$store.state.user.username}}
-
- "user_b" v-if="$store.state.pk.b_id != $store.state.user.id">
-
"$store.state.pk.opponent_photo" alt=""> -
{{$store.state.pk.opponent_username}}
-
-
- "user_a" v-if="$store.state.pk.a_id != $store.state.user.id">
-
"$store.state.pk.opponent_photo" alt=""> -
{{$store.state.pk.opponent_username}}
-
- "user_a" v-if="$store.state.pk.a_id == $store.state.user.id">
-
"$store.state.user.photo" alt=""> -
{{$store.state.user.username}}
-
-
-
- import GameMap from './GameMap';
-
- export default{
- components:{
- GameMap,
- }
- }
-
-
-
- div.playground{
- width:60vw;
- height: 70vh;
- margin: 70px auto;
- }
- .user_a > img{
- width: 10vh;
- border-radius: 40%;
- }
- .user_a{
- margin-left: 10%;
- }
- .user_b > img{
- width: 10vh;
- border-radius: 40%;
- }
- .user_b{
- margin-left: 80%;
- }
- p{
- margin-left:27px ;
- }

对局回放
通过保存对局每个状态的信息实现对局的回放功能
backend
pojo
Record.java
mapper
RecordMapper.java
创建数据库

Record.java
- package com.kill9.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
- @AllArgsConstructor
- @NoArgsConstructor
- public class Record {
- @TableId(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;
- }
RecordMapper.java
- package com.kill9.backend.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.kill9.backend.pojo.Record;
- import org.apache.ibatis.annotations.Mapper;
-
- @Mapper
- public interface RecordMapper extends BaseMapper
{ - }
WebSocketServer.java
注入RecordMapper,为了保存对局信息
- @Component
- // url链接:ws://127.0.0.1:3000/websocket/**
- @ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
- public class WebSocketServer {
- ...
-
- private static UserMapper userMapper;
- public static RecordMapper recordMapper;
-
- ...
-
- @Autowired
- public void setRecordMapper(RecordMapper recordMapper) {
- WebSocketServer.recordMapper = recordMapper;
- }
-
- ...
-
- }
Player.java
将玩家的蛇的方向偏移量转化成String
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Player {
-
- ...
-
- public String getStepsString() {
- StringBuilder res = new StringBuilder();
- for(int d : steps) {
- res.append(d);
- }
- return res.toString();
- }
- }
Game.java
将对局信息保存至数据库
- public class Game extends Thread {
-
- ...
-
- private String getMapString() {
- StringBuilder res = new StringBuilder();
- for(int i = 0; i < rows; i++) {
- for(int j = 0; j < cols; j++) {
- res.append(g[i][j]);
- }
- }
- return res.toString();
- }
-
- private void saveToDataBase() {
- Record record = new Record(
- null,
- playerA.getId(),
- playerA.getSx(),
- playerA.getSy(),
- playerB.getId(),
- playerB.getSx(),
- playerB.getSy(),
- playerA.getStepsString(),
- playerB.getStepsString(),
- getMapString(),
- loser,
- new Date()
- );
- WebSocketServer.recordMapper.insert(record);
- }
-
- private void sendResult() { // 向两名玩家发送游戏结果
- ...
-
- saveToDataBase();
- senAllMessage(resp.toJSONString());
- }
-
- ...
- }
-

完成~~