• p5.js 写个连连看


    预览点这里 ,代码在相应的 github 仓库中

    (首次加载比较慢,以后可以看看怎么优化一下打包)

    (一)准备工作

    • vue3 + vite + p5 + eslint

       // .eslintrc.cjs
       module.exports = {
        env: {
          browser: true,
          es2021: true,
        },
        extends: ["eslint:recommended", "plugin:vue/vue3-essential"],
        parserOptions: {
          ecmaVersion: "latest",
          sourceType: "module",
        },
        plugins: ["vue"],
        rules: {},
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • // 数据结构
      cells = [{ 
        x, 
        y, 
        avaliable, // 格子是否可用:初始化游戏时,用来判断此格子能不能放图片
        imgId
      }];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 图片是在 icones 下载的 svg,方便且图片体积小

    • 使用 p5 的 library
      click 时需要判断是否点到了某个格子。之前写五子棋时就有同样的需求,那时自己写了个方法实现的。这次想锻炼一下检索能力,就查了查有没有现成的包,果然找到了—— p5.collide2D
      但是说明里没讲怎样引用。在项目里直接 import 会报错:Uncaught ReferenceError: p5 is not defined
      只能去看 p5.collide2d.js 的源码了:

      p5.prototype._collideDebug = false;
      p5.prototype.collideRectRect = function (x, y, w, h, x2, y2, w2, h2) {...};
      p5.prototype.collideRectRectVector = function (p1, sz, p2, sz2) {...};
      
      • 1
      • 2
      • 3

      这样的写法是不支持模块化引用的,只能自己改一改了。
      把代码下载到本地,然后在文件开头加上 import p5 from "p5";

    (二) 画图

    • 预加载图片
      new p5(function (p) {
        // 在 p5.preload 中加载图片
        p.preload = function () {
          imageNames.forEach((name) => {
            const url = new URL(`./assets/svg/${name}.svg`, import.meta.url).href; // vite 中使用 `new URL` 获取图片地址
            imageMaps[name] = p.loadImage(url); // p5.loadImage 可以直接加载 svg 格式
          });
        };
        p.setup = function () {};
        p.draw = function () {};
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 初始化游戏
        const cells = []
        function newGame() {
          cells.length = 0;
          initCells();
          placeImges();
        }
      
        function initCells() {
          for (let i = 0; i < 8; i++) {
            for (let j = 0; j < 14; j++) {
              cells.push({ x: i, y: j, available: true , imgId: undefined });
            }
          }
        }
      
        function placeImges() {
          const cellsToPlace = cells.filter((cell) => cell.available); // avaliable 为 false 的格子,不放图片
          while (cellsToPlace.length > 0) {
            const [cell1] = cellsToPlace.splice(0, 1);
            const randomIndex = p5.prototype.random(0, cellsToPlace.length);
            const [cell2] = cellsToPlace.splice(randomIndex, 1);
            const image = p5.prototype.random(imageNames);
            cell1.imgId = image;
            cell2.imgId = image;
          }
        }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26

    (三)关键算法:判断相连

    判断两个格子是否能相连。如果能相连,还要返回相连路径
    在这里插入图片描述

    3.1 傻瓜算法

    先写了个傻瓜算法,粗暴的遍历查找

    • y 从 min 到 max( cell3和 cell4 沿 y 轴向下移),判断 cell1 -> cell3 -> cell4 -> cell2 通不通
    • x 从 min 到 max( cell3和 cell4 沿 x 轴向右移),判断 cell1 -> cell3 -> cell4 -> cell2通不通
      在这里插入图片描述在这里插入图片描述

    这样就简化为判断三条直线通不通

    但有很多不必要的计算,找的也不是最短路径

    3.2 优化算法

    小伙伴提供了一个有趣的思路,步骤如下:

    1. check if 不拐弯
    2. check if 拐一个弯
    3. check if 转两个弯,从上走
    4. check if 转两个弯,从外走,找最短的

    具体解释如下:

    passableCells

    passableCells:cell 沿着上下左右四个方向延伸,直到遇到障碍物为止

    如下图:cell1 的 passableCells 是黄色格子,cell2 的 passableCells 是蓝色格子;

    在这里插入图片描述在这里插入图片描述

    更近一步,passableCells 还可以分为圈上的圈外的
    如下图: 指的是 cell1 和 cell2 围成的矩形,有三角标记的格子就是“圈上的”
    在这里插入图片描述在这里插入图片描述
    对于圈外的 passableCells,要知道它的 方向到圈的距离
    如下图,三角标记的格子到 的距离都是1
    在这里插入图片描述

    step1: check if 不拐弯
        // check if 不拐弯
        const isDirectConnecting = checkIsDirectConnectable(cell1, cell2);
        if (isDirectConnecting) {
          return [cell1, cell2];	// 联通路径
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    step2:check if 拐一个弯

    等价于判断 passableCells_cell1_innerRect(cell1的延伸格子,内圈上的) 和 passableCells_cell2_innerRect 是否有交集

    如下图:无交集,说明不可能只拐一个弯就连上
    在这里插入图片描述
    有交集(星星格子),说明能连上。联通路径是 celll1->交集点->cell2
    在这里插入图片描述

        // check if 拐一个弯
        const [intersectionCell] = getIntersection(
          passableCells_cell1_innerRect ,
          passableCells_cell2_innerRect
        );
        if (intersectionCell) {
          return [cell1, intersectionCell, cell2];	// 联通路径
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    step3:check if 转两个弯 在圈上走

    等价于判断:passableCells_cell1_innerRect 中的格子 和 passableCells_cell2_innerRect 的格子,是否有能直接连接的

    如下图:找到能直接连接的了:
    在这里插入图片描述
    没有能直接连接的:
    在这里插入图片描述

        // check if 转两个弯 从圈上走
        for (let i = 0; i <= passableCells_cell1_innerRect.length - 1; i++) {
          const cell3 = passableCells_cell1_innerRect[i];
          for (let j = 0; j <= passableCells_cell2_innerRect.length - 1; j++) {
            const cell4 = passableCells_cell2_innerRect[j];
            if (checkIsDirectConnectable(cell3, cell4)) {
              return [cell1, cell3, cell4, cell2];	// 联通路径
            }
          }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    step4:check if check if 转两个弯 从 外走,找最短路径

    等价于判断:passableCells_cell1_outerRect中的格子 和 passableCells_cell2_outerRect 的格子,是否有能直接连接的
    在这里插入图片描述

        // check if check if 转两个弯 从圈外走,找最短路径
        // 找最短路径
        let distance = 1;
        let leftCount = passableCells_cell1_outerRect.length;
        while (leftCount > 0) {
          const cell5s = passableCells_cell1_outerRect.filter(
            (o) => o.toInnerRectDistance === distance
          );
          for (let i = 0; i <= cell5s.length - 1; i++) {
            const cell5 = cell5s[i];
            const cell6s = passableCells_cell2_outerRect.filter((o) => o.dir === cell5.dir); // 能连上的肯定是“方向”相同的
            for (let j = 0; j <= cell6s.length - 1; j++) {
              const cell6 = cell6s[j];
              if (checkIsDirectConnectable(cell5, cell6)) {
                return [cell1, cell5, cell6, cell2];
              }
            }
            leftCount--;
          }
          distance++;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    再看一遍路径长度

    step1 不拐弯 肯定是最近的
    在这里插入图片描述
    step2 拐一个弯step3 转两个弯,在圈上走 ,路径长度是一样的,都是 x坐标差 + y坐标差
    在这里插入图片描述在这里插入图片描述
    step4 转两个弯 从圈外走,路径长度是 x坐标差 + y坐标差 + 2 * toInnerRectDistance
    在这里插入图片描述

    (四)完善功能

    图片自动引入

      const imageNames = [];
    
      const svgs = import.meta.glob("../assets/svg/*.svg");
      for (const path in svgs) {
        const matchRes = path.match(/\/svg\/(\w+)\.svg/);
        const imageName = matchRes[1];
        imageNames.push(imageName);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    提供多种初始牌型

    // 修改 initCells 方法
    function initCells() {
       const strategy = randomPick([
          function(i,j){ ...},
          function(i,j){ ...},
        ]);
        for (let i = 0; i < 8; i++) {
          for (let j = 0; j < 14; j++) {
            cells.push({
              x: i,
              y: j,
              // avaliable: true,
              available: strategy(i, j), 
              imgId: undefined,
            });
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

    消除时显示路径

    点击 cell1 和 cell2 后,若能相连,则消除:

    在这里插入图片描述

    添加显示相连的路径:

    在这里插入图片描述

    上图中,路径出现时,两端的图片已经消失了,感觉怪怪的。

    所以要让图片多显示一会儿:(要保证图片还显示,但不能“堵路”。不然会影响游玩手感)

    在这里插入图片描述

      p.touchEnded = function () {
        ...
        if (connectingLine) {
          // 消除的时候,添加 tempDisplayConnectingLines 和 tempDisplayEliminatedCells
          addTempDisplayItem({ type: "connectingLine", value: connectingLine, time: 800 });
          addTempDisplayItem({
            type: "eliminatedCells",
            value: [cloneDeep(activeCell), cloneDeep(targetCell)],
            time: 800,
          });
          activeCell.active = false;
          // imgId 必须立刻清空,不能“堵路”
          activeCell.imgId = undefined;
          targetCell.imgId = undefined;      
        }
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
      const tempDisplay = ref([]);
      const tempDisplayConnectingLines = computed();
      const tempDisplayEliminatedCells = computed();
    
      function addTempDisplayItem(obj) {
        const id = uniqueId();
        tempDisplay.value.push({ id, type: obj.type, value: obj.value});
        // 添加进来的item,一段时间后会被清理掉
        setTimeout(() => {
          removeTempDisplayItem(id);
        }, obj.time);
      }
      function removeTempDisplayItem(id) {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    其他

    新游戏、判断是否需要洗牌、洗牌、提示等,这些功能都没什么好说的
    在这里插入图片描述
    为了支持移动端,点击事件用 p5.touchEnded 替代 p5.click

    部署

    写完基本功能后截了个动图,爸妈看后说想玩一玩,所以我就发布在 github pages 上了。

    (途中发现一个事情,vite build后的页面不能直接打开。因为打包后的代码还是模块化的,不能在 file:// 中打开)

    github pages 可以设置 docs 为发布的目标文件夹

    在这里插入图片描述

    修改打包设置

    export default defineConfig({
      // ...
      build: {
          outDir: "docs",
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    每次修改完代码之后,重新打包,然后再 commit push(新的打包 docs 文件夹一定要 push 上去)
    github 就会自动 build 和 deploy 了,需要一定时间,进度可以看 repo 的 actions 页签:
    在这里插入图片描述

  • 相关阅读:
    04.9. 环境和分布偏移
    python连接redis库
    70. 爬楼梯 (进阶),322. 零钱兑换,279.完全平方数
    Z41H-64C高压闸阀型号解析
    无人机生态环境监测、图像处理与 GIS 数据分析
    【Linux】多线程基础
    万能适配器basequickadapter + recycleview实现单选并且默认选择第一个
    Win10下安装CARLA
    网络安全常用靶场推荐
    python利用joblib进行并行数据处理
  • 原文地址:https://blog.csdn.net/tangran0526/article/details/126420856