实现微服务:匹配系统(中)

news2024/11/22 21:59:15

目录

1、同步两个玩家的位置思路解析

2、实现了三个棋盘的同步原理

3、初始化一下我们的playerAplayerB

4、什么是线程为什么要用多线程?

5、如何去实现等待两名玩家输入

6、前端向后端发消息

7、在数据库中创建表record


1、同步两个玩家的位置思路解析

 除了地图同步以外、我们还需要同步两个玩家的位置
同步玩家的位置我们可以标记一下、至于谁在A谁在B我们需要在云端确定
确定完之后我们会把每一个玩家的位置传给前端,我们可以傻瓜式的确定a在左下角b在
右上角、我们在存地图的时候需要存一下玩家的id和位置
在game这个类里我们需要加一个player类来维护玩家的位置信息
一般开发思路需要用什么定义什么、先定义需要用到的各种函数
有参构造函数无参构造函数、存一下每个玩家每一次的指令是什么


2、实现了三个棋盘的同步原理

现在有三个棋盘、还有一个在云端
有两个浏览器就是有两个client、状态同步的机制
client向云端发送消息表示这个蛇动了一下、当服务器接收到两个蛇的移动之后
服务器就会把两个蛇移动的信息分别返回给Client1client2
同步给两名玩家、这样我们就实现了三个棋盘的同步 


3、初始化一下我们的playerAplayerB

首先我们构造map的时候传入两名玩家的userid、初始化一下我们的playerAplayerB
为了需要访问到我们的player、我们需要写两个函数
后端就可以把两个玩家的信息传过去、前端做出相应修改

 


4、什么是线程为什么要用多线程?

Game不能作为单线程来处理、线程:一个人干就是单线程,两个人干就是多线程
涉及到两个线程之间的通信以及加锁的问题
我们需要先把game变成一个支持多线程的类
就变成多线程了、我们需要实现thread类的一个入口函数
alt+insert就可以实现、重载run函数
start函数就是thread函数的一个api、可以另起一个线程来执行这个函数
为了方便我们需要先把我们的game存放到这个类里面
我们的线程就要一步一步等待下一步操作的操作
这里设计到两个线程同时读写一个变量、这样就会有读写冲突、涉及到顺序问题 


5、如何去实现等待两名玩家输入

两名玩家都输入我们就进行下一步
如果超过一定时间之后两名玩家还没有输入的话
我们要结束这个操作、告诉我们哪个玩家没有输入
就输了、可以用sleep函数、如果是正在进行中的话
我们应该将这一步操作广播给两位玩家、需要同步一下
我们从服务器分别接收到两名玩家的输入之后、需要将两名玩家的输入分别广
播给两个人、比如说我们两个玩家,同时都向服务器发送了请求
c1不知道c2的操作s向c1c2广播操作 


6、前端向后端发消息

当我们移动的时候、之前我们是在gamemap里面判断的
两个线程同时操纵一个变量、至少有一个变量是写的话那就需要加锁子
前端写完之后后端需要接收到这个请求
gameobject需要存下来才能访问到蛇、每一个新的游戏都会new一个新的类
都会开一个新的线程 


7、在数据库中创建表record

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

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

<template>
    ...
</template>

<script>
...

export default {
    setup() {
        ...

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

        ...
    }
}
</script>

<style scoped>

...

</style>

Pk页面:接收后端的移动事件

src/views/pk/PkIndexView.vue

<template>
    ...
</template>

<script>
...

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(() => {
            ...
        });
    }
}
</script>

<style scoped>

</style>

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<Integer> steps;
    private boolean check_tail_increasing(int step){//检验当前回合蛇的长度是否增加
        if(step<=10) return true;
        return step % 3 == 1;
    }

    public List<Cell> getCells(){
        List<Cell> 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<Cell> cellsA, List<Cell> 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<Cell> cellsA = playerA.getCells();
        List<Cell> 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
<template>
    <div class="result-board">
        <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 == $store.state.user.id">
            Lose
        </div>
        <div class="result-board-text" v-else-if="$store.state.pk.loser == 'B' && $store.state.pk.b_id == $store.state.user.id">
            Lose
        </div>
        <div class="result-board-text" v-else>
            Win
        </div>

        <div class="result-board-btn">
            <button @click="restart" type="button" class="btn btn-warning btn-lg">
                再来一局
            </button>
         </div>

    </div>

</template>
<script>
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,
        }
    }
}
</script>
<style scoped>
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;
}
</style>

Pk页面

src/views/pk/PkIndexView.vue

<template>
    ...

    <ResultBoard v-if="$store.state.pk.loser != 'none'" />
</template>

<script>

...

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

            ...
        });
        ...
    }
}
</script>

<style scoped>

</style>

完善pk页面 对局中添加玩家头像,确认自己是哪条蛇

PlayGround.vue

<template>
    <div class="playground">
        <div class="user_b" v-if="$store.state.pk.b_id == $store.state.user.id">
        <img :src="$store.state.user.photo" alt="">
        <p>{{$store.state.user.username}}</p>
        </div>
        <div class="user_b" v-if="$store.state.pk.b_id != $store.state.user.id">
        <img :src="$store.state.pk.opponent_photo" alt="">
        <p>{{$store.state.pk.opponent_username}}</p>
        </div>
        <GameMap/>
        <div class="user_a" v-if="$store.state.pk.a_id != $store.state.user.id">
        <img :src="$store.state.pk.opponent_photo" alt="">
        <p>{{$store.state.pk.opponent_username}}</p>
        </div>
        <div class="user_a" v-if="$store.state.pk.a_id == $store.state.user.id">
        <img :src="$store.state.user.photo" alt="">
        <p>{{$store.state.user.username}}</p>
        </div>
    </div>
</template>

<script>
import GameMap from './GameMap';

export default{
    components:{
        GameMap,
    }
}

</script>

<style scoped>

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

对局回放

通过保存对局每个状态的信息实现对局的回放功能

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

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

    ...
}

 完成~~

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

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

相关文章

koa框架(一) 认识koa

koa ​koa是express原班人马打造的轻量、健壮、富有表现力的nodejs框架。目前koa有koa1和koa2两个版本&#xff1b;koa2依赖Node.js 7.6.0或者更高版本&#xff1b;koa不在内核方法中绑定任何中间件&#xff0c;它仅仅是一个轻量级的函数库&#xff0c;几乎所有功能都必须通过…

[附源码]java毕业设计网上点餐系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

干货分享丨第五届“大数据安全与隐私计算”学术会议

开放隐私计算 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服务区”。 177篇原创内容 …

网页设计大作业模板-网页设计大作业(文房四宝 5页)-实训素材

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 茶文化网站 | 中华传统文化题材 | 京剧文化水墨风书画 | 中国民间年画文化艺术网站 | 等网站的设计与制作| HTML期末大学生网页设计作业&#xff0c;W…

通过STM32F103C8T6配置完成基于SPI协议的0.96OLED屏显

文章目录前言一、SPI协议1、应用2、组成二、OLED屏显和汉字点阵编码原理1、汉字点阵编码原理2、OLED屏显三、OLED模块四、显示自己的学号和姓名五、显示温湿度&滑动显示长字符六、总结七、参考资料前言 硬件&#xff1a;stm32f103c8t6 核心板软件&#xff1a;keil5 mdk软件…

混沌引力搜索算法(CGSA)解决三个机械工程设计问题(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

_linux 进程间通信(匿名管道)

文章目录1. 匿名管道2. 利用通过匿名管道实现进程间通信2.1 实现思路2.2 父子进程实现通信的简单代码2.3 结果展示如下3. 总结管道特点4. 扩展&#xff08;好玩的--简单内存池&#xff09;思路&#xff1a;代码&#xff1a;1. 匿名管道 查看手册(man)&#xff1a; 翻译 #incl…

[附源码]计算机毕业设计JAVA基于JSP技术的新电商助农平台

[附源码]计算机毕业设计JAVA基于JSP技术的新电商助农平台 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; …

Amine-PEG-Azide,NH2-PEG-N3,胺-PEG-叠氮PEG试剂供应

1、名称 英文&#xff1a;Amine-PEG-Azide&#xff0c;NH2-PEG-N3 中文&#xff1a;胺-聚二乙醇-叠氮 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;DSPE PEG Azide PEG 4、分子量&#xff1a;可定制&#xff0c;NH2-PEG-N3 2000、胺-聚二乙醇-叠氮 10000、NH2-PEG-…

从零开始做一款Unity3D游戏<一>——亲自上手使用Unity

游戏设计入门 游戏设计文档 Hero Born游戏的单页文档 构建关卡 创建基本图形 在三维中思考 材质 白盒环境 光照基础 创建光源 Light组件的属性 在Unity中制作动画 创建动画片段 记录关键帧 曲线与切线 粒子系统 总结 本文主要来自<<C#实践入门>>哈…

【单片机】独立看门狗IWDG初始化

目录 看门狗简介&#xff1a; 初始化例子&#xff1a; 看门狗使能&#xff1a; 写保护&#xff1a; 看门狗重载&#xff1a; 看门狗复位时间计算&#xff1a;基本上看stm中文手册对照框图就很明确 看门狗简介&#xff1a; 此器件具有两个嵌入式看门狗外设&#xff0c;具有…

3-4数据链路层-局域网

文章目录一.基本概念1.网络拓扑2.传输介质3.介质访问控制方法4.局域网的分类5.逻辑链路控制&#xff08;LLC&#xff09;子层与媒体接入控制&#xff08;MAC&#xff09;子层二.以太网&#xff08;一&#xff09;以太网的传输介质与网卡1.传输介质2.网卡&#xff08;二&#xf…

[附源码]计算机毕业设计JAVA基于JSP学生信息管理系统

[附源码]计算机毕业设计JAVA基于JSP学生信息管理系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM …

工具及方法 - 编辑二进制文件(使用VSCode和Notepad++的插件Hex Editor)

Visual Studio Code 在VSCode里安装插件&#xff0c;可以实现很多功能。 打开VSCode&#xff0c;在菜单里选择&#xff0c; View->Extensions&#xff0c;就会出现扩展插件的查找页面。 输入想要查找的插件名称&#xff0c;搜索&#xff0c;然后点击安装。 比如要用VSCode…

高性能零售IT系统的建设06-当应对大量HTTP请求时兼顾性能、处理速度的架构设计

前言 这个系列不像我的那些个“保姆式”教程&#xff0c;那些保姆式教程我一周最多可以写8篇&#xff0c;因为太简单了。充其量花的时间就是用“看漫画”的方式去组织我的截图和尽量少文字多Sample。 而高性能系统建设系列这一块不仅仅只有代码&#xff0c;相反它甚至不会多写…

力扣(LeetCode)25. K 个一组翻转链表(C++)

模拟 前置知识 : 反转链表、两两交换链表中的节点 。 LeetCode 有相应题目&#xff0c;可以先做。 设置哑结点 &#xff0c; 便于操作头结点。 翻转至少要 kkk 个结点 &#xff0c; 先检查剩余结点够不够 kkk 个。 不够 kkk 个就翻转完成了。 翻转分为组内翻转和首尾变向两步…

查找xml文件

一 前言 相比xml.dom.minidom&#xff0c;lxml.etree具有高效的查找方法&#xff0c;更方便&#xff0c;直接&#xff1b; 二、知识点&#xff1a;查找感兴趣的元素 举例一&#xff1a;递归遍历其下的所有子树&#xff08;包括子级&#xff0c;子级的子级&#xff0c;等等 …

nero platinum刻录光盘简要教程(文章末尾有教程链接)

nero platinum suit 介绍 使用nero express可以完成多数情况下的光盘应用。在nero platinum suit中单击nero express&#xff0c;这时可以看到nero express的工作界面&#xff0c;在这里面可以刻录数据光盘&#xff0c;可以刻录音乐光盘&#xff0c;可以刻录视频光盘&#xff…

SpringMVC集成静态资源

集成静态资源 静态资源&jsp是被谁处理的 tomcat是一个servlet容器&#xff0c;servlet容器中通常有servlet来处理各种请求&#xff0c;那么当访问静态资源或者jsp的时候&#xff0c;当然也是有servlet来处理这些请求的&#xff0c;但是并未在web.xml中配置哪个servlet来处…

SpringBoot+Vue项目小区疫苗接种管理系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 使用框架&#xff1a;spring boot 前端技术&#xff1a;JavaScript、Vue 、css3 开发工具&#xff1a;IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库&#xff1a;MySQL 5.7/8.0 数据库管理工具&#xff1a;phpstudy/Navicat JDK版…