项目实战——匹配系统(中)

目录

一、同步两个玩家

二、三个棋盘的同步

三、初始化玩家

四、多线程的使用

五、等待两名玩家输入

六、前后端通信

七、数据库创表record


一、同步两个玩家

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

 

由于要区分玩家,所以要在之前Game.java添加一个Player类存储玩家信息,

包括:
玩家Id,
玩家起始位置(sx,sy)
记录每个玩家走过的路径steps,即每个玩家历史上执行过的操作序列,用List存

consumer/utils/Player.java:

package com.popgame.backend.consumer.utils;


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

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor

public class Player {
    private Integer id;
    private Integer sx;
    private Integer sy;
    private List<Integer> steps;
}

在consumer/utils/Game.java里添加Player类,playerA表示左下角的玩家,playerB表示右上角的玩家,同时添加获取A,B player的函数,方便外部调用。

    private Player playerA, playerB;
    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.mark = new boolean[rows][cols];
        playerA = new Player(idA, this.rows - 2, 1, new ArrayList<>());
        playerB = new Player(idB, 1, this.cols - 2, new ArrayList<>());
    }

    public Player getPlayerA() {
        return playerA;
    }

    public Player getPlayerB() {
        return playerB;
    }

 注意在consumer/WebSocketServer.java里传参的时候也要修改:

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

 为了方便,我们可以把与游戏相关的信息封装成一个JSONObject
consumer/WebSocketServer.java:

  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.getMark());

            ...
            //直接传游戏信息给玩家A和玩家B
            respA.put("game", respGame);
            ...

            respB.put("game", respGame);

 修改前端
store/pk.js:


    state: {
        socket: null, //ws链接
        opponent_username: "",
        opponent_photo: "",
        status: "matching", //matching表示匹配界面,playing表示对战界面
        game_map: 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.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;
        },
    ...
    },

 在PKindex.vue里面直接把整个数据传进去就好了

二、三个棋盘的同步

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

 

三、初始化玩家

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

 

 

四、多线程的使用

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

consumer/utils/Game.java:

public class Game extends Thread{
    ...
    @Override
    public void run() {
        super.run();
    }
}

 在consumer/WebSocketServer.java 里面通过start()开始执行(是 Thread类的一个API)

  game.createMap();
            users.get(a.getId()).game = game; //需要在前面新建一个game属性
            users.get(b.getId()).game = game;
            game.start();

 将用户的操作nextStep存起来,方便外面的线程调用,
在Game线程里面会读取两个玩家的操作nextStepA/B的值,
在外面Client线程里面则会修改这两个变量的值,
这里涉及到了线程的读写同步问题!
需要加上进程同步锁
一般来说就是先上锁再读写,后解锁
try{} finally {lock.unlock();}可以保证报异常的情况下也可以解锁而不会产生死锁
简单总结一下就是:先上锁再操作,具体可以参考OS相关的内容o(╯□╰)o
所以以下涉及到nextStepA 和 nextStepB 的,不管是读还是写,只要出现了的话就要考虑到上锁和解锁方面的问题了

consumer/utils/Game.java:

 //两名玩家的下一步操作,0123表示上右下左(与前端一致)
    private Integer nextStepA = null;
    private Integer nextStepB = null;
    //进程同步锁
    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();
        }
    }
    ...
     private boolean nextStep() {
        //等待玩家的下一步操作

    }

 

五、等待两名玩家输入

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

后端接受前端两名玩家输入的操作后,才开始进行下一步操作。为了游戏的流畅性,提高玩家的游戏体验感,我们规定,如果超过一定的时间后,另一名玩家仍然未能给予操作,我们就判定这个玩家lose了。

可以用sleep函数来实现等待效果,定最长等待时间为5s。
这里可以按照自己的情况合理地规定等待时间,可以通过增加循环次数,减少sleep时间优化玩家操作手感,以牺牲服务器的计算量换取玩家的操作的流畅性。
tips:要在循环里面上锁,在外面上锁会死锁!
还需要注意的是,我们前端设置1s走5步,200ms走一步,所以为了操作顺利,不会因为操作太快而读入多个操作,我们每一次读取前都要先sleep 200ms,规范一下。

因为后面要在外面调用每名玩家操作对应的ws链接,且需要向前端传递信息,需要先将下面两段代码改成

public
consumer/WebSocketServer.java:

 final public static ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
 public void sendMessage(String message) {
        //异步通信要加上锁
        synchronized (this.session) {
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

 consumer/utils/Game.java:

 private boolean nextStep() {
        //等待玩家的下一步操作
        try {
            Thread.sleep(200); //前端1s走5步,200ms走一步,因此为了操作顺利,每一次读取都要先sleep 200ms
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
                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) {
        //向每一个人广播信息 后端->前端
        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);
            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()); //将JSON转化为字符串
    }

    @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 = "over";
                lock.lock();
                try {
                    if (nextStepA == null && nextStepB == null) {
                        loser = "all";
                    } else if (nextStepA == null) {
                        loser = "A";
                    } else {//nextStep() = false会有卡超时边界依然输入的情况,但是为了规则合理性,在这里全部判输了
                        loser = "B";
                    }
                } finally {
                    lock.unlock();
                }
                sendResult();
                break;
            }
        }
    }

六、前后端通信

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

修改前端
scripts/GameMap.js:

add_events() {
        this.ctx.canvas.focus();

        const [snake0, snake1] = this.snakes;
        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 >= 0) { //一个合法的操作
                //前端向后端发消息: 前端 -> 后端
                this.store.state.pk.socket.send(JSON.stringify({
                    event: "move",
                    direction: d,
                }));
            }

        });
    }

 同时在后端接受消息,并编写移动函数move()
consumer/WebSocketServer.java:

public void move(int direction) {
        if (game.getPlayerA().getId().equals(user.getId())) {
            //蛇A
            game.setNextStepA(direction);
        } else if (game.getPlayerB().getId().equals(user.getId())) { //蛇B
            game.setNextStepB(direction);
        }

    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 从Client接收消息
        System.out.println("receive message!");
        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)) {
            int d = data.getInteger("direction");
            move(d);
        }
    }

在前端编写move和result的逻辑函数,让小蛇动起来(✺ω✺)
同时,为了分别取出两条蛇可以将GameObject在store/pk.js里先存下来,记得写对应的update函数哦!

然后我们再在components/GameMap.vue里修改
components/GameMap.vue:

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

蛇的去世判断要从前端搬到后端判断

先在前端写好情况分支选择

views/pk/PKindex.vue:

  onMounted(() => { //当当前页面打开时调用

          ...

            socket.onmessage = msg => { //前端接收到信息时调用的函数
                ...
                } else if (data.event === "move") {
                    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") {
                    const game = store.state.pk.gameObject;
                    const [snake0,snake1] = game.snakes;

                    if (data.loser === "all" || data.loser === "A") {
                        snake0.status = "dead";
                    }
                    if (data.loser === "all" || data.loser === "B") {
                        snake1.status = "dead";
                    }
                }
            }

            ...

        });

在后端写judge逻辑
注意:要先添加一个Cell类存储蛇的全部身体部分,在Player类里面把蛇的身体都存储下来,
然后在Game类里判断的时候再循环一遍两个Player,各自取出自己的每一节cell逐个判断。
判断逻辑包括:撞墙、撞到自己、撞到他人,这些都会导致自己lose掉比赛

consumer/utils/Player.java:

...

   private boolean check_tail_increasing(int step) { //检测当前回合蛇的长度是否增加
        if (step <= 10) return true;
        else {
            return step % 3 == 1;
        }
    }


    public List<Cell> getCells() {
        List<Cell> res = new ArrayList<>(); //存放蛇的身体
        int[][] fx = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        int x = sx, y = sy;
        res.add(new Cell(x, y));
        int step = 0; //回合数
        for (int d : steps) {
            x += fx[d][0];
            y += fx[d][1];
            res.add(new Cell(x, y));
            if (!check_tail_increasing(++step)) {
                res.remove(0);
            }
        }
        return res;
    }
    ...

 consumer/views/Game.java:

...
  private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
        int n = cellsA.size();
        Cell cell = cellsA.get(n - 1); //取出最后一位(蛇头)
        if (mark[cell.x][cell.y]) { //如果最后一位是墙的话
            return false;
        }
        for (int i = 0; i < n - 1; i++) {
            if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) { //如果自己碰到自己就算输
                return false;
            }
        }

        for (int i = 0; i < n - 1; i++) {
            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 validB = check_valid(cellsB, cellsA);
        if (!validA || !validB) {
            status = "over";
            if (!validA && !validB) {
                loser = "all";
            } else if (!validA) {
                loser = "A";
            } else {
                loser = "B";
            }
        }
    }
...

 写个游戏结果画面
首先在views/pk/PKindex.vue里面添加游戏胜负显示逻辑

...
                else if (data.event === "result") {
                    const game = store.state.pk.gameObject;
                    const [snake0,snake1] = game.snakes;

                    if (data.loser === "all" || data.loser === "A") {
                        snake0.status = "dead";
                    }
                    if (data.loser === "all" || data.loser === "B") {
                        snake1.status = "dead";
                    }
                    store.commit("updateLoser",data.loser);
                }
...

在前端写一个组件components/ResultBoard.vue 这就是游戏结束后显示的结果版面,把谁是loser存在store里面就可以全局调用来判断了

components/ResultBoard.vue:

<template>
    <div class="result-board">
        <div class="result-board-text draw" v-if="$store.state.pk.loser == 'all'" >
            Draw
        </div>

         <div class="result-board-text lose" v-else-if="$store.state.pk.loser =='A' && $store.state.pk.a_id == $store.state.user.id" >
            Lose
        </div>

         <div class="result-board-text lose" v-else-if="$store.state.pk.loser =='B' && $store.state.pk.b_id == $store.state.user.id" >
            Lose
        </div>

         <div class="result-board-text win" v-else >
            WIN
        </div>

        <div class="result-board-btn">
            <button type="button" class="btn">Try again</button>
        </div>

    </div>
</template>

实现Try again逻辑
接下来我们把Try again按钮实现一下,玩家可以在游戏结束后点击这个按钮再来一局游戏。
实现逻辑也比较简单,每次点击按钮,把游戏页面展示状态status从playing 改成 matching即可,这样整个游戏页面就返回到匹配页面了。
不要忘记了要updateLoser改成none,即重新开始游戏前还没有loser
还有把对手头像updateOpponent成默认的灰头像。 

 

七、数据库创表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

然后像前面一样,建立相应的pojo,mapper层

准备工作完成后,我们就可以开始写将数据写入数据库的逻辑了
consumer/utils/Game.java

        StringBuilder res = new StringBuilder();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (mark[i][j]) res.append(1);
                else res.append(0);
            }
        }
        return res.toString();
    }

private void saveRecord() {
        Record record = new Record(
                null, //因为之前创建数据库时是把id定义为自动递增,所以这里不用手动传id
                playerA.getId(),
                playerA.getSx(),
                playerA.getSy(),
                playerB.getId(),
                playerB.getSx(),
                playerB.getSy(),
                playerA.getStepsString(),
                playerB.getStepsString(),
                getMapString(),
                loser,
                new Date()
        );

        WebSocketServer.recordMapper.insert(record); //ws里数据库的注入

    }

 

 老样子,git 维护:

kob: springBoot 究极项目 (gitee.com)icon-default.png?t=M85Bhttps://gitee.com/geng-chaoyang-11/kob

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值