第六章---匹配系统(上)

发布时间:2023年12月23日

1.流程分析

在这里插入图片描述
整个匹配的过程是异步过程,也就是在Matching system中执行匹配的过程,会执行一个未知的时间,当出现符合条件的匹配结果时,才会立即将结果返回给前端。这种流程很难用之前的Http来达到预期效果(http为请求一次返回一次,且一般立即响应)。对于匹配系统,请求一次,返回的时间位置,而且可能多次返回。

websocket协议,不仅客户端可以向服务器主动发送请求,服务器也可以主动向客户端发送请求,是一种对称的通信方式。

在这里插入图片描述
之前的地图生成方式,是在用户本地(浏览器中)随机生成,如果两名玩家都在本地实现地图,地图就会产生冲突。因此,需要将生成地图的整个过程,由服务器统一完成。此外,判断游戏是否失败的逻辑(蛇撞击),如果在用户本地(浏览器)中实现,就可能会导致用户作弊。所以,不仅是生成地图,而是整个游戏的过程(蛇的移动、判定),都要做服务器端统一完成,服务器端的相关参数、判定结果返回给前端,前端只用来渲染画面,不做任何判定逻辑。

在这里插入图片描述

1.1websocket原理

将前端建立的每个websocket连接在后端维护起来

添加consumer.WebSocketServer

package com.kob.backend.consumer;

import org.springframework.stereotype.Component;

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

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        // 建立连接
    }

    @OnClose
    public void onClose() {
        // 关闭链接
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 从Client接收消息
    }

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

在用户开始匹配的时候,每个client向后端发送一个请求,就会在后端开辟一个线程,创建并维护一个websocket连接(实际上就是new一个WebSocketServer类的实例)

WebSocketServer client1 = new WebSocketServer();
WebSocketServer client2 = new WebSocketServer();

后端接收到请求之后,将信息发送给匹配系统。

2.集成WebSocket

1)在pom.xml文件中添加依赖:

  • spring-boot-starter-websocket
  • fastjson

2)添加config.WebSocketConfig配置类:

package com.kob.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {

        return new ServerEndpointExporter();
    }
}

3)添加consumer.WebSocketServer

package com.kob.backend.consumer;

import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        //  建立连接时自动调用
    }

    @OnClose
    public void onClose() {
        // 关闭链接时自动调用
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // Server从Client接收消息时触发
    }

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

上面最核心的一个函数是onMessage,负责Server从Client接收消息时处理相关逻辑。那如何在通过后端,向前端client发送信息呢?

定义Session对象,每个连接本质上是通过Session维护

private Session session = null;

新增sendMessage函数,用于后端向当前连接发送信息

public void sendMessage(String message){
    // Server发送消息
    synchronized (this.session){
        try{
            this.session.getBasicRemote().sendText(message);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

另外还需要存储下每个connection对应的用户是谁,这样才能清楚哪两个用户之间发生了匹配,用户信息也要存储到Session中。并且需要根据用户的ID,找到相应的WebSocketServer连接是哪一个,所以将两者的映射关系存储在ConcurrentHashMap中,ConcurrentHashMap是一个线程安全的哈希表

private User user;
private static ConcurrentHashMap<Integer,WebSocketServer>
    userConnectionInfo = new ConcurrentHashMap<>();

由于WebSocket不属于Spring的一个组件,不是单例模式,因此,注入mapper的方式有些区别

private static UserMapper userMapper;

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

在建立连接时,需要建立用户ID与WebSocketServer实例的映射

@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
    // 建立连接时自动调用
    this.session = session;
    System.out.println("Connected!");
    int userId = Integer.parseInt(token);//假设token为userId
    this.user = userMapper.selectById(userId);
    userConnectionInfo.put(userId, this);
}

在关闭连接时,删除这种映射

@OnClose
public void onClose() {
    // 关闭链接时自动调用
    System.out.println("Disconnected!");
    if(this.user != null){
        userConnectionInfo.remove(this.user.getId());
    }
}

此时的WebSocketServer.java为:

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    private User user;
    private static ConcurrentHashMap<Integer,WebSocketServer>
            userConnectionInfo = new ConcurrentHashMap<>();
    private Session session = null;

    private static UserMapper userMapper;

    @Autowired
    public void setUserMapper(UserMapper userMapper){
        WebSocketServer.userMapper = userMapper;
    }
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        // 建立连接时自动调用
        this.session = session;
        System.out.println("Connected!");
        int userId = Integer.parseInt(token);//假设token为userId
        this.user = userMapper.selectById(userId);
        userConnectionInfo.put(userId, this);
    }

    @OnClose
    public void onClose() {
        // 关闭链接时自动调用
        System.out.println("Disconnected!");
        if(this.user != null){
            userConnectionInfo.remove(this.user.getId());
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // Server从Client接收消息时触发
        System.out.println("Receive message!");
    }

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

4)配置config.SecurityConfig,将/websocket/{token}一类的url链接全部放行

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/websocket/**");
}

3.接口调试

我们在views\pk\PkIndexView.vue中对WebSocket进行测试

期望在当前组件被加载成功之后,建立一个连接。

需要引入vue的两个与生命周期有关的函数

  • onMounted是当组件被挂载完成之后执行的函数
  • onUnmounted是当组件被卸载之后执行的函数

同时,需要将WebSocket存储到全局变量中,在store中开一个新的module用于存储所有和pk相关的全局变量

src\store\pk.js

export default ({
    state: {
       status:"matching",//matching表示匹配界面 playing表示对战界面
       socket:null,//存储前后端建立的connection
       opponent_username:"",//对手名
       opponent_photo:"",//对手头像
    },
    mutations: {
      
    },
    actions: {
       
    },
    modules: {
    }
  })

由于在成功创建连接之后,需要将连接信息,存储到全局变量中

所以需要在src\store\pk.js实现几个辅助函数

export default ({
    state: {
       status:"matching",//matching表示匹配界面 playing表示对战界面
       socket:null,//存储前后端建立的connection
       opponent_username:"",//对手名
       opponent_photo:"",//对手头像
    },
    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;
      }
    },
    actions: {
       
    },
    modules: {
    }
  })

然后在views\pk\PkIndexView.vue引入全局变量useStore

在当前组件被挂载的时候(可以简单理解为页面被打开的时候),也就是onMounted执行的时候,我们需要创建connection,在onUnmounted执行的时候,关闭连接。

export default {
    components:{
        PlayGround
    },
    setup() {
        const store = useStore();
        const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.id}`;
        let socket = null;
        onMounted(() => {
            socket = new WebSocket(socketUrl);
            socket.onopen = () => {//如果连接成功,将socket存储到全局变量中
                console.log("connected!");
                store.commit("updateSocket",socket);
            }
            socket.onmessage = msg =>{
                const data = JSON.parse(msg.data);
                console.log(data);
            }
            socket.onclose = () =>{
                console.log("disconnected!");
            }
        });

        onUnmounted(()=>{
            socket.close();
        })
    }
}
3.1建立连接

当进入到对战页面时,可以在后端和浏览器的控制台中看到连接成功的输出
在这里插入图片描述
在这里插入图片描述
此时如果切换到其他页面,又会断开连接

在这里插入图片描述
在这里插入图片描述
注意如果刷新页面,就会先断开连接,后建立连接

在这里插入图片描述

而且,必须要在页面卸载时,关闭连接

onUnmounted(()=>{
	socket.close();
})

否则,切换到其他页面的时候,没有关闭连接,但是在每一次进来的时候,又会创建连接
在这里插入图片描述
刷新或者关闭时,会关闭所有的连接,从输出看出不止一个
在这里插入图片描述
所以,如果不进行正常关闭,在切换到其他页面时,旧连接不会关闭,因此会产生很多冗余的连接。

在成功连接后,后端输出获取到的用户信息如下:
在这里插入图片描述
此时建立连接时,是直接将用户的ID传输过来,但这样显然是不安全的,因为前端可以通过修改{token}的方式,伪装成任意一个用户的身份建立连接,因此需要添加验证,这里仍然是使用Jwt进行验证

3.2 Jwt验证

前端直接将jwt-token传过去

const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}`;

后端验证的方式,在config.filter.JwtAuthenticationTokenFilter已经给出

那就是就是如果能从token中解析出userId就认为是合法的,否则就是不合法

String userid;
try {
    Claims claims = JwtUtil.parseJWT(token);
    //如果能解析出userid表示合法 否则不合法
    userid = claims.getSubject();
} catch (Exception e) {
    throw new RuntimeException(e);
}

为了日后方便,将这段代码提出,放在一个单独的工具类consumer.utils.JwtAuthentication

package com.kob.backend.consumer.utils;

import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
public class JwtAuthentication {
    public static Integer getUserId(String token){
        int userId = -1;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            //如果能解析出userid表示合法 否则不合法
            userId = Integer.parseInt(claims.getSubject());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return userId;
    }
}

此时WebSocketServer.java中onOpen函数体更新为

@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
    // 建立连接时自动调用
    this.session = session;
    System.out.println("Connected!");
    int userId = JwtAuthentication.getUserId(token);
    this.user = userMapper.selectById(userId);
    if(this.user != null)
        userConnectionInfo.put(userId, this);
    else
        this.session.close();
}

这样就能够成功实现jwt验证

4.前端实现

此时前端还只有对战界面,并没有匹配界面,我们需要实现匹配界面,以及匹配界面和对战界面的切换

与切换有关的全局变量,就是在pk.js中定义的statusmatching表示匹配界面,playing表示对战界面

那就需要当statusplaying的时候再显示对战页面

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

并且需要创建一个新的组件MatchGround.vue,用于表示匹配界面

<template>
    <div class="matchground">
        <div class="row">
            <div class="col-6">
                <div class="user_photo">
                    <img :src="$store.state.user.photo" alt="">
                </div>
                <div class="user_username">
                    {{ $store.state.user.username }}
                </div>
            </div>
            <div class="col-6">
                <div class="user_photo">
                    <img :src="$store.state.pk.opponent_photo" alt="">
                </div>
                <div class="user_username">
                    {{ $store.state.pk.opponent_username }}
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-12" style="text-align:center; padding-top:12vh">
                    <button @click="click_match_btn" class="btn btn-success btn-lg">{{match_btn_info}}</button>
            </div>
        </div>
    </div>
</template>

为按钮绑定一个click_match_btn触发函数,当点击"开始匹配",使用WebSocketsentAPI向后端发送包含event:"start-matching"的字符串(注意,JSON.stringify是将JSON格式处理为字符串,后续还可以恢复JSON格式)

<script>
import { ref } from 'vue'
import { useStore } from 'vuex';
export default {
    setup(){
        const store = useStore();
        let match_btn_info = ref("开始匹配");
        const click_match_btn = () =>{
            if(match_btn_info.value === "开始匹配"){
                match_btn_info.value = "取消";
                //JSON.stringify将JSON转换为字符串
                store.state.pk.socket.sent(JSON.stringify({
                    event:"start-matching",
                }));
            }else{
                match_btn_info.value = "开始匹配";
                store.state.pk.socket.sent(JSON.stringify({
                    event:"stop-matching",
                }));
            }
        };
        return{
            match_btn_info,
            click_match_btn,
        }
    }
}
</script>

后端收到请求时,就会将message字符串解析为JSON格式,然后根据event值来分配给不同的任务

private void startMatching(){
    System.out.println("start matching!");
}
private void stopMatching(){
    System.out.println("stop matching!");
}
@OnMessage
public void onMessage(String message, Session session) {//当做路由 分配任务
    // Server从Client接收消息时触发
    System.out.println("Receive message!");
    JSONObject data = JSONObject.parseObject(message);//将字符串解析成JSON
    String event = data.getString("event");
    if("start-matching".equals(event)){//防止event为空的异常
        startMatching();
    } else if ("stop-matching".equals(event)) {
        stopMatching();
    }
}

在后端需要建立一个线程安全的Set作为匹配池

private static CopyOnWriteArraySet<User> 
        matchpool = new CopyOnWriteArraySet<>();

然后在相应的时间添加和删除

@OnClose
public void onClose() {
    // 关闭链接时自动调用
    System.out.println("Disconnected!");
    if(this.user != null){
        userConnectionInfo.remove(this.user.getId());
        matchpool.remove(this.user);
    }
}

private void startMatching(){
    System.out.println("start matching!");
    matchpool.add(this.user);
}
private void stopMatching(){
    System.out.println("stop matching!");
    matchpool.remove(this.user);
}

由于现在还没有实现微服务,暂时先实现一个傻瓜式的匹配,也就是匹配池中大于等于两个用户的时候,就实现两两匹配,也就是两个用户user1user2,并通过两个用户自己的连接,告诉前端匹配成功的相关消息

private void startMatching(){
    System.out.println("start matching!");
    matchpool.add(this.user);
    while (matchpool.size() >= 2){
        Iterator<User> iterator = matchpool.iterator();
        User user1 = iterator.next();
        User user2 = iterator.next();
        matchpool.remove(user1);
        matchpool.remove(user2);
        //分别给user1和user2传送消息告诉他们匹配成功了
        //通过user1的连接向user1发消息
        JSONObject resp1 = new JSONObject();
        resp1.put("event","start-matching");
        resp1.put("opponent_username",user2.getUsername());
        resp1.put("opponent_photo",user2.getPhoto());
        WebSocketServer webSocketServer1 = userConnectionInfo.get(user1.getId());//获取user1的连接
        webSocketServer1.sendMessage(resp1.toJSONString());

        //通过user2的连接向user2发消息
        JSONObject resp2 = new JSONObject();
        resp2.put("event","start-matching");
        resp2.put("opponent_username",user1.getUsername());
        resp2.put("opponent_photo",user1.getPhoto());
        WebSocketServer webSocketServer2 = userConnectionInfo.get(user2.getId());
        webSocketServer2.sendMessage(resp2.toJSONString());
    }
}

在前端的PkIndexView.vue中,当接收到后端发送的消息之后,相关逻辑的实现在onmessage函数中

如果匹配成功,就要更新对手信息

4.1匹配测试

注意,需要两个用户进行测试的话,必须在两个不同的浏览器中。一个浏览器只能允许同时登录一个用户,因为在Local Storage中会共用一个jwt_token

在这里插入图片描述
前端如何匹配成功,就更新对手的用户名和头像。

import PlayGround from '../../components/PlayGround.vue'
import MatchGround from '../../components/MatchGround.vue'
import { onMounted } from 'vue'
import { onUnmounted } from 'vue'
import { useStore } from 'vuex'
export default {
    components:{
        PlayGround,
        MatchGround
    },
    setup() {
        const store = useStore();
        const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}`;
        let socket = null;
        onMounted(() => {

            store.commit("updateOpponent",{
                username:"我的对手",
                photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
            });
            socket = new WebSocket(socketUrl);
            socket.onopen = () => {//如果连接成功,将socket存储到全局变量中
                console.log("connected!");
                store.commit("updateSocket",socket);
            }
            socket.onmessage = msg =>{
                const data = JSON.parse(msg.data);
                console.log(data);
                if(data.event === "start-matching"){
                    store.commit("updateOpponent",{
                        username:data.opponent_username,
                        photo:data.opponent_photo
                    });
                    // store.commit("updateStatus","playing")
                }

            }
            socket.onclose = () =>{
                console.log("disconnected!");
            }
        });

        onUnmounted(()=>{
            socket.close();
        })
    }
}

在这里插入图片描述
然后在匹配成功之后,设置延迟两秒显示,然后跳转到对战页面

在这里插入图片描述
在这里插入图片描述
如果切换其他页面再切换回来的时候,地图又发生了变化,期望点击其他页面的时候自动放弃,再切换回来的时候,重新回到匹配页面。

那就需要在卸载页面(onUnmounted)的时候,不仅需要断开连接,同时还要将状态切换为matching状态。

但此时有一个很大的问题,就是两个人的游戏地图不一致。这是因为地图是在浏览器本地生成,为了解决同步问题,需要由服务器统一接管。

5.地图同步

接下来需要在服务器端实现之前分析的Game流程

在这里插入图片描述
添加consumer.utils.Game.java,用于管理整个游戏流程

参考assets\scripts\GameMap.js

画地图参考GameMap.jscreate_walls()函数

import java.util.Random;

public class Game {
    final private Integer rows;
    final private Integer cols;
    final private Integer inner_walls_count;
    final private int[][] g;
    //辅助数组
    final private static int[] dx = {-1,0,1,0};
    final private static int[] dy = {0,1,0,-1};
    public Game(Integer rows, Integer cols, Integer inner_walls_count) {
        this.rows = rows;
        this.cols = cols;
        this.inner_walls_count = inner_walls_count;
        this.g = new int[rows][cols];
    }

    public int[][] getG() {//返回地图
        return g;
    }

    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];
            int y = sy + dy[i];
            if(x >= 0 && x < this.rows && y >= 0 && y < this.cols && 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;//0表示可通行区域 1表示障碍物
            }
        }
        //给四周加上障碍物
        for(int r = 0; r < this.rows; r++){//给左右两侧设置为1
            g[r][0]=1;
            g[r][this.cols-1]=1;
        }

        for(int c = 0; c < this.cols; c++){//给上下两侧设置为1
            g[0][c] = g[this.rows-1][c] = 1;
        }

        //在内部随机生成inner_walls_count个对称的障碍物
        Random random = new Random();
        for(int i = 0; i < this.inner_walls_count / 2; i++){
            for (int j = 0; j < 1000; j++) {
                int r = random.nextInt(this.rows);//返回0~rows-1的随机值
                int c = random.nextInt(this.cols);//返回0~cols-1的随机值
                if(g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1)
                    continue;//已经有了 不能重复添加 直接进入下一轮循环 j++
                if(r == this.rows - 2 && c == 1 || r == 1 && c == this.cols-2)
                    continue;//保证左上角和右下角不能有障碍物

                //成功设置一个障碍物后 直接退出当前for i++
                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;
        }
    }
}

然后在WebSocketServer.java

当开始匹配的时候,实例化一个Game对象用于生成地图,并将生成的地图返回给连接中的两个用户。

当然,最终的地图应该是保存在webSocket中,也就是只对当前匹配的两个用户可见,对其他连接的用户不可见,这一点放在后面实现。

private void startMatching(){
    System.out.println("start matching!");
    matchpool.add(this.user);
    while (matchpool.size() >= 2){
        Iterator<User> iterator = matchpool.iterator();
        User user1 = iterator.next();
        User user2 = iterator.next();
        matchpool.remove(user1);
        matchpool.remove(user2);
        Game game = new Game(13,14,20);
        game.createMap();

        JSONObject resp1 = new JSONObject();
        resp1.put("event","start-matching");
        resp1.put("opponent_username",user2.getUsername());
        resp1.put("opponent_photo",user2.getPhoto());
        resp1.put("gamemap",game.getG());
        WebSocketServer webSocketServer1 = userConnectionInfo.get(user1.getId());
        webSocketServer1.sendMessage(resp1.toJSONString());

        JSONObject resp2 = new JSONObject();
        resp2.put("event","start-matching");
        resp2.put("opponent_username",user1.getUsername());
        resp2.put("opponent_photo",user1.getPhoto());
        resp2.put("gamemap",game.getG());
        WebSocketServer webSocketServer2 = userConnectionInfo.get(user2.getId());
        webSocketServer2.sendMessage(resp2.toJSONString());
    }
}

此时后端可以返回地图,前端写好接收地图的逻辑

src\store\pk.js

export default ({
    state: {
       status:"matching",//matching表示匹配界面 playing表示对战界面
       socket:null,//存储前后端建立的connection
       opponent_username:"",//对手名
       opponent_photo:"",//对手头像
       gamemap:null//地图
    },
    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;
      },
      updayeGamemap(state, gamemap){
        state.gamemap = gamemap;
      }
    },
    actions: {
       
    },
    modules: {
    }
  })

src\views\pk\PkIndexView.vue

socket.onmessage = msg => {
                const data = JSON.parse(msg.data);
                console.log(data);
                if (data.event === "start-matching") {
                    store.commit("updateOpponent", {
                        username: data.opponent_username,
                        photo: data.opponent_photo
                    });
                    //匹配成功后,延时2秒,进入对战页面
                    setTimeout(() => {
                        store.commit("updateStatus", "playing")
                    }, 2000);
                    store.commit("updateGamemap",data.gamemap)//更新地图
                }

            }

后端获取gamemap并更新到全局变量之后,要将获取到的gamemap渲染到画布上

首先在组件GameMap.vue中将全局变量store传递到GameMap的构造函数中

src\components\GameMap.vue

<script>
import { GameMap } from '../assets/scripts/GameMap'
import {onMounted, ref} from 'vue' //用于定义变量
import { useStore } from 'vuex';
export default {
    setup(){
        const store = useStore();
        let parent = ref(null);
        let canvas = ref(null);
        onMounted(()=>{
            new GameMap(canvas.value.getContext('2d'), parent.value, store)
        });
        return{
            parent,
            canvas
        }
    }
}
</script>

对于scripts\GameMap.js

相关的代码更新为:

export class GameMap extends GameObject {
    constructor(ctx, parent, store){
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.store = store;
        this.L = 0;
        this.rows = 13;
        this.cols = 14;
        this.inner_walls_count = 10;//定义内部障碍物数量
        this.walls = [];//用于保存障碍物,属于对象数组

        this.snakes = [
            new Snake({id:0, color:"#4876EC",r: this.rows - 2, c: 1},this),
            new Snake({id:1, color:"#F94848",r: 1, c: this.cols - 2},this),
        ];
    }
   
    //画地图:创建障碍物
    create_walls(){
        //直接将地图取出--后端传过来
        console.log(this.store)
        const g = this.store.state.pk.gamemap;
        //创建障碍物对象 并添加到this.walls数组
        for(let r = 0; r < this.rows; r++){
            for(let c = 0; c < this.cols; c++){
                if(g[r][c]){
                    this.walls.push(new Wall (r,c,this));
                }
            }
        }
    }
    start(){
        this.create_walls();//不用循环1000次了 因为直接接收的后端生成的
        this.add_listening_events();
    }

至此,就解决了地图同步问题

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_51366201/article/details/135173519
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。