日期:2021年6月25日标签:MiniCode

webgl实现3D俄罗斯方块 #

试玩地址:3d tetris

源码: https://github.com/pengfeiw/3d-tetris

学习webgl/opengl已经有一段时间了,打算做个小程序练一练,所以就有了这个俄罗斯方块。采用的技术是webgl + typescript + canvas,至于界面使用的是react + material-ui框架。下面我将介绍一些实现的技术点。

一.介绍 #

3d俄罗斯方块

界面左侧是可以打开或者关闭的设置面板。可以通过distance和rotate调整界面中视角和距离,以适应不同用户的视觉习惯。下面也讲解了基本操作按键,旋转、左移、右移、加速以及暂停。中间是游戏主体部分,是一个canvas,通过webgl实现游戏内容的绘制。为了使图块视觉效果明显,我加入了平行光照,让方块更真实一点。右侧是当前游戏信息和按钮控制,右侧的图块预览也是一个canvas,不过是canvas的二维绘制,通过CanvasRenderingContext2D实现预览效果。

二.俄罗斯方块实现思路 #

我不讲解具体的技术细节,我只讲一些功能是如何实现,比如碰撞检测算法。对细节感兴趣想要学习的朋友,可以下载我的源码参考学习。

图形方块的绘制——旋转、移动 #

一个俄罗斯方块游戏,有很多不同的形状,典型的有I、O、S、Z、L、J、T几种。

俄罗斯方块

可以用一个4x4的区域绘制这些形状,所有的形状都在这个4x4的区域范围内。这里采用二进制01的思想,将4x4的区域内都赋予0和1,如果是0表示这一个cell不需要绘制,如果是1表示需要绘制,那么一个s形的block可以表示成如下几种形态:

俄罗斯方块

这里为什么是四个,而不是两个呢?是因为从左至右表示的是每次旋转90度后的形态,恰好s形旋转180度后又和原来的形态重合了。为了方便记下这一组状态,可以使用一个16位的二进制数存储,用16进制表示。那么这7个形状的表示如下:

const shapeO: ShapeData = [0x0660, 0x0660, 0x0660, 0x0660];
const shapeI: ShapeData = [0x4444, 0x0F00, 0x4444, 0x0F00];
const shapeZ: ShapeData = [0x0c60, 0x2640, 0x0c60, 0x2640];
const shapeS: ShapeData = [0x0360, 0x4620, 0x0360, 0x4620];
const shapeL: ShapeData = [0x4460, 0x0E80, 0x6220, 0x0170];
const shapeJ: ShapeData = [0x2260, 0x08E0, 0x6440, 0x0710];
const shapeT: ShapeData = [0x04E0, 0x4640, 0x0720, 0x2620];

使用这种方式的好处就是,每次当我们进行旋转操作,只需要将当前的index + 1既可得到下一个形态。假设当前正在移动的block是ShapeI,并且此时的形态是0x0F00,位于数组中的第二个(索引值为1),此时再旋转一次,我们将索引加1,得到此时的形态为0x444。我们只需要记住上面的shadpeData数组,以及此时的数组中的形态索引shapeIndex,就可以得到此时的形状。 知道了如何绘制block的形状,我们还需要知道block此时的位置。我们可以将block的移动区域分成宽和高固定的格子,例如宽10,高20。那么只需要记住当前block所属的4x4的区域的坐标即可,可以记左上角坐标,也可以是左下角,只要能定位即可。有了block的坐标就可以轻松的实现图块的移动了。 图块的自动下落的功能,通过设置block的坐标y随时间自动变化,并且还可以设置一个下落速度,随着游戏分数的增加而增加。图块左右移动可以设置block的坐标x随键盘或者按钮的控制而变化。

block移动区域的绘制 #

作为block主要移动区域,也就是我游戏中的网格区域,每当一个block下落到最下方,需要重新绘制已固定的block。那么我如何知道那些网格区域已经有block存在,并且需要绘制呢?聪明的你一定想到了,同样可以用0和1做记号,如果是1表示网格区域上方已经有block了,需要绘制block。用二进制的思想是不是很方便的解决了我们的难题。接着往下看。

碰撞检测 #

上面讲解了,如何去记录当前激活的图块的形态及坐标,也知道了使用0和1的方式去记录网格区域的状态,通过状态我们可以绘制出当前的游戏画面。那么有一个问题,就是我们怎么确定block移动到最下方就不能继续向下移动了,怎么检测block的左右侧或者下方是否以经存在固定的block了,导致无法移动。这就需要我们做碰撞检测了。 这里使用了一种更加巧妙的方式了,同样使用二进制的方法。因为我们使用了0和1记录了当前网格中所有的状态,所以我们可以使用与(&)操作符进行碰撞检测。具体做法是,如果此时block要向下移动,我们很容易知道其往下一步的4x4的区域(也就是block将要覆盖的4x4的区域),并且获得这个区域的16个二进制0和1的值,组成一个16进制数areaValue。如果当前block的valueareaValue进行与运算,等于0表示可以移动,非0表示不能移动。

例如下图中,绿色表示一个活动方块,此时block的value,用二进制01表示为[0100-0100-0110-0000],这个block往下一步的4x4二进制区域值为[0000-0000-0110-1100]。这两个数值进行与运算后0x0060, 结果不为0,所以有碰撞无法向下继续移动。左右移动可以使用同样的方式。

俄罗斯方块

网格区域的左侧和右侧以及下侧,我们都可以设置为1值,防止block移动到网格区域外。但是因为图块是从上方往下移动的,所以最上面的网格区域外设置成0值。

二进制的优美,在这个游戏中,我是切切实实的感受到了。

关于minicode,我会一直保持更新的,感兴趣的朋友不妨关注一下。你有任何问题,也可以给我留言,也可以留下联系方式加个好友。

(完)

目录