Ответ всегда заключается в использовании массива или std :: vector. Типы, такие как связанный список или std :: map, обычно абсолютно ужасны в играх, и это определенно включает случаи, такие как наборы игровых объектов.
Вы должны хранить сами объекты (не указатели на них) в массиве / векторе.
Вы хотите непрерывную память. Вы действительно этого хотите. Итерирование любых данных в несмежной памяти приводит к большим потерям кэша в целом и устраняет возможность для компилятора и ЦП выполнять эффективную предварительную выборку кэша. Одно это может убить производительность.
Вы также хотите избежать выделения памяти и освобождения. Они очень медленные, даже с быстрым распределителем памяти. Я видел, как игры получают 10-кратное увеличение FPS, просто удаляя несколько сотен выделений памяти в каждом кадре. Не похоже, что это должно быть так плохо, но это может быть.
Наконец, большинство структур данных, которые вам нужны для управления игровыми объектами, могут быть гораздо более эффективно реализованы в массиве или векторе, чем в дереве или списке.
Например, для удаления игровых объектов вы можете использовать swap-and-pop. Легко реализуется с помощью чего-то вроде:
std::swap(objects[index], objects.back());
objects.pop_back();
Вы также можете просто пометить объекты как удаленные и поместить их индекс в свободный список для следующего раза, когда вам нужно будет создать новый объект, но лучше использовать swap-and-pop. Это позволяет вам делать простой цикл for для всех живых объектов без разветвления, кроме самого цикла. Для интеграции физики пули и тому подобного это может значительно повысить производительность.
Что еще более важно, вы можете найти объекты с простой парой поиска таблиц из стабильного уникального использования структуры карты слотов.
Ваши игровые объекты имеют индекс в своем основном массиве. Их можно очень эффективно найти только с помощью этого индекса (намного быстрее, чем карта или даже хеш-таблица). Однако индекс нестабилен из-за подкачки и всплывающих окон при удалении объектов.
Карта слотов требует двух уровней косвенности, но оба являются простыми поисками массива с постоянными индексами. Они быстрые . Действительно быстро.
Основная идея заключается в том, что у вас есть три массива: список основных объектов, список косвенных ссылок и список свободных списков косвенных ссылок. Ваш основной список объектов содержит ваши фактические объекты, где каждый объект знает свой уникальный идентификатор. Уникальный идентификатор состоит из индекса и тега версии. Список косвенности - это просто массив индексов для основного списка объектов. Свободный список - это стек индексов в списке косвенных ссылок.
Когда вы создаете объект в основном списке, вы находите неиспользуемую запись в списке косвенных ссылок (используя свободный список). Запись в списке косвенных ссылок указывает на неиспользуемую запись в основном списке. Вы инициализируете свой объект в этом месте и присвойте его уникальный идентификатор индексу выбранной записи списка косвенного обращения и существующему тегу версии в основном элементе списка плюс один.
Когда вы уничтожаете объект, вы делаете swap-and-pop как обычно, но вы также увеличиваете номер версии. Затем вы также добавляете индекс списка косвенности (часть уникального идентификатора объекта) в свободный список. При перемещении объекта как части «своп-и-поп» вы также обновляете его запись в списке косвенного обращения на новое место.
Пример псевдокода:
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)
Уровень косвенности позволяет вам иметь стабильный идентификатор (индекс уровня косвенности, где записи не перемещаются) для ресурса, который может перемещаться во время сжатия (основной список объектов).
Тег версии позволяет вам сохранить идентификатор объекта, который может быть удален. Например, у вас есть идентификатор (10,1). Объект с индексом 10 удаляется (скажем, ваша пуля попадает в объект и уничтожается). Объекту в этом месте памяти в главном списке объектов затем повышается номер версии, давая ему (10,2). Если вы попытаетесь снова найти (10,1) из устаревшего идентификатора, поиск вернет этот объект через индекс 10, но увидит, что номер версии изменился, поэтому идентификатор больше не действителен.
Это самая быстрая структура данных, которую вы можете иметь со стабильным идентификатором, который позволяет объектам перемещаться в памяти, что важно для локальности данных и согласованности кэша. Это быстрее, чем любая возможная реализация хеш-таблицы; Хеш-таблица, по крайней мере, должна вычислять хеш (больше инструкций, чем поиск в таблице), а затем должна следовать цепочке хеширования (либо связанный список в ужасном случае std :: unordered_map, либо список с открытым адресом в любая не глупая реализация хеш-таблицы), а затем должна выполнять сравнение значений для каждого ключа (не дороже, но, возможно, дешевле, чем проверка тега версии). Очень хорошая хеш-таблица (не та, которая есть ни в одной реализации STL, поскольку STL требует хеш-таблицу, которая оптимизируется для других вариантов использования, чем вы играете для списка игровых объектов), может сэкономить на одной косвенности,
Существуют различные улучшения, которые вы можете внести в базовый алгоритм. Например, использовать что-то вроде std :: deque для основного списка объектов; один дополнительный уровень косвенности, но позволяет вставлять объекты в полный список без аннулирования любых временных указателей, которые вы получили из карты слотов.
Вы также можете избежать сохранения индекса внутри объекта, так как индекс может быть рассчитан по адресу памяти объекта (это - объекты), и даже лучше требуется только при удалении объекта, в этом случае у вас уже есть идентификатор объекта (и, следовательно, индекс) в качестве параметра.
Извинения за рецензию; Я не чувствую, что это самое ясное описание. Уже поздно, и это трудно объяснить, не тратя больше времени, чем я, на примеры кода.