Як я можу безпечно підтримувати зв’язок компонент-об'єкт та зберігати вміст компонентів, кероване сховищем?


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.

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

Як я можу вирішити цю проблему, зберігаючи код і зберігаючи кеш?

Відповіді:


6

Ваша модель спілкування здається прекрасною, а варіант 1 спрацював би нормально, якби тільки ви могли безпечно зберігати ці вказівники. Ви можете вирішити цю проблему, вибравши іншу структуру даних для зберігання компонентів.

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 та спосіб внутрішньої організації пам’яті дуже залежить від того, як ви будете використовувати свої компоненти. Якщо ви перейдете через них залежно від їх типу, ви хочете зберегти компоненти за типом, якщо ви повторите їх на основі їх ігрового об'єкта, вам потрібно буде знайти спосіб переконатися, що вони близькі інший на основі цього, тощо ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.