• Springboot实现匹配系统(上)


    匹配系统的流程

    要实现匹配系统起码要有两个客户端client1,client2,当客户端打开对战页面并开始匹配时,会给后端服务器server发送一个请求,而匹配是一个异步的过程,什么时候返回结果是不可预知的,所以我们要写一个专门的匹配系统,维护一堆用户的集合,当用户发起匹配请求时,请求会先传给后端服务器,然后再传给匹配系统处理,匹配系统会不断地在用户里去筛选,将rating较为相近的的用户匹配到一组。当成功匹配后,匹配系统就会返回结果给springboot的后端服务器,继而返回给客户端即前端。然后我们就能在前端看到匹配到的对手是谁啦。

     

    举个例子,两个客户端请求两个链接,新建两个类:

    1. public class WebSocketServer {
    2. @OnOpen
    3. public void onOpen(Session session, @PathParam("token") String token) {
    4. // 建立连接
    5. WebSocketServer client1 = new WebSocketServer();
    6. WebSocketServer client2 = new WebSocketServer();
    7. }
    8. @OnClose
    9. public void onClose() {
    10. // 关闭链接
    11. }
    12. @OnMessage
    13. public void onMessage(String message, Session session) {
    14. // 从Client接收消息
    15. }
    16. @OnError
    17. public void onError(Session session, Throwable error) {
    18. error.printStackTrace();
    19. }
    20. }

    websocket协议

    因为匹配是异步的过程,且需要前后端双向交互,而普通的http协议是单向的,一问一答式的,属于立即返回结果的类型,不能满足我们的异步需求,因此我们需要一个新的协议websocket: 不仅客户端可以主动向服务器端发送请求,服务器端也可以主动向客户端发送请求,是双向双通的,且支持异步。简单来说就是客户端向后端发送请求,经过不确定的时间,会返回一次或多次结果给客户端。

    基本原理: 每一个ws连接都会在后端维护起来,客户端连接服务器的时候会创建一个WebSocketServer类。每创建一个链接就是new一个WebSocketServer类的实例,所有与链接相关的信息,都会存在这个类里面。

    比如我们的Clint1连接到我们的服务器、其实一个连接就是一个类

    其实就是一个websocketserver类,每来一个连接,其实就是new一个这个类的实例

    先创建这个类,我们每次来一个连接的时候本质上就是new一个这个类的实例

    每一个连接都是这个类的一个实列来维护的、所有和这个连接相关的信息都会存到这个类里面、如果是每一个连接自己独有的信息、比如说维护这个连接对应的用户是谁那可以存成私有变量、如果是维护所有连接的公共信息

    比如我们想去维护一下当前哪些用户建立的连接、那么可以存成一个静态变量

    WebSocket就是一个多线程、每来一个连接就会开一个新的线程来维护它这个websocket就是一个类

    每来一个连接就会开一个线程创建一个类,去维护这个连接流程

    用户开始匹配的时候向后端发送一个请求、就会在后端websocket里new一个新的类开一个线程来维护这个链接那么接收到这个请求之后、我们会把我们的信息发送给我们的匹配系统

    匹配系统是一个单独的额外的程序、匹配系统当接收到很多的用户之后随着时间的推移

    出现两名玩家的战斗力比较接近匹配出来一局、匹配系统就会将信息返回给我们的后端服务器

    也就是我们的websocket服务器、websocket服务器接受到这个信息之后就会将这个信息返回给这局对战的两名玩家、根据两名玩家建立的链接返还到他们的前端的浏览器里面

    同时在我们的服务器端创建一个游戏的过程、因为整个游戏的判断地图的生成都是在云端进行的

    前面逻辑的优化

    由于每次刷新都会刷新不同的地图,为了公平起见,应该把地图的生成放在服务器端,然后返回结果给前端。

    我们在云端维护游戏的整个流程

    先生成一个地图,将两个地图传给两个客户端,传完之后等待用户输入

    waiting、我们可以从代码端获取下一步操作也可以客户端返还代码端要用微服务了,waiting可以写一个死循环每次循环前先sleep一秒钟

    然后判断一下是否两条蛇的下一步操作都有了、如果有的话进行下一步如果没有的话继续等待、当然我们可以设定一个最大时间最大5s
    如果5s之内没有得到下一步操作的话、我们就判断没有输入操作的蛇输
    如果超时就判断输赢、如果获得输入写一个judging程序、判断是否合法和撞墙、这个游戏的逻辑

    为了防止作弊,游戏的一系列操作、判断逻辑都应该放在服务器端,前端只是呈现动画。

    我们可以从客户端获取输入,也可以通过微服务从代码端获得输入。

    简单的流程如下:

    Game -> Create map -> 返回给客户端 -> 客户端等待匹配waiting (sleep) -> 匹配成功则进行一系列游戏逻辑 

    准备配置

    1. 集成WebSocket 

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

    spring-boot-starter-websocket
    fastjson

    1. org.springframework.boot
    2. spring-boot-starter-websocket
    3. 2.7.4
    4. com.alibaba
    5. fastjson
    6. 2.0.11

    添加config/WebSocketConfig配置类

    1. package com.kill9.backend.config;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.context.annotation.Bean;
    5. import org.springframework.context.annotation.Configuration;
    6. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    7. @Configuration
    8. public class WebSocketConfig {
    9. @Bean
    10. public ServerEndpointExporter serverEndpointExporter() {
    11. return new ServerEndpointExporter();
    12. }
    13. }

     添加consumer/WebSocketServer类

    1. package com.kill9.backend.consumer;
    2. import org.springframework.stereotype.Component;
    3. import javax.websocket.*;
    4. import javax.websocket.server.PathParam;
    5. import javax.websocket.server.ServerEndpoint;
    6. @Component
    7. @ServerEndpoint("/websocket/{token}")
    8. public class WebSocketServer {// 注意不要以'/'结尾
    9. @OnOpen
    10. public void onOpen(Session session, @PathParam("token") String token) {
    11. // 建立连接
    12. }
    13. @OnClose
    14. public void onClose() {
    15. // 关闭链接
    16. }
    17. @OnMessage
    18. public void onMessage(String message, Session session) {
    19. // 从Client接收消息
    20. }
    21. @OnError
    22. public void onError(Session session, Throwable error) {
    23. error.printStackTrace();
    24. }
    25. }

    实现后端向前端发送信息,要手写个辅助函数sendMessage:

    首先要存储所有链接,因为我们要根据用户Id找到所对应的链接是什么,才可以通过这个链接向前端发请求(全局变量要用static)其次还要有链接与用户一一对应,每个链接都用一个session维护

    需要注意的是:WebSocketServer并不是一个标准的Springboot的组件,不是一个单例模式(每一个类同一时间只能有一个实例,这里每建一个链接都会new一个类,所以不是单例模式),向里面注入数据库并不像在Controller里一样直接@Autowired,要改成先定义一个static变量,再@Autowired加入到setUsersMapper函数上,如下:

    1. private static UsersMapper usersMapper;
    2. @Autowired
    3. public void setUsersMapper(UsersMapper usersMapper) {
    4. WebSocketServer.usersMapper = usersMapper; //静态变量访问要用类名访问
    5. }

    @Autowired写在set()方法上,在spring会根据方法的参数类型从ioc容器中找到该类型的Bean对象注入到方法的行参中,并且自动反射调用该方法(被@Autowired修饰的方法一定会执行),所以一般使用在set方法中、普通方法不用。

    consumer/WebSocketServer.java:

    1. package com.kill9.backend.consumer;
    2. import com.kill9.backend.mapper.UserMapper;
    3. import com.kill9.backend.pojo.User;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.stereotype.Component;
    6. import javax.websocket.*;
    7. import javax.websocket.server.PathParam;
    8. import javax.websocket.server.ServerEndpoint;
    9. import java.io.IOException;
    10. import java.util.concurrent.ConcurrentHashMap;
    11. @Component
    12. @ServerEndpoint("/websocket/{token}")
    13. public class WebSocketServer {// 注意不要以'/'结尾
    14. //与线程安全有关的哈希表,将userID映射到相应用户的WebSocketServer
    15. private static ConcurrentHashMap users = new ConcurrentHashMap<>();
    16. //当前链接请求的用户
    17. private User user;
    18. //后端向前端发信息,每个链接用session维护
    19. private Session session = null;
    20. private static UserMapper userMapper;
    21. @Autowired
    22. public void setUsersMapper(UserMapper userMapper) {
    23. WebSocketServer.userMapper = userMapper; //静态变量访问要用类名访问
    24. }
    25. @OnOpen
    26. public void onOpen(Session session, @PathParam("token") String token) {
    27. // 建立连接
    28. System.out.println("connected");
    29. this.session = session;
    30. //为了方便调试,初阶段只把token当成userId看
    31. Integer userId = Integer.parseInt(token);
    32. this.user = userMapper.selectById(userId);
    33. users.put(userId,this);
    34. }
    35. @OnClose
    36. public void onClose() {
    37. // 关闭链接
    38. System.out.println("disconnected!");
    39. //断开连接的话要将user移除
    40. if (this.user != null) {
    41. users.remove((this.user.getId()));
    42. }
    43. }
    44. @OnMessage
    45. public void onMessage(String message, Session session) {
    46. // 从Client接收消息
    47. System.out.println("receive message!");
    48. }
    49. //后端向前端发信息
    50. private void sendMessage(String message){
    51. //异步通信要加上锁
    52. synchronized (this.session){
    53. try {
    54. this.session.getBasicRemote().sendText(message);
    55. } catch (IOException e) {
    56. e.printStackTrace();
    57. }
    58. }
    59. }
    60. @OnError
    61. public void onError(Session session, Throwable error) {
    62. error.printStackTrace();
    63. }
    64. }

     放行websocket连接

    配置config/SecurityConfig

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

    前端

    onMounted: 当组件被挂载的时候执行的函数
    onUnmonted: 当组件被卸载的时候执行的函数
    初步调试阶段,我们是将token传进user.id的

    store/pk.js:

    1. import ModuleUser from './user'
    2. export default {
    3. state: {
    4. socket:null ,//ws连接
    5. opponent_username:"",
    6. opponent_photo:"",
    7. status:"matching",matching表示匹配界面,playing表示对战界面
    8. },
    9. getters: {
    10. },
    11. mutations: {
    12. updateSocket(state,socket){
    13. state.socket = socket;
    14. },
    15. updateOpponent(state,opponent){
    16. state.opponent_username = opponent.username,
    17. state.opponent_photo = opponent.opponent_photo;
    18. },
    19. updateStatus(state,status){
    20. state.status = status;
    21. }
    22. },
    23. actions: {
    24. },
    25. modules: {
    26. user: ModuleUser,
    27. }
    28. }

    将pk引入store中

    store/index.js

    1. import { createStore } from 'vuex'
    2. import ModuleUser from './user'
    3. import ModulePk from './pk'
    4. export default createStore({
    5. state: {
    6. },
    7. getters: {
    8. },
    9. mutations: {
    10. },
    11. actions: {
    12. },
    13. modules: {
    14. user:ModuleUser,
    15. pk:ModulePk,
    16. }
    17. })

    前端与后端建立连接

    views/pk/PKIndex.vue

    至此,前端与后端就可以通过ws互相连接了。

    将token改成jwt验证

    若使用userId建立ws连接,用户可伪装成任意用户,因此这是不安全的 

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

    添加ws的jwt验证,根据token判断用户是否存在

    consumer/utils/JwtAuthenciation.java

    1. package com.kill9.backend.consumer.utils;
    2. import com.kill9.backend.utils.JwtUtil;
    3. import io.jsonwebtoken.Claims;
    4. public class JwtAuthentication {
    5. public static Integer getUserId(String token){
    6. int userId = -1;
    7. try{
    8. Claims claims = JwtUtil.parseJWT(token);
    9. userId = Integer.parseInt(claims.getSubject());
    10. }catch (Exception e) {
    11. throw new RuntimeException(e);
    12. }
    13. return userId;
    14. }
    15. }

    修改后端
    consumer/WebSocketServer.java

    如果可以正常解析出jwt token的话表示登录成功,否则登录不成功,直接close

    1. @OnOpen
    2. public void onOpen(Session session, @PathParam("token") String token) throws IOException {
    3. // 建立连接
    4. System.out.println("connected");
    5. this.session = session;
    6. //为了方便调试,初阶段只把token当成userId看
    7. Integer userId = JwtAuthentication.getUserId(token);
    8. this.user = userMapper.selectById(userId);
    9. if(this.user!=null){
    10. // ID 对应一个连接
    11. users.put(userId,this);
    12. }else{
    13. this.session.close();
    14. }
    15. }

     实现前端逻辑

    匹配界面

    用grid系统布局自己头像:对手头像= 6 : 6
    逻辑很简单,只要点击匹配按钮,就向后端发送请求开始匹配.

    components/MatchGround.vue

    对战界面和匹配界面的切换
    views/pk/PKindex.vue

    解决token为空bug

    在user.js中

     这里调用updateUser会把原来已经更新的token覆盖为空

     

    前端向后端发送请求:store.state.pk.socket.send(字符串);
    以开始匹配为例:

    1. store.state.pk.socket.send(JSON.stringify({
    2. event: "start matching",
    3. }));

    后端可以在onMessage那里接收到前端的请求,并且解析传送过来的数据
    consumer/WebSocketServer.java 

    1. @OnMessage
    2. public void onMessage(String message, Session session) {
    3. // 从Client接收消息
    4. System.out.println("receive message!");
    5. //将字符串转换成json对象
    6. JSONObject data = JSONObject.parseObject(message);
    7. String event = data.getString("event");
    8. //防止空指针
    9. if("start matching".equals(event)){
    10. startMatching();
    11. }else if("stop matching".equals(event)){
    12. stopMatching();
    13. }
    14. }

    用线程安全的set定义匹配池:

    final private static CopyOnWriteArraySet matchPoll = new CopyOnWriteArraySet<>();

    开始匹配时,将用户放进拼配池里,取消匹配时将用户移除匹配池
    匹配过程在目前调试阶段可以简单地两两匹配

    1. private void startMatching(){
    2. System.out.println("start Matching");
    3. //自己加入匹配池
    4. matchPool.add(this.user);
    5. //当匹配池有两个人才能匹配
    6. while(matchPool.size() >= 2){
    7. Iterator it = matchPool.iterator();
    8. //从匹配池挑两个人出来
    9. User a = it.next(),b = it.next();
    10. //当两个匹配成功就从匹配池删除
    11. matchPool.remove(a);
    12. matchPool.remove(b);
    13. //给A返回数据
    14. JSONObject respA = new JSONObject();
    15. respA.put("event","start_matching");
    16. respA.put("opponent_username",b.getUsername());
    17. respA.put("opponent_photo",b.getPhoto());
    18. users.get(a.getId()).sendMessage(respA.toJSONString());
    19. JSONObject respB = new JSONObject();
    20. respA.put("event","start_matching");
    21. respA.put("opponent_username",a.getUsername());
    22. respA.put("opponent_photo",a.getPhoto());
    23. users.get(b.getId()).sendMessage(respB.toJSONString());
    24. }
    25. }
    26. private void stopMatching(){
    27. System.out.println("stop Matching");
    28. //把自己从匹配池删掉
    29. matchPool.remove(this.user);
    30. }

    后端返回信息给前端后,在前端接受并处理信息
    views/PKindex.vue 

    1. socket.onmessage = msg =>{ //前端接收到信息时调用的函数
    2. const data = JSON.parse(msg.data); //不同的框架数据定义的格式不一样
    3. if(data.event==="start_matching"){//这个这个start-matching是respA或respB返回的
    4. //匹配成功,更新对手信息
    5. //调用pk.js中 actions中的函数
    6. store.commit("updateOpponent",{
    7. username: data.opponent_username,
    8. photo: data.opponent_photo,
    9. });
    10. }
    11. }

    这样,我们就能初步实现匹配系统了。
    匹配成功2s后跳到pk页面,只需要updateStatus即可
    注意的是卸载组件的时候要记得把状态改为matching
    views/PK/PKindex.vue

    1. socket.onmessage = msg =>{ //前端接收到信息时调用的函数
    2. const data = JSON.parse(msg.data); //不同的框架数据定义的格式不一样
    3. if(data.event==="start_matching"){//这个这个start-matching是respA或respB返回的
    4. //匹配成功,更新对手信息
    5. //调用pk.js中 actions中的函数
    6. store.commit("updateOpponent",{
    7. username: data.opponent_username,
    8. photo: data.opponent_photo,
    9. });
    10. setTimeout(()=>{
    11. store.commit("updateStatus", "playing");//延时函数,单位是毫秒
    12. },2000);
    13. }
    14. }
    15. socket.onclose = () =>{
    16. console.log("disconnect");
    17. store.commit("updateStatus", "matching");
    18. }

    添加随机图片

    1. onMounted(()=>{
    2. store.commit("updateOpponent",{
    3. username:"Opponent",
    4. photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
    5. })

    解决同步问题

    前文也提到过,生成地图,游戏逻辑等与游戏相关的操作都应该放在服务端,不然的话客户每次刷新得到的地图都不一样,游戏的公平性也不能得到保证。因此,我们要将之前在前端写的游戏逻辑全部转移到后端(云端),前端只负责动画的演示即可。

    后端实现
    首先要在后端创建一个Game类实现游戏流程,其实就是把之前在前端写的js全部翻译成Java就好了
    consumer/utils/Game.java

    直接按照之前的gamemap.js搬过去就好了

    1. package com.kill9.backend.consumer.utils;
    2. import java.util.Random;
    3. //支持多线程
    4. public class Game{
    5. private final Integer rows;
    6. private final Integer cols;
    7. private final Integer innerWallsCount;
    8. private final static int[] dx = {-1,0,1,0},dy={0,1,0,-1};
    9. final int[][] g;
    10. public Game(
    11. Integer rows,
    12. Integer cols,
    13. Integer innerWallsCount)
    14. {
    15. this.rows = rows;
    16. this.cols = cols;
    17. this.innerWallsCount = innerWallsCount;
    18. this.g = new int[rows][cols];
    19. }
    20. public int[][] getG(){
    21. return g;
    22. }
    23. //判断连通性 flood fill
    24. private boolean check_connectivity(int sx,int sy,int tx,int ty){
    25. if(sx==tx&&sy==ty) return true;
    26. g[sx][sy] = 1;
    27. for(int i = 0;i<4;i++){
    28. int x = sx+dx[i],y = sy+dy[i];
    29. if(x<this.rows && x>=0 && y<this.cols && y>=0 && g[x][y]==0){
    30. if(check_connectivity(x,y,tx,ty)){
    31. //恢复原来的数组
    32. g[sx][sy] = 0;
    33. return true;
    34. }
    35. }
    36. }
    37. //恢复现场
    38. g[sx][sy] = 0;
    39. return false;
    40. }
    41. //画地图
    42. private boolean draw(){
    43. //初始化
    44. for(int i = 0;i<this.rows;i++){
    45. for(int j = 0;j<this.cols;j++){
    46. g[i][j] = 0;
    47. }
    48. }
    49. //给四周加墙
    50. for(int r = 0;r<this.rows;r++){
    51. g[r][0] = g[r][this.cols-1] = 1;
    52. }
    53. for(int c = 0;c<this.cols;c++){
    54. g[0][c] = g[this.rows-1][c] = 1;
    55. }
    56. Random random = new Random();
    57. for(int i = 0;i<this.innerWallsCount/2;i++){
    58. for(int j = 0;j<1000;j++){
    59. int r = random.nextInt(this.rows);
    60. int c = random.nextInt(this.cols);
    61. //画过的不画
    62. if(g[r][c]==1||g[this.rows-1-r][this.cols-1-c]==1) continue;
    63. g[r][c] = g[this.rows-1-r][this.cols-1-c] = 1;
    64. break;
    65. }
    66. }
    67. return check_connectivity(this.rows-2,1,1,this.cols-2);
    68. }
    69. public void createMap(){
    70. for(int i = 0;i<1000;i++){
    71. if(draw()) break;
    72. }
    73. }
    74. //将地图转成字符串
    75. private String getMapString(){
    76. StringBuilder res = new StringBuilder();
    77. for(int i = 0;i
    78. for(int j = 0;j
    79. res.append(g[i][j]);
    80. }
    81. }
    82. return res.toString();
    83. }
    84. }

     然后在websocketserver中 传给前端

    1. while(matchPool.size() >= 2){
    2. Iterator it = matchPool.iterator();
    3. //从匹配池挑两个人出来
    4. User a = it.next(),b = it.next();
    5. System.out.println(a);
    6. System.out.println(b);
    7. //当两个匹配成功就从匹配池删除
    8. matchPool.remove(a);
    9. matchPool.remove(b);
    10. //给A返回数据
    11. JSONObject respA = new JSONObject();
    12. respA.put("event","start-matching");
    13. respA.put("opponent_username",b.getUsername());
    14. respA.put("opponent_photo",b.getPhoto());
    15. respA.put("gamemap",game.getG());
    16. users.get(a.getId()).sendMessage(respA.toJSONString());
    17. JSONObject respB = new JSONObject();
    18. respB.put("event","start-matching");
    19. respB.put("opponent_username",a.getUsername());
    20. respB.put("opponent_photo",a.getPhoto());
    21. respB.put("gamemap",game.getG());
    22. users.get(b.getId()).sendMessage(respB.toJSONString());
    23. }

    在 前端 pk.js中

    1. import ModuleUser from './user'
    2. export default {
    3. state: {
    4. socket:null ,//ws连接
    5. opponent_username:"",
    6. opponent_photo:"",
    7. status:"matching",matching表示匹配界面,playing表示对战界面
    8. gamemap:null,
    9. },
    10. getters: {
    11. },
    12. mutations: {
    13. updateSocket(state,socket){
    14. state.socket = socket;
    15. },
    16. updateOpponent(state,opponent){
    17. state.opponent_username = opponent.username,
    18. state.opponent_photo = opponent.photo;
    19. },
    20. updateStatus(state,status){
    21. state.status = status;
    22. },
    23. updateGamemap(state,gamemap){
    24. state.gamemap = gamemap;
    25. }
    26. },
    27. actions: {
    28. },
    29. modules: {
    30. user: ModuleUser,
    31. }
    32. }

    在pkindexview中

    1. socket.onmessage = msg =>{ //前端接收到信息时调用的函数
    2. const data = JSON.parse(msg.data); //不同的框架数据定义的格式不一样
    3. console.log(data);
    4. if(data.event==="start-matching"){//这个这个start-matching是respA或respB返回的
    5. //匹配成功,更新对手信息
    6. //调用pk.js中 actions中的函数
    7. store.commit("updateOpponent",{
    8. username: data.opponent_username,
    9. photo: data.opponent_photo,
    10. });
    11. setTimeout(()=>{
    12. store.commit("updateStatus", "playing");//延时函数,单位是毫秒
    13. },2000);
    14. store.commit("updateGamemap",data.gamemap);
    15. }
    16. }

    在gamemap.vue中传一个store给gamemap.js

    1. import { AcGameObject } from "./AcGameObject";
    2. import { Wall } from "./Wall";
    3. import { Snake } from "./Snake";
    4. export class GameMap extends AcGameObject{
    5. constructor(ctx,parent,store){
    6. super();
    7. this.ctx = ctx;
    8. this.parent = parent;
    9. this.L = 0;
    10. this.rows = 13;
    11. this.cols = 14;
    12. this.store = store
    13. this.inner_walls_count = 20;
    14. this.wall = [];
    15. //创建两条蛇
    16. this.snakes = [
    17. //左下角
    18. new Snake({id:0,color:"#0c8998",r:this.rows-2,c:1},this),
    19. //右上角
    20. new Snake({id:1,color:"#faaf00",r:1,c:this.cols-2},this),
    21. ];
    22. }
    23. start(){
    24. this.add_listening_events();
    25. for(let i = 0;i<1000;i++){
    26. if(this.create_walls()){
    27. break;
    28. }
    29. }
    30. }
    31. add_listening_events(){
    32. this.ctx.canvas.focus();//聚焦
    33. const [snake0,snake1] = this.snakes;
    34. //添加键盘监听事件
    35. this.ctx.canvas.addEventListener("keydown",e=>{
    36. if(e.key === 'w') snake0.set_direction(0);//上
    37. else if(e.key === 'd') snake0.set_direction(1);//右
    38. else if(e.key === 's') snake0.set_direction(2);//下
    39. else if(e.key === 'a') snake0.set_direction(3);//左
    40. else if(e.key === 'ArrowUp') snake1.set_direction(0);
    41. else if(e.key === 'ArrowRight') snake1.set_direction(1);
    42. else if(e.key === 'ArrowDown') snake1.set_direction(2);
    43. else if(e.key === 'ArrowLeft') snake1.set_direction(3);
    44. });
    45. }
    46. // check_connectivity(g,sx,sy,tx,ty){
    47. // if(sx==tx&&sy==ty) return true;
    48. // g[sx][sy] = true;
    49. // let dx = [-1,0,1,0],dy=[0,1,0,-1];
    50. // for(let i = 0;i<4;i++){
    51. // let x = sx+dx[i],y = sy+dy[i];
    52. // if(!g[x][y] && this.check_connectivity(g,x,y,tx,ty))
    53. // return true;
    54. // }
    55. // return false;
    56. // }
    57. create_walls(){
    58. truefalse
    59. const g = this.store.state.pk.gamemap;
    60. for(let r = 0;r<this.rows;r++){
    61. for(let c = 0;c<this.cols;c++){
    62. if(g[r][c]){
    63. this.wall.push(new Wall(r,c,this));
    64. }
    65. }
    66. }
    67. }
    68. update_size(){
    69. //计算小正方形的边长
    70. this.L = Math.min(this.parent.clientWidth/this.cols,this.parent.clientHeight / this.rows);
    71. this.ctx.canvas.width = this.L * this.cols;
    72. this.ctx.canvas.height = this.L * this.rows;
    73. }
    74. next_step(){ //让两条蛇都进入下一个回合
    75. for(const snake of this.snakes){
    76. snake.next_step();
    77. }
    78. }
    79. check_valid(cell){
    80. //检测目标位置是否合法,没有撞到两条蛇的身体和墙
    81. for(const w of this.wall){
    82. if(w.r === cell.r && w.c === cell.c) return false;
    83. }
    84. for(const snake of this.snakes){
    85. let k = snake.cells.length;
    86. if(!snake.check_tail_increasing())
    87. //当蛇尾会前进的时候,蛇尾不要判断
    88. k--;
    89. for(let i = 0 ;i
    90. if(snake.cells[i].r === cell.r && snake.cells[i].c === cell.c)
    91. return false;
    92. }
    93. }
    94. return true;
    95. }
    96. check_ready(){
    97. //判断两条蛇是否准备下一回合了
    98. for(const snake of this.snakes){
    99. //非静止
    100. if(snake.status !== "idle") return false;
    101. //无指令
    102. if(snake.direction === -1) return false;
    103. }
    104. return true;
    105. }
    106. update(){
    107. this.update_size();
    108. if(this.check_ready()){
    109. this.next_step();
    110. }
    111. this.render();
    112. }
    113. render() {
    114. const color_eve = "#f2d2be",color_odd = "#fbb5b6";
    115. for(let r = 0;r<this.rows;r++){
    116. for(let c = 0;c<this.cols;c++){
    117. if((r+c)%2==0){
    118. this.ctx.fillStyle = color_eve;
    119. }else{
    120. this.ctx.fillStyle = color_odd;
    121. }
    122. this.ctx.fillRect(c*this.L,r*this.L,this.ctx.canvas.width,this.ctx.canvas.height);
    123. }
    124. }
    125. }
    126. }

    ok~~

  • 相关阅读:
    Spring学习笔记12 面向切面编程AOP
    VGG网络
    数据挖掘经典十大算法_NaiveBayes朴素贝叶斯
    python基础知识(二):变量和常用数据类型
    HDCP@SKE交互
    2023年深圳市专精特新企业申报详解与答疑汇总!
    腾讯云4核8G服务器CVM S5性能测评(CPU/流量/系统盘)
    基于统计学库statsmodel实现时间序列预测
    使用TensorRT 和 Triton 在Jetson NX上的模型部署
    Android 集成Flutter模块踩坑之路
  • 原文地址:https://blog.csdn.net/weixin_49884716/article/details/127874700