WebGL第三十四课:2D拼图游戏

708 阅读7分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

本文标题:WebGL第三十四课:2D拼图游戏

友情提示

这篇文章是WebGL课程专栏的第34篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

本课代码直接跳转获取:三十四课代码

引子

这篇文章的主要目的就是展示,如何用多个分散的对象来显示图片。

我们的代码里,抽象了一个GridObject类,这个类如果要渲染出来,那么他包含了6个顶点和两个三角形,组成了一个矩形的形状。我们可以对这个6个顶点的UV进行设置,让他可以显示图片。

前面几篇文章,都是正常的设置UV,让格子的顶点和图片正好贴合,从而,一个格子显示一个完整的图片。

但是有的时候,我们需要将一个完整的图片分开,例如,拆成九个,九宫格式的显示图片。

效果如下:

image.png

效果分析

上面的效果,说实在的,也有很多种办法可以实现。但是最好还是用九个不同的格子,来分别锚定不同的UV。这样实现之后,控制度更加自由。例如,其中某一个格子可以进行旋转:

image.png

细节实现

我们来分析一下最左下角的一个格子,也就是一半猫爪子的那个:

image.png

这个格子和完整的图片来锚定一下:

左下角锚定的是完整图片的左下角,也就是UV = [0,0]

右上角锚定的完整图片的1/3处的位置,UV = [1/3, 1/3]

再来看看这个格子的位置:从canvas坐标系来说,这个格子的位置应该是[-2/3, -2/3]

这样来分析的话,九个格子的 UV 和 位置 都可以 得到。

我们来更改一下GridObject的代码,使之支持,可以修改UV:

    constructor(scalex, scaley, posx, posy) {
        ...
        ...
        // 新加一个字段,uv
        this.uv = { leftbottom: [0, 0], topright: [1, 1] };
    }
    genData(gl) {
        this.data = [
            // 第一个三角形
            -1, -1, this.uv.leftbottom[0], this.uv.leftbottom[1], // 左下角点
            1, -1, this.uv.topright[0], this.uv.leftbottom[1], // 右下角点
            1, 1, this.uv.topright[0], this.uv.topright[1], // 右上角点
            // 第二个三角形
            1, 1, this.uv.topright[0], this.uv.topright[1],  // 右上角点
            -1, 1, this.uv.leftbottom[0], this.uv.topright[1],  // 左上角点
            -1, -1, this.uv.leftbottom[0], this.uv.leftbottom[1],  // 左下角点
        ];
        ...
        ...
        ...
    }

好了,经过修改代码,GridObject的UV信息可以随时修改了。

初始化九个格子

根据上面小节进行的分析和对GridObject的修改,我们可以在初始化的时候,正确写出九个格子的生成代码了:

function generateGrid(gl) {
    //////////////////////////
    let level = 0; // 九宫格的最下面是第0层,中间一层是1,最上面是2
    let tempGrid;
    let xScale = 0.3;
    let yScale = 0.3;
    for (; level <= 2; level++) {
        tempGrid = new GridObject(xScale, yScale, -2 / 3, -2 / 3 + level * (2 / 3));
        tempGrid.uv.leftbottom[1] = (1 / 3) * level;
        tempGrid.uv.topright[0] = 1 / 3;
        tempGrid.uv.topright[1] = 1 / 3 + (1 / 3) * level;
        gridList.push(tempGrid);

        tempGrid = new GridObject(xScale, yScale, 0, -2 / 3 + level * (2 / 3));
        tempGrid.uv.leftbottom[0] = 1 / 3;
        tempGrid.uv.leftbottom[1] = (1 / 3) * level;
        tempGrid.uv.topright[0] = 2 / 3;
        tempGrid.uv.topright[1] = 1 / 3 + (1 / 3) * level
        gridList.push(tempGrid);
        
        tempGrid = new GridObject(xScale, yScale, 2 / 3, -2 / 3 + level * (2 / 3));
        tempGrid.uv.leftbottom[0] = 2 / 3;
        tempGrid.uv.leftbottom[1] = (1 / 3) * level;
        tempGrid.uv.topright[0] = 3 / 3;
        tempGrid.uv.topright[1] = 1 / 3 + (1 / 3) * level;
        gridList.push(tempGrid);

    }

    gridList.forEach(element => {
        element.genData(gl);
    });
}

指的注意的是,上面代码的xScaleyScale都是0.3

为什么这样呢?因为这样,可以让格子之间保持一个小的边界,更容易观察,从效果上讲,有的人更喜欢加一个小边界,如果你不想有这么大的边界,可以将上面两个值变成 1/3 - 0.01,效果如下:

image.png

值越靠近1/3中间的缝隙就越小, 这个看个人喜好。

拼图游戏

为了实现一个拼图游戏,我们将右上角的格子,去掉,因为拼图游戏就是这样规定的,必须少一个格子:

image.png

然后从逻辑上讲,我们能够控制一个格子的移动。

这个其实GridObject已经实现了,直接修改

posx
posy

这两个字段即可,我们试验一下,将尾巴格子移动到右上角,也就是第7个格子,的位置设置成:

gridList[7].posx = 2/3;
gridList[7].posy = 2/3;

效果如下:

image.png

哟呵,真有意思!

格子移动的限制

对于拼图游戏来讲,任何一个状态下,能动的格子最多有三个,而且,他们能动的方向是不一样的。

所以,我按下键盘上的WASD来控制,可以精确到其中的格子。

比如在默认状态下,我如果按了w,那么肯定是中间一排,最右边一个往上移动一个格子。

根据这个思路,当我们按下键盘w时,

  • 首先要找到哪一个格子是空的
  • 然后在这个空格格子下方寻找,是否有格子
  • 如果有格子,则将这个格子移动上去
  • 如果没有格子,则什么也不做,忽略键盘事件

为了寻找哪个格子是空的,我们需要一个额外的数组来记录格子的占用情况:

var NineGrid = [-1, -1, -1, -1, -1, -1, -1, -1, -1];

下标代表九宫格的位置,0是左下角,8是右上角。 里面存储的数值,是gridList的下标,代表是哪个小图片。

初始化之后,NineGrid 应该是:

[0, 1, 2, 3, 4, 5, 6, 7, -1]

这说明,右上角的格子没有占用,其他的都被某一个格子占用。

有了这个NineGrid数组,很容易找到空格子,那就是遍历,找到 -1即可。

然后就是找到某一个格子的上下左右,是什么格子,这个可以看图,直接写死:

var NineGridNeigbour = [
    [3, -1, -1, 1], // 0号格子上下左右分别是 3号,无效,无效,1号
    [4, -1, 0, 2], // 1号格子上下左右
    [5, -1, 1, -1], // 2号格子上下左右
    [6, 0, -1, 4], // 3号格子上下左右
    [7, 1, 3, 5], // 4号格子上下左右
    [8, 2, 4, -1], // 5号格子上下左右
    [-1, 3, -1, 7], // 6号格子上下左右
    [-1, 4, 6, 8], // 7号格子上下左右
    [-1, 5, 7, -1], // 8号格子上下左右
];
// cur: 当前下标
// dir: 方向 0 1 2 3 分别代表 上下左右
// 通过一个格子和方向,找到对应的格子
function NineGridFind(cur, dir) {
    return NineGridNeigbour[cur, dir];
}

由代码可以看出,如果找到的返回值是-1,说明当前格子的某一个方向已经是边界,无意义。

键盘事件,和后续逻辑

监听键盘事件,非常容易:

window.onkeyup = function (event) {
    console.log(event.keyCode);
};

event.keyCode 是一个数字,很容易就试验出来:

  • w: 87
  • s: 83
  • a: 65
  • d: 68

我们将这个事件函数的逻辑填上, 首先展示出,应该动的格子是哪一个,我们让这个格子旋转45°:

    window.onkeyup = function (event) {
        console.log(event.keyCode);
        let emptyIdx = NineGridEmpty(); // 遍历找到空格子 
        let target = -1;
        if (event.keyCode == 87) { // 按了键盘的w
            target = NineGridFind(emptyIdx, 1);// 1 代表要找到下面的
        } else if (event.keyCode == 83) { // s
            target = NineGridFind(emptyIdx, 0);// 0 代表要找到上面的
        } else if (event.keyCode == 65) { // a
            target = NineGridFind(emptyIdx, 3);// 3 代表要找到右边的
        } else if (event.keyCode == 68) { // d
            target = NineGridFind(emptyIdx, 2);// 2 代表要找到左边的
        }
        console.log('target', target, 'emptyIdx', emptyIdx);
        if (target == -1) { // 边界无意义
            return;
        }
        let targetGrid = gridList[target];
        targetGrid.rotate = 45;
        gl_draw();
    };

好了,我们在键盘上按下w键,效果:

image.png

再试验一下按下d键,效果:

image.png

嗯,对的,找到了需要挪动的格子。

接下来就简单了,让目标格子移动即可,移动之后,别忘了设置NineGrid,来更新一下九宫格的占用情况。

代码如下:

    window.onkeyup = function (event) {
      console.log(event.keyCode);
      let emptyIdx = NineGridEmpty(); // 遍历找到空格子 
      let target = -1;
      let posxMove = 0;
      let posyMove = 0;
      if (event.keyCode == 87) { // 按了键盘的w
          target = NineGridFind(emptyIdx, 1);// 1 代表要找到下面的
          posyMove = 2 / 3;
      } else if (event.keyCode == 83) { // s
          target = NineGridFind(emptyIdx, 0);// 0 代表要找到上面的
          posyMove = -2 / 3;
      } else if (event.keyCode == 65) { // a
          target = NineGridFind(emptyIdx, 3);// 3 代表要找到右边的
          posxMove = -2 / 3;
      } else if (event.keyCode == 68) { // d
          target = NineGridFind(emptyIdx, 2);// 2 代表要找到左边的
          posxMove = 2 / 3;
      }
      console.log('target', target, 'emptyIdx', emptyIdx);
      if (target == -1) { // 边界无意义
          return;
      }
      let targetGrid = gridList[NineGrid[target]];
      // targetGrid.rotate += 15;
      targetGrid.posx += posxMove;
      targetGrid.posy += posyMove;
      NineGrid[emptyIdx] = NineGrid[target];
      NineGrid[target] = -1;
      gl_draw();
  };

好了,到这里为止,你就可以随便按wsad来移动格子了,先打乱,然后再拼好,看看你行不行!!

image.png




  正文结束,下面是答疑

小能能说:如果我想按下键盘的时候,有一个移动渐变,而不是很生硬的移过去,怎么搞?

  • 那就逐渐改变目标格子的位置即可,具体实施办法有很多,这里先不提。