微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Spring Boot 6.2 实现后端与两个客户端之间的同步(逻辑)&&多线程&&读写锁

第一步:传递Player的位置

Game.java前创建Player

在这里插入图片描述

consumer/utils/Player.java

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;//存方向0123
}

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

private final 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

export default {
  state: {
    status:"matching",//matching匹配页面 playing对战页面
    socket:null,
    opponent_username:"",
    opponent_photo:"",
    gamemap:null,
    //add
    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;
    },
    updateGamemap(state,game){
   		 //add
        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 ;
        state.gamemap = game.gamemap ;
    }
  },
  actions: {
  },
  modules: {
  }
}

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

store.commit("updateGame",data.game);

第二步:实现游戏同步(实现云端与两个客户端之间的同步)

实际上我们在游戏对战的时候存在三个棋盘,两个是对战双方客户端里存在的棋盘,一个是云端存在的棋盘,我们要求实现云端与两个客户端之间的同步。

实现方法

玩家每一次操作都会上传至云端服务器,当服务器接收到两个玩家的操作后,就会将两个玩家的蛇的移动信息同步给两个玩家。

流程

在这里插入图片描述

引入线程

为了优化游戏体验度,我们的Game不能作为单线程去处理,每一个Game要另起一个新线程来做。
Next Step开始的操作可以当成一个线程,获取用户操作可以当成另一个线程。

这里我们涉及到两个线程之间进行通信的问题,以及线程开锁解锁的问题。

每一局单独的游戏都会new 一个新的Game类,都是一个单独的线程。

将类改成多线程

继承一个 Thread类,并且ALT + INS重写run()方法
我们开始进行线程的执行的时候,线程的入口函数就是这个run()函数

consumer/utils/Game.java

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

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

game.createMap();
//a,b共同的地图==>将地图赋给a,b对应的连接
users.get(a.getId()).game = game; 
users.get(b.getId()).game = game;
game.start();

读写锁的问题

在这里插入图片描述


consumer/utils/Game.java

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

实现接受Client端输入的操作

在这里插入图片描述


consumer/WebSocketServer.java

public final static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();

consumer/utils/Game.java

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(){//两名玩家的下一步
   try {
       Thread.sleep(200);//因为前端走一格200ms
   } catch (InterruptedException e) {
       e.printstacktrace();
   }

   //超时5s判断
   for(int i = 1; i <= 5;i ++)
   {
       try {
           Thread.sleep(1000);
           lock.lock();
           try{
               if(this.nextStepA != null && this.nextStepB != null) {
                   //记录方向
                   playerA.getSteps().add(nextStepA);
                   playerB.getSteps().add(nextStepB);
                   return true;
               }
           } finally {
               lock.unlock();
           }
       } catch (InterruptedException e) {
           e.printstacktrace();
       }

   }
   return false ;
}

编写后端逻辑

consumer/utils/Game.java

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(){//两名玩家的下一步
    try {
        Thread.sleep(200);//因为前端走一格200ms
    } catch (InterruptedException e) {
        e.printstacktrace();
    }

    //超时5s判断
    for(int i = 1; i <= 5;i ++)
    {
        try {
            Thread.sleep(1000);
            lock.lock();
            try{
                if(this.nextStepA != null && this.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 sendResult(){//向两个client端公布结果
    JSONObject resp = new JSONObject();
    resp.put("event","result");
    resp.put("loser",loser);
    sendAllMessage(resp.toJSONString());
}

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());
        this.nextStepA = this.nextStepB = null;//清空下一步
    } finally {
        lock.unlock();
    }
}

public void sendAllMessage(String message){
    WebSocketServer.users.get(playerA.getId()).sendMessage(message);
    WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}

//线程的入口
@Override
public void run() {
   for(int i = 0;i <1000;i++){

       if(nextStep()){//获取了两条蛇的下一步
           judge();//是否合法
           if(this.status.equals("playing")){
               sendMove();
           } else if(this.status.equals("finished")){
               sendResult();
               break;//结束
           }
       } else {
           this.status = "finished" ;//完成:结束
           lock.lock();
           try {
               if (nextStepA == null && nextStepB == null) {//平局
                   this.loser = "all";
               } else if (nextStepA == null) {
                   this.loser = "A";
               } else {
                   this.loser = "B";
               }
           } finally {
               lock.unlock();
           }
           sendResult();//向前端发送对战结果
           break;
       }
   }
}

consumer/WebSocketServer.java:接受前端信息:方向键等

    @OnMessage
    public void onMessage(String message, Session session) {
        // 接收前端信息
        System.out.println("receive message!");
        JSONObject data = JSONObject.parSEObject(message);//解析message
        String event = data.getString("event");//类似map
        if("start-matching".equals(event)){
            startMatching();
        } else if("stop-matching".equals(event)){
            stopMatching();
        } else if("move".equals(event)){
            move(data.getInteger("direction"));
        }
    }

修改前端

scripts/GameMap.js

//获取键盘输入
add_listening_events(){
    this.ctx.canvas.focus();//cts[DOM] canvas[画板]
    
    //为画板绑定keydown事件
    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,
            }))
        }       
    })
}

在前端编写moveresult的逻辑函数,让蛇动起来
同时,为了分别取出两条蛇可以将GameObjectstore/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 = "die";
                }
                if (data.loser === "all" || data.loser === "B") {
                    snake1.status = "die"; 
                }
            }
        }

        ...

    });

在后端写judge逻辑

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

consumer/utils/Player.java

//检查这一步是否合法
//判断cellsA是否合法的,判断cellsB的合法直接调用处反着写即可
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 ;


    for(int i = 0; i < n-1 ;i++ ){
        if(cell.x == cellsA.get(i).x && cell.y == cellsA.get(i).y){//撞自己
            return false ;
        }
    }

    for(int i = 0; i < n-1; i++) {
        if (cell.x == cellsB.get(i).x && cell.y == cellsB.get(i).y) {//撞另一条蛇
            return false;
        }
    }

    return true;
}

//判断loser
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)//结束游戏
    {
        this.status = "finished" ;
        if(!validA&&!validB){
            this.loser = "all" ;
        } else if(!validA){
            this.loser = "A" ;
        } else if(!validB){
            this.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里面就可以全局调用来判断了

实现再来逻辑

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

<template>
  <div class="result-board">

    <!-- 为什么是==,而不是=== -->
    <!-- $store.state.pk.a_id(数字1)  $store.state.user.id(字符串1) -->
    <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 === parseInt($store.state.user.id)">
        Lose
    </div>
    <div class="result-board-text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id === parseInt($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("updateLoser","none");
            store.commit("updateOpponent",{
                username:"我的对手",
                photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
            });
        }

        return {
            restart,
        }
    },
}
</script>

<style scoped>
div.result-board {
    height: 30vh;
    width: 30vw;
    background-color: rgba(50, 50, 100, 0.5);
    position:absolute;
    top:30vh;
    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>

第三步:设计录像数据库(后期存储对战录像)

为了后期存储对战录像,我们需要先设计一个存储对象的数据库
数据库内容包括

id 自动递增、主键、唯一
a_id
a_sx
a_sy
b_id
b_sx
b_sy
a_steps
b_steps
map
loser
create_time

consumer/utils/Player.java

    public String getStepsstring() {
        StringBuilder res = new StringBuilder();
        for(int x:steps){
            res.append(x);
        }
        return res.toString();
    }

consumer/utils/Game.java

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

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 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里数据库的注入
    }

原文地址:https://www.jb51.cc/wenti/3286663.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐