Я делаю шутер от первого лица и знаю о множестве различных типов контейнеров, но я хотел бы найти контейнер, который является наиболее эффективным для хранения динамических объектов, которые будут добавляться и удаляться из игры довольно часто. EX-Пуля.
Я думаю, что в этом случае это будет список, так что память не будет смежной и никогда не будет происходить изменение размера. Но тогда я также обдумываю использование карты или набора. Если у кого-то есть полезная информация, я был бы признателен.
Я пишу это в C ++, кстати.
Также я придумала решение, которое, я думаю, будет работать.
Для начала я собираюсь выделить вектор большого размера, скажем, 1000 объектов. Я собираюсь отследить последний добавленный индекс в этом векторе, чтобы я знал, где находится конец объектов. Затем я также создам очередь, в которой будут храниться индексы всех объектов, которые «удалены» из вектора. (Никакого фактического удаления не будет сделано, я просто буду знать, что этот слот свободен). Поэтому, если очередь пуста, я добавлю к последнему добавленному индексу в векторе + 1, иначе я добавлю к индексу вектора, который был в начале очереди.
источник
Ответы:
Ответ всегда заключается в использовании массива или std :: vector. Типы, такие как связанный список или std :: map, обычно абсолютно ужасны в играх, и это определенно включает случаи, такие как наборы игровых объектов.
Вы должны хранить сами объекты (не указатели на них) в массиве / векторе.
Вы хотите непрерывную память. Вы действительно этого хотите. Итерирование любых данных в несмежной памяти приводит к большим потерям кэша в целом и устраняет возможность для компилятора и ЦП выполнять эффективную предварительную выборку кэша. Одно это может убить производительность.
Вы также хотите избежать выделения памяти и освобождения. Они очень медленные, даже с быстрым распределителем памяти. Я видел, как игры получают 10-кратное увеличение FPS, просто удаляя несколько сотен выделений памяти в каждом кадре. Не похоже, что это должно быть так плохо, но это может быть.
Наконец, большинство структур данных, которые вам нужны для управления игровыми объектами, могут быть гораздо более эффективно реализованы в массиве или векторе, чем в дереве или списке.
Например, для удаления игровых объектов вы можете использовать swap-and-pop. Легко реализуется с помощью чего-то вроде:
Вы также можете просто пометить объекты как удаленные и поместить их индекс в свободный список для следующего раза, когда вам нужно будет создать новый объект, но лучше использовать swap-and-pop. Это позволяет вам делать простой цикл for для всех живых объектов без разветвления, кроме самого цикла. Для интеграции физики пули и тому подобного это может значительно повысить производительность.
Что еще более важно, вы можете найти объекты с простой парой поиска таблиц из стабильного уникального использования структуры карты слотов.
Ваши игровые объекты имеют индекс в своем основном массиве. Их можно очень эффективно найти только с помощью этого индекса (намного быстрее, чем карта или даже хеш-таблица). Однако индекс нестабилен из-за подкачки и всплывающих окон при удалении объектов.
Карта слотов требует двух уровней косвенности, но оба являются простыми поисками массива с постоянными индексами. Они быстрые . Действительно быстро.
Основная идея заключается в том, что у вас есть три массива: список основных объектов, список косвенных ссылок и список свободных списков косвенных ссылок. Ваш основной список объектов содержит ваши фактические объекты, где каждый объект знает свой уникальный идентификатор. Уникальный идентификатор состоит из индекса и тега версии. Список косвенности - это просто массив индексов для основного списка объектов. Свободный список - это стек индексов в списке косвенных ссылок.
Когда вы создаете объект в основном списке, вы находите неиспользуемую запись в списке косвенных ссылок (используя свободный список). Запись в списке косвенных ссылок указывает на неиспользуемую запись в основном списке. Вы инициализируете свой объект в этом месте и присвойте его уникальный идентификатор индексу выбранной записи списка косвенного обращения и существующему тегу версии в основном элементе списка плюс один.
Когда вы уничтожаете объект, вы делаете swap-and-pop как обычно, но вы также увеличиваете номер версии. Затем вы также добавляете индекс списка косвенности (часть уникального идентификатора объекта) в свободный список. При перемещении объекта как части «своп-и-поп» вы также обновляете его запись в списке косвенного обращения на новое место.
Пример псевдокода:
Уровень косвенности позволяет вам иметь стабильный идентификатор (индекс уровня косвенности, где записи не перемещаются) для ресурса, который может перемещаться во время сжатия (основной список объектов).
Тег версии позволяет вам сохранить идентификатор объекта, который может быть удален. Например, у вас есть идентификатор (10,1). Объект с индексом 10 удаляется (скажем, ваша пуля попадает в объект и уничтожается). Объекту в этом месте памяти в главном списке объектов затем повышается номер версии, давая ему (10,2). Если вы попытаетесь снова найти (10,1) из устаревшего идентификатора, поиск вернет этот объект через индекс 10, но увидит, что номер версии изменился, поэтому идентификатор больше не действителен.
Это самая быстрая структура данных, которую вы можете иметь со стабильным идентификатором, который позволяет объектам перемещаться в памяти, что важно для локальности данных и согласованности кэша. Это быстрее, чем любая возможная реализация хеш-таблицы; Хеш-таблица, по крайней мере, должна вычислять хеш (больше инструкций, чем поиск в таблице), а затем должна следовать цепочке хеширования (либо связанный список в ужасном случае std :: unordered_map, либо список с открытым адресом в любая не глупая реализация хеш-таблицы), а затем должна выполнять сравнение значений для каждого ключа (не дороже, но, возможно, дешевле, чем проверка тега версии). Очень хорошая хеш-таблица (не та, которая есть ни в одной реализации STL, поскольку STL требует хеш-таблицу, которая оптимизируется для других вариантов использования, чем вы играете для списка игровых объектов), может сэкономить на одной косвенности,
Существуют различные улучшения, которые вы можете внести в базовый алгоритм. Например, использовать что-то вроде std :: deque для основного списка объектов; один дополнительный уровень косвенности, но позволяет вставлять объекты в полный список без аннулирования любых временных указателей, которые вы получили из карты слотов.
Вы также можете избежать сохранения индекса внутри объекта, так как индекс может быть рассчитан по адресу памяти объекта (это - объекты), и даже лучше требуется только при удалении объекта, в этом случае у вас уже есть идентификатор объекта (и, следовательно, индекс) в качестве параметра.
Извинения за рецензию; Я не чувствую, что это самое ясное описание. Уже поздно, и это трудно объяснить, не тратя больше времени, чем я, на примеры кода.
источник
массив фиксированного размера (линейная память)
с внутренним свободным списком (O (1) alloc / free, стабильные индикаторы)
со слабыми ссылочными ключами (повторное использование слота делает недействительным ключ)
нулевые разыменования служебной информации (когда известно-допустимо)
Обрабатывает все: от пуль до монстров, от текстур до частиц и т. Д. Это лучшая структура данных для видеоигр. Я думаю, что это произошло от Bungie (еще во времена марафона / мифов), я узнал об этом в Blizzard, и я думаю, что это было в жемчужинах игрового программирования того времени. Это вероятно во всей игровой индустрии на данный момент.
Q: «Почему бы не использовать динамический массив?» A: Динамические массивы вызывают сбои. Простой пример:
Вы можете представить случаи с более сложными (например, глубокие стеки вызовов). Это верно для всех массивов, таких как контейнеры. При создании игр у нас достаточно понимания нашей проблемы, чтобы заставить размеры и бюджеты на все в обмен на производительность.
И я не могу сказать этого достаточно: действительно, это лучшая вещь когда-либо. (Если вы не согласны, опубликуйте свое лучшее решение! Предостережение - необходимо решить проблемы, перечисленные в верхней части этого поста: линейная память / итерация, O (1) alloc / free, стабильные индексы, слабые ссылки, нулевые издержки или есть удивительная причина, почему вам не нужен один из них;)
источник
DataArray
кажется, что динамическое выделение массива в ctor. Так что в моем понимании это может иметь какой-то иной смысл.На это нет правильного ответа. Все зависит от реализации ваших алгоритмов. Просто выберите тот, который вы считаете лучшим. Не пытайтесь оптимизировать на этой ранней стадии.
Если вы часто удаляете объекты и воссоздаете их, я предлагаю вам посмотреть, как реализованы пулы объектов.
Редактировать: зачем усложнять вещи со слотами, а что нет. Почему бы просто не использовать стопку, не убрать последний элемент и не использовать его повторно? Поэтому, когда вы добавляете один, вы будете делать ++, а когда вы добавляете один, который вы делаете - чтобы отслеживать конечный индекс.
источник
Это зависит от вашей игры. Контейнеры различаются по тому, как быстро осуществляется доступ к определенному элементу, как быстро удаляется элемент и как быстро добавляется элемент.
Обычно вы хотите использовать список, если вы хотите, чтобы ваш список объектов сортировался не так, как в хронологическом порядке, и, следовательно, нужно вставлять новые объекты, а не добавлять, в противном случае - деку. Deque обладает повышенной гибкостью по сравнению с вектором, но на самом деле не имеет большого недостатка.
Если у вас действительно много сущностей, вы должны взглянуть на разделение пространства.
источник