这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战
本文标题:WebGL第三十四课:2D拼图游戏
友情提示
这篇文章是WebGL课程专栏的第34篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
本课代码直接跳转获取:三十四课代码
引子
这篇文章的主要目的就是展示,如何用多个分散的对象来显示图片。
我们的代码里,抽象了一个GridObject
类,这个类如果要渲染出来,那么他包含了6个顶点和两个三角形,组成了一个矩形的形状。我们可以对这个6个顶点的UV进行设置,让他可以显示图片。
前面几篇文章,都是正常的设置UV,让格子的顶点和图片正好贴合,从而,一个格子显示一个完整的图片。
但是有的时候,我们需要将一个完整的图片分开,例如,拆成九个,九宫格式的显示图片。
效果如下:
效果分析
上面的效果,说实在的,也有很多种办法可以实现。但是最好还是用九个不同的格子,来分别锚定不同的UV。这样实现之后,控制度更加自由。例如,其中某一个格子可以进行旋转:
细节实现
我们来分析一下最左下角的一个格子,也就是一半猫爪子的那个:
这个格子和完整的图片来锚定一下:
左下角锚定的是完整图片的左下角,也就是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);
});
}
指的注意的是,上面代码的xScale
和yScale
都是0.3
。
为什么这样呢?因为这样,可以让格子之间保持一个小的边界,更容易观察,从效果上讲,有的人更喜欢加一个小边界,如果你不想有这么大的边界,可以将上面两个值变成 1/3 - 0.01
,效果如下:
值越靠近1/3
中间的缝隙就越小, 这个看个人喜好。
拼图游戏
为了实现一个拼图游戏,我们将右上角的格子,去掉,因为拼图游戏就是这样规定的,必须少一个格子:
然后从逻辑上讲,我们能够控制一个格子的移动。
这个其实GridObject
已经实现了,直接修改
posx
posy
这两个字段即可,我们试验一下,将尾巴格子移动到右上角,也就是第7个格子,的位置设置成:
gridList[7].posx = 2/3;
gridList[7].posy = 2/3;
效果如下:
哟呵,真有意思!
格子移动的限制
对于拼图游戏来讲,任何一个状态下,能动的格子最多有三个,而且,他们能动的方向是不一样的。
所以,我按下键盘上的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键,效果:
再试验一下按下d键,效果:
嗯,对的,找到了需要挪动的格子。
接下来就简单了,让目标格子移动即可,移动之后,别忘了设置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来移动格子了,先打乱,然后再拼好,看看你行不行!!
正文结束,下面是答疑
小能能说:如果我想按下键盘的时候,有一个移动渐变,而不是很生硬的移过去,怎么搞?
- 那就逐渐改变目标格子的位置即可,具体实施办法有很多,这里先不提。