提到物体的管理,那么我们的第一反应就是分门别类——把相同类别的东西归到一起。
场景管理也是如此,把不同的物体归属到不同类别里,而相似性的判断是根据物体的空间相干性。一个直观的想法就是,我们依然以网格来划分空间,物体落到哪个网格里,它就属于哪个网格,这是迭代层次的想法。
但是这样的话可能会出现很多空白连续的网格,它们不维护任何物体的信息,是“多余”的。还有一种可能性,就是当我们的网格划分的太细时,把一些大的物体切割得支离破碎,当我们的网格划分的太大,又容易导致划分得过于粗糙,不利于我们得到更详细的信息。
不过,从某个层面来说,这些“网格”就是场景划分的最终结果,我们可以通过这个想法了解到场景管理的本质。
在考虑到有稀疏物体分布的场景的情况下,我们可以考虑用不同尺度来描述物体的分布。
设想一个二维的平面,我们可以先把它按田字形划分为四份。
如果这四份子空间中,存在不包含任何物体的子空间,那么我们就标记一下。然后在包含物体的子空间,继续划分为四个子空间,以此类推。
不知不觉中,我们构建了这样一棵树,从根节点出发,不断生长出四个子结点的树。这就是四叉树。
转换到三维空间,我们可以把空间切分成八份。
当我们把物体分门别类的时候,我们所希望的就是下次我们能快速找到我们所需的物体。我们更偏向于用树来管理我们的场景,也是因为树构建了这样的一个快速索引,从另外一个角度来说,它来提供了不同尺度的我们感兴趣的信息。
四叉树
四叉树并不仅仅针对二维的游戏场景上,这里所指的二维是因为四叉树本身仅仅考虑了两个维度。在三维的游戏场景中,我们也可以使用四叉树,因为在四叉树应用最广泛的室外地形场景中,z轴往往只代表地形高度信息,或者说它的信息相比起xy轴而言,并没有特别重要。这是我们在场景管理中可以适当忽略的部分。
定义:每个节点有四个孩子或者没有孩子的树。
数据结构:
(图片来自网络)
上图中,每个节点对应一个平面区域,每一层的区域大小相同。
圆形代表该区域存在物体,且有四个子区域。
白色正方形代表空间中存在物体,并且已经是不需继续划分的,或者已经到了最大深度而不再划分。
黑色正方形代表空间中不存在物体。
初步看来,我们可能要这样描述一个四叉树节点:
struct treeNode {
treeNode* upLeft; //四个子树
treeNode* upRight;
treeNode* downLeft;
treeNode* downRight;
int objectNum; //包含对象个数
Object* object; //包含的对象
Box box; //所在的包围盒
bool hasChild; //是否有孩子
}
优点:
可加速搜索,快速找到包含/不包含物体的地图,广泛应用于室外场景的管理。
与bsp树相比是动态的,因为可以动态载入或者删除物体并且更新树。而Bsp作为一棵二叉树却不能修改主要是因为它的切割面的选择是根据周围的物体来选择的。
缺点:
划分出来的四叉树往往没有二叉树那么平衡。
应用:
(1)二维碰撞检测的广阶段
(2)地形渲染剔除机制
(3)根据四叉树只绘制玩家周围物体,或者用LoD技术
(4)动态载入游戏对象
八叉树
相比起四叉树,我们认为八叉树进行了维度的扩展,能够包含更加丰富的信息。这也意味着它只能应用于三维的场景管理。
定义:每个节点有八个孩子或者没有孩子的树。
数据结构:
(图片来自网络)
上图给出了一个八叉树的数据结构:
struct treeNode {
treeNode* child[8];
bool hasChild;
Box box;
int objectNum;
Object* object;
}
在我看过的某本图形学中,还介绍了一种线性八叉树,如果有线性八叉树,那么可想而知应该也是有线性四叉树的。
它和数组存储的二叉树是一样的,按照层次遍历的顺序,一个个把结点存在线性的数组里。但是我们在二叉树中一般都是不提倡数组存储形式的,因为总会有别有用心的人设计出一棵结点不多但是非常倾斜的二叉树存储方式来搞垮你的数据结构。
但是在游戏的场景中,就不需要怎么担心这个问题。我们一般都不会创建太多层的树,这也就意味着数组的范围是有一个可以接受的上限的,虽然这中间可能有不少无效结点,但是我们的索引速度加快了。它更适用于物体较为密集的场景。
从某种程度上看来,线性八叉树有点接近网格存储的方式。
优缺点和四叉树基本差不多,和四叉树相比就是需要多维护一层次信息,在得到更好的体验时也要付出一定的时间和空间代价。