我经常说,我们以前玩过的很多游戏虽然看上去是2D的,但其实是3D游戏——比如热血物语、双截龙系列,它们具有一套三维的立体空间逻辑,从而不再使得游戏的舞台限制在只有左右移动和上下跳跃这两种轴向——拥有三个维度极为自由开放的移动范围相信也给很多玩家留下了深刻的印象,在上次的文章(https://indienova.com/indie-game-development/make-a-fc-nes-collision/)里,我们已经介绍了这种游戏的物理部分简单实现,本次将会聊到这类游戏在渲染上的重要技巧。
显然,在用2D精灵模拟的3D空间里,任何一个物体都不简单:如图所示的一个立方体,具有一个oblique视角(或者,我们叫cabinet视角更准确)的造型。这样的视角下,如果有另外一个精灵和它相互遮挡,应该怎么处理它们的渲染顺序关系呢?
注:为了简化描述,本文章的3D空间假设所有物件的z坐标都相同,即"没有和地面的高度差"。
任意两个物体之间的排序
按照我们一般的简单思路(比如在经典的topdown视角下,如RPGMAKER系列),谁在世界上具有较大的y值,谁就靠近屏幕,谁就应该更靠后地渲染,对吧?不过在这样的视角下,就没有那么简单了,来看一个例子:
如图,根据十字形标记,我们可以轻松地发现shari的y坐标小于立方体的坐标,所以她理应被挡在立方体的后面(先于立方体渲染),对吧?可是当她来到另外一侧的时候,情况就变化了:
如图,shari的y坐标仍然小于立方体的y坐标,但这个位置上,我们是希望shari遮挡立方体而不是被立方体遮挡的,所以我们需要考虑到更多的情况。
显然,这个世界里的任何一个物体都应该携带它的"底座"信息。如图,红色的平行四边形就是上面立方体的"底座"信息,而绿色是它的高度信息(如果你需要一些更细致的排序,比如算上z轴,本文暂不讨论)有了这个信息,我们就可以确定任意两个对象之间的绘制顺序。
我们把两个要排序的物体分别命名为A和B,A底座的上边缘称为aTop,下边缘称为aBottom;B底座的上边缘称为bTop,下边缘称为bBottom。
aBottom的y坐标小于bTop的y坐标:
这时候A显然是在B的后方,也就是A先于B渲染。
aTop的y坐标大于bBottom的y坐标:
这时候A显然是在B的前方,也就是B先于A渲染。
其它的情况:
由于我们这里的视角下,侧面是一个45度的斜线,所以我们可以很轻松地把两个物体下边缘的y轴距离加到aBottom的右端点去进行判断,像是这样:
distanceY = bBottom.y - aBottom.y
(我们用left和right表示一个线段的左端点和右端点)
aBottom.left.x > bBottom.right.x + distanceY
如果这个判断是成立的,那就是上图所述的情况:A在B的前方,也就是B先于A渲染。
否则就是下图所述的情况:A在B的后方,也就是A先于B渲染。
对场景内所有物体的排序
好,至此我们已经可以为场景内任意的两个对象进行排序了,那么,我们也可以为场景中所有的对象组织排序。很多人的第一反应是,直接用冒泡/选择/归并/快排这样的算法来对场景中需要排序的物体进行排序不就好了吗?其实这是不合适的,因为场景中的物体遮挡存在拓扑关系,如果直接使用线性表的排序方法,可能在swap一对物体以确保他们遮挡关系的同时又会直接破坏了另一对物体的遮挡关系,因此,我们的第一步是对场景中需要排序的物体进行连线。
带有弧线的那一端是射线的末端,末端的物体比首端的物体更先渲染(位置更靠后)。
这里说明一下,我们只对需要排序的一对物体进行连线(出于性能考虑),所以你的显示对象可能需要有相应的bounding_box或是什么的。
当场景中的物体足够多,遮挡关系足够复杂时,不难发现,我们的物体遮挡关系实际上构建出了一个有向图。而有向图可以通过拓扑排序来获取一个不唯一的线性序列,照着这个线性序列进行渲染就可以获得正确的场景排序了。
(图中蓝绿色的矩形是判断精灵相交/重叠用的bounding_box,DRAWIDX则是拓扑排序后得到的渲染顺序值)
这样,就得到了整个场景的正确呈现顺序。
最后一点问题
相信我们都看到过一些"视觉错觉"的趣味图片,对吧?譬如三个无限循环的台阶,相互遮挡的三个棱柱……之类的,很有意思,不过在游戏开发当中可一点也不有趣。很显然,当游戏场景中出现了几个互相存在遮挡和被遮挡关系的对象时,我们构造出的有向图就成环了。而成环的有向图是不能进行拓扑排序的,这就导致我们没有办法去按正确顺序呈现画面。解决这个问题的最好方法就是把容易引起该问题的那个物体分割为两个物体,使得其中的一个部分"专门遮挡"而另一个部分"专门被遮挡",这样就可以消除我们构造的有向图中的环。
本次的讨论就到这里,祝大家开发顺利,新年快乐!
好!
这个东西很有用,但是没有抠像立体的情况,感觉上只需要注意y轴在我项目中已经很好了