Как я могу безопасно поддерживать связь между компонентами и объектами с помощью кеш-памяти?

9

Я создаю игру, в которой используются игровые объекты на основе компонентов, и мне трудно реализовать способ взаимодействия каждого компонента со своим игровым объектом. Вместо того, чтобы объяснять все сразу, я объясню каждую часть соответствующего примера кода:

class GameObjectManager {
    public:
        //Updates all the game objects
        void update(Time dt);

        //Sends a message to all game objects
        void sendMessage(Message m);

    private:
        //Vector of all the game objects
        std::vector<GameObject> gameObjects;

        //vectors of the different types of components
        std::vector<InputComponent> input;
        std::vector<PhysicsComponent> ai;
        ...
        std::vector<RenderComponent> render;
}

GameObjectManagerСодержит все игровые объекты и их компоненты. Он также отвечает за обновление игровых объектов. Это достигается путем обновления векторов компонентов в определенном порядке. Я использую векторы вместо массивов, чтобы практически не было ограничений на количество игровых объектов, которые могут существовать одновременно.

class GameObject {
    public:
        //Sends a message to the components in this game object
        void sendMessage(Message m);

    private:
        //id to keep track of components in the manager
        const int id;

        //Pointers to components in the game object manager
        std::vector<Component*> components;
}

GameObjectКласс знает , что его компоненты и отправлять им сообщения.

class Component {
    public:
        //Receives messages and acts accordingly
        virtual void handleMessage(Message m) = 0;

        virtual void update(Time dt) = 0;

    protected:
        //Calls GameObject's sendMessage
        void sendMessageToObject(Message m);

        //Calls GameObjectManager's sendMessage
        void sendMessageToWorld(Message m);
}

ComponentКласс чисто виртуальные , так что классы для различных типов компонентов могут осуществлять обработку сообщений и обновление. Он также может отправлять сообщения.

Теперь возникает проблема того, как компоненты могут вызывать sendMessageфункции в GameObjectи GameObjectManager. Я придумал два возможных решения:

  1. Дайте Componentуказатель на свой GameObject.

Однако, поскольку игровые объекты находятся в векторе, указатели могут быстро стать недействительными (то же самое можно сказать и о векторе GameObject, но, надеюсь, решение этой проблемы также может решить эту проблему). Я мог бы поместить игровые объекты в массив, но тогда мне пришлось бы передавать произвольное число для размера, который мог бы быть слишком большим и тратить память.

  1. Дайте Componentуказатель на GameObjectManager.

Однако я не хочу, чтобы компоненты могли вызывать функцию обновления менеджера. Я единственный человек, работающий над этим проектом, но я не хочу иметь привычку писать потенциально опасный код.

Как я могу решить эту проблему, сохраняя мой код безопасным и дружественным к кешу?

AlecM
источник

Ответы:

6

Кажется, ваша модель связи в порядке, и первый вариант будет работать нормально, если только вы сможете безопасно хранить эти указатели. Вы можете решить эту проблему, выбрав другую структуру данных для хранения компонентов.

А std::vector<T>был разумным первым выбором. Однако поведение аннулирования итератора контейнера является проблемой. То, что вам нужно, - это структура данных, которая является быстрой и когерентной для итерации, и которая также сохраняет стабильность итератора при вставке или удалении элементов.

Вы можете построить такую ​​структуру данных. Он состоит из связанного списка страниц . Каждая страница имеет фиксированную емкость и содержит все свои элементы в одном массиве. Счетчик используется для указания количества активных элементов в этом массиве. Страница также имеет свободный список (позволяет повторного использованию вырубленных записей) и список пропуска ( что позволяет пропускать через освобоженные записи во время итерации.

Другими словами, концептуально что-то вроде:

struct Page {
   int count;
   int capacity;           // Optional if every page is a fixed size.
   T * m_storage;
   bool * m_skip;          // Skip list; can be bit-compressed.
   std::stack<int> m_free; // Can be replaced with a specialized stack.

   Page * next;
   Page * prior;           // Optional, allows reverse iteration
};

Я безо всякого воображения называю эту структуру данных книгой (потому что это набор страниц, которые вы перебираете), но у структуры есть и другие имена.

Мэтью Бентли называет это "колонией". Реализация Мэтью использует поле пропуска подсчета прыжков (извинения за ссылку MediaFire, но именно так Бентли сам размещает документ), которое превосходит более типичный список пропусков на основе логических значений в подобных структурах. Библиотека Bentley предназначена только для заголовков и может быть легко вставлена ​​в любой проект C ++, поэтому я бы посоветовал вам просто использовать это вместо создания собственного. Здесь много тонкостей и оптимизаций, о которых я здесь говорю.

Поскольку эта структура данных никогда не перемещает элементы после их добавления, указатели и итераторы на этот элемент остаются действительными до тех пор, пока сам этот элемент не будет удален (или сам контейнер не будет очищен). Поскольку в нем хранятся фрагменты смежно распределенных элементов, итерация выполняется быстро и в основном согласована с кэшем. Вставка и удаление являются разумными.

Это не идеально; можно нарушить когерентность кэша с помощью шаблона использования, который включает в себя значительное удаление из фактически случайных точек в контейнере, а затем итерацию по этому контейнеру до того, как последующие вставки будут заполнены элементами. Если вы часто находитесь в этом сценарии, вы будете пропускать потенциально большие области памяти за раз. Однако на практике я считаю этот контейнер разумным выбором для вашего сценария.

Другие подходы, которые я оставлю для других ответов для рассмотрения, могут включать подход на основе дескриптора или структуру типа карты слотов (где у вас есть ассоциативный массив целочисленных «ключей» для целочисленных «значений», значениями являются индексы в резервном массиве, который позволяет перебирать вектор, все еще получая доступ по «индексу» с некоторой дополнительной косвенностью).


источник
Привет! Есть ли какие-нибудь ресурсы, где я могу узнать больше об альтернативах "колонии", которые вы упомянули в предыдущем абзаце? Они где-нибудь реализованы? Я изучал эту тему в течение некоторого времени, и мне действительно интересно.
Ринат Велиахмедов
5

Быть «дружественным кешу» - это проблема больших игр . Мне кажется, это преждевременная оптимизация.


Один из способов решить эту проблему, не будучи «дружественным к кешу», - создать объект в куче, а не в стеке: использовать newи (умные) указатели для ваших объектов. Таким образом, вы сможете ссылаться на ваши объекты, и их ссылка не будет признана недействительной.

Для более дружественного к кешу решения вы могли бы самостоятельно управлять выделением / распределением объектов и использовать дескрипторы этих объектов.

По сути, при инициализации вашей программы объект резервирует кусок памяти в куче (назовем его MemMan), затем, когда вы хотите создать компонент, вы сообщаете MemMan, что вам нужен компонент размера X, он ' Я зарезервирую его для вас, создам дескриптор и сохраню внутренне, где в его расположении находится объект для этого дескриптора. Он вернет дескриптор, и это единственное, что вы сохраните в объекте, никогда не указатель на его местоположение в памяти.

Когда вам нужен компонент, вы попросите MemMan получить доступ к этому объекту, что он с радостью сделает. Но не держите ссылку на это, потому что ....

Одна из задач MemMan - хранить объекты в памяти рядом друг с другом. Раз в несколько игровых кадров вы можете указать MemMan переставлять объекты в памяти (или он может делать это автоматически при создании / удалении объектов). Он обновит свою карту расположения маркера в память. Ваши дескрипторы всегда будут действительны, но если вы сохранили ссылку на пространство памяти ( указатель или ссылку ), вы найдете только отчаяние и запустение.

В учебниках говорится, что такой способ управления памятью имеет как минимум 2 преимущества:

  1. меньше кешей, потому что объекты находятся в памяти рядом друг с другом и
  2. это уменьшает количество обращений к памяти, которые вы делаете в ОС, которые, как говорят, занимают некоторое время.

Имейте в виду, что то, как вы используете MemMan и как вы будете организовывать внутреннюю память, действительно зависит от того, как вы будете использовать свои компоненты. Если вы будете перебирать их в зависимости от их типа, вы захотите сохранить компоненты по типу, если вы перебираете их в зависимости от их игрового объекта, вам нужно будет найти способ убедиться, что они близки к одному. другой, основанный на этом, и т.д ...

Vaillancourt
источник