答案总是使用数组或std :: vector。诸如链表或std :: map之类的类型在游戏中通常绝对是可怕的,并且肯定包括诸如游戏对象集合之类的情况。
您应该将对象本身(而不是指向它们的指针)存储在数组/向量中。
您需要连续的内存。您真的很想要它。遍历非连续内存中的任何数据通常会导致很多高速缓存未命中,并且使编译器和CPU无法进行有效的高速缓存预取。仅此一项就可以降低性能。
您还希望避免内存分配和释放。即使使用快速的内存分配器,它们也非常慢。我已经看到,只要删除每帧几百个内存分配,游戏就可以获得10倍FPS的提升。看起来应该没有那么糟糕,但事实可能如此。
最后,您关心的用于管理游戏对象的大多数数据结构可以比使用树或列表更有效地实现在数组或向量上。
例如,要删除游戏对象,您可以使用交换和弹出。可以轻松实现,例如:
std::swap(objects[index], objects.back());
objects.pop_back();
您也可以将对象标记为已删除,并在下次需要创建新对象时将它们的索引放在空闲列表中,但是进行交换和弹出效果更好。它使您可以对所有活动对象进行简单的for循环,而无需循环本身。对于子弹物理集成等而言,这可以显着提高性能。
更重要的是,您可以使用槽位图结构从稳定的唯一表中查找一对简单的表查询对象。
您的游戏对象的主数组中都有一个索引。仅使用此索引就可以非常有效地查找它们(比映射甚至是哈希表要快得多)。但是,索引在删除对象时由于交换和弹出而不稳定。
插槽映射需要两层间接寻址,但是两者都是具有恒定索引的简单数组查找。他们很快。真快。
基本思想是,您具有三个数组:主对象列表,间接列表和间接列表的空闲列表。您的主要对象列表包含您的实际对象,其中每个对象都知道自己的唯一ID。唯一ID由索引和版本标签组成。间接列表只是主对象列表的索引数组。空闲列表是指向间接列表的索引的堆栈。
在主列表中创建对象时,您会在间接列表中找到一个未使用的条目(使用空闲列表)。间接列表中的条目指向主列表中未使用的条目。您可以在该位置初始化对象,然后将其唯一ID设置为所选的间接列表条目的索引以及主列表元素中现有的版本标签加上一个。
销毁对象时,您可以照常进行交换和弹出操作,但同时还要增加版本号。然后,您还将间接列表索引(对象唯一ID的一部分)添加到空闲列表中。在将对象作为交换和弹出的一部分进行移动时,还可以将其在间接列表中的条目更新为新位置。
伪代码示例:
Object:
int index
int version
other data
SlotMap:
Object objects[]
int slots[]
int freelist[]
int count
Get(id):
index = indirection[id.index]
if objects[index].version = id.version:
return &objects[index]
else:
return null
CreateObject():
index = freelist.pop()
objects[count].index = id
objects[count].version += 1
indirection[index] = count
Object* object = &objects[count].object
object.initialize()
count += 1
return object
Remove(id):
index = indirection[id.index]
if objects[index].version = id.version:
objects[index].version += 1
objects[count - 1].version += 1
swap(objects[index].data, objects[count - 1].data)
间接层使您可以为压缩期间可以移动的资源(主对象列表)具有稳定的标识符(间接层的索引,其中条目不移动)。
版本标记允许您将ID存储到可能被删除的对象。例如,您有一个ID(10,1)。索引为10的对象将被删除(例如,子弹击中一个对象并被销毁)。然后,位于主对象列表中该内存位置中的对象的版本号会增加,即为(10,2)。如果尝试从一个过时的ID再次查找(10,1),则该查找通过索引10返回该对象,但可以看到版本号已更改,因此该ID不再有效。
这是具有绝对稳定的ID的绝对最快的数据结构,该ID允许对象在内存中移动,这对于数据局部性和缓存一致性非常重要。这比哈希表的任何实现都快。哈希表至少需要计算一个哈希(比表查找更多的指令),然后必须遵循哈希链(在std :: unordered_map的可怕情况下是一个链表,或者在其中是一个打开地址的列表)哈希表的所有非愚蠢实现),然后必须对每个键进行值比较(比版本标签检查更昂贵,但可能更便宜)。一个非常好的哈希表(不是STL的任何实现中的哈希表,因为STL要求哈希表针对与游戏对象列表有关的不同用例进行优化)可以节省一个间接访问,
您可以对基本算法进行各种改进。例如,对主对象列表使用类似std :: deque的东西;间接层的另一层,但允许将对象插入完整列表,而不会使您从插槽图获取的任何临时指针无效。
您还可以避免将索引存储在对象内部,因为可以从对象的内存地址(此-对象)计算索引,并且只有在删除对象时才需要更好,在这种情况下,您已经有了对象的ID(因此,索引)作为参数。
致歉 我认为这不是最清晰的描述。时间太晚了,如果不花比我更多的时间在代码示例上的时间,就很难解释。