定义:将对象根据它们的位置存储在数据结构中,来高效地定位对象。
在游戏中,AI向最近的敌人攻击是很常见的,但如果有很多单位的话,他们AI需要频繁的查找单位,然后在检测是不是距离最近的单位,时间复杂度是o(n ^ 2)
之所以会出现这个问题,是因为我们没有指明潜在的对象顺序。假如我们游戏的所有单位都只在一条直线上,我们只需要二分查找就可以找到最近对象了。操作系统中,有着局部性原理,我们也可以根据游戏物体的位置来存储对象,这样就能更快的找到他们
注意事项:
空间分区的存在是为了将O(n)或者O(n²) 的操作降到更加可控的数量级。 你拥有的对象越多,这就越重要。相反的,如果n足够小,也许不需要担心这个
这不仅能够用于存储可移动的游戏对象,还可用于静态美术等场景资源。如果对象发生移动,我们需要在数据结构中重新追踪该对象,这也会增加CPU消耗。
实现模式有很多种,都有各自的性能优势
将整个游戏战场分割为固定大小的网格中,每个格子分别存储一组单位,对象在网格中的存储方式应该是双向链表,因为他增删结点比较迅速
在单位移动需要改变位置时,我们需要先判断是否穿越了格子的边界,如果穿越,我们需要先移除再添加到网格中
对于近距离攻击来说,只运算网格中的单位相互攻击是没啥大问题的。但是对于其他的攻击方式形成了新的问题,比如有一个远距离攻击的法师单位或者是能够冲锋的骑兵单位,就不能只判断单一网格。
这时候我们不仅需要比较同一网格,还需要比较邻近网格对象
层次划分:
层次空间划分将空间分成几个区域。 然后,如果其中一个区域还包含多个对象,再划分它。 这个过程递归进行,直到每个区域都有少于最大数量的对象在其中。
平面划分:
内存使用量确定,因为划分不会有增删,分区的内存使用量固定
在对象改变位置时更新得更快,层次更新则需要多层级调整,甚至改变层次划分
与对象有关:
固定划分中,移动单位意味着只需要从网格中删除,在加入到另一个网格中就可以了,但不是固定划分的话就需要改变边界,对象可以增量添加。 添加对象意味着找到正确的划分然后放入,这点可以一次性完成,没有任何性能问题。
与对象无关:
在BSP和KD树中这样各地适应对象划分世界,可以让每个部分都包含接近数目的对象,划分边界时需要考虑每一边各有多少对象。
Oct-Tree:八叉树,对于三维空间中的一个场景将其横竖切三刀,切成八块,在二维空间表现为四叉。。
KD-Tree:k-d树每次只会选取一个基轴方向进行分割,比如二维空间中先沿x方向分割,然后再沿y轴方向分割。
BSP-Tree:也是每次划分选择一个方向将空间分成两部分,与KD-Tree相比它不是横平竖直的划分,不好计算
其中八叉树可以拥有两者的优点:对象能够批量增加马,移动对象很快,分区是平衡的
空间划分还有一个值得注意的问题,空间分区是否是游戏对象存储的唯一地方
**如果唯一:**这能够减少内存消耗
**如果不是:**如果我们需要用除了位置关系外其他的方式来访问对象,比如特定种类的兵种,有了其他的数据保存结构我们能够更快的访问
选择一套数据结构来存储网格及对象,这里要考虑静态场景和动态场景的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zHz20HyF-1669003839676)(游戏优化之空间划分/image-20221121120551536.png)]
方案a就是用的之前讲的双向链表,但其会造成内存浪费也会降低cpu cache命中
方案b用数组取代了链表来存储场景中的对象。数组是一块连续的内存地址,可以提高cpu cache的命中,而且相对于链表可以节省内存空间。每一个网格需要记录存储对象的其实位置,以及所属对象的个数。这需要遍历两次场景第一次确定每个网格的对象数量及对象的总数,第二次遍历分配场景对象。方案b适合静态场景,并且可以离线预处理
还有种方案是,如果场景很大但是游戏角色并不多,比如Apex等吃鸡游戏,那么就可以使用哈希表来存储,避免了大量检索空的网格
每种空间分区数据结构基本上都是将一维数据结构扩展成更高维度的数据结构。