Ми починаємо з базового підходу системи-компоненти-сутності .
Створимо збірки (термін, похідний від цієї статті) лише з інформації про типи компонентів . Це робиться динамічно під час виконання, так само, як ми додавали б / видаляли компоненти до об'єкта по черзі, але давайте назвемо його точніше, оскільки мова йде лише про інформацію про тип.
Тоді ми будуємо сутності, задаючи збірку для кожного з них. Після того, як ми створюємо сутність, її збірка стає непорушною, це означає, що ми не можемо безпосередньо змінити її на місці, але все-таки ми можемо отримати підпис існуючої сутності до локальної копії (разом із вмістом), внести належні зміни до неї та створити нову сутність з нього.
Тепер для ключового поняття: кожного разу, коли створена сутність, вона присвоюється об'єкту, який називається актором збірки , що означає, що всі об'єкти одного підпису будуть знаходитися в одному контейнері (наприклад, в std :: vector).
Тепер системи просто перебирають кожне відро, яке їх цікавить, і виконують свою роботу.
Цей підхід має деякі переваги:
- компоненти зберігаються в декількох (точно: кількість відра) суміжних шматок пам’яті - це покращує зручність пам’яті та простіше скинути стан гри
- системи обробляють компоненти лінійно, що означає поліпшену когерентність кешу - словники до побачення та випадкові стрибки пам'яті
- створити нову сутність так само просто, як зіставити збірку на відро і відсунути необхідні компоненти до його вектора
- видалити об'єкт так само просто, як один дзвінок на std :: move, щоб поміняти останній елемент на видалений, тому що замовлення не має значення в цей момент
Якщо у нас є багато організацій з абсолютно різними підписами, переваги когерентності кешу зменшуються, але я не думаю, що це відбудеться в більшості програм.
Існує також проблема з недійсним покажчиком після перерозподілу векторів - це можна вирішити шляхом введення структури на зразок:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
Отже, колись з якоїсь причини в нашій логіці гри ми хочемо відслідковувати новостворену сутність, всередині відра ми реєструємо entitywatcher , і як тільки сутність повинна бути std :: move'd під час видалення, ми шукаємо її спостерігачів та оновлюємо їх real_index_in_vector
до нових цінностей. Здебільшого це накладає лише один пошук словника для кожного видалення сутності.
Чи є якісь недоліки у цього підходу?
Чому рішення ніде не згадується, незважаючи на те, що воно досить очевидне?
EDIT : Я редагую питання, щоб "відповісти на відповіді", оскільки коментарів недостатньо.
ви втрачаєте динамічний характер підключаються компонентів, який був створений спеціально для того, щоб піти від побудови статичного класу.
Я не. Можливо, я не пояснив це досить чітко:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
Це так само просто, як просто взяти підпис існуючої організації, змінити її та знову завантажити як нову сутність. Підключений, динамічний характер ? Звичайно. Тут я хотів би підкреслити, що існує лише один клас "збирання" та один "відро". Відра керуються даними та створюються під час виконання в оптимальній кількості.
вам потрібно буде пройти всі відра, які можуть містити дійсну ціль. Без зовнішньої структури даних виявлення зіткнень може бути однаково важким.
Ну, ось чому ми маємо вищезгадані зовнішні структури даних . Вирішення проблеми настільки ж просто, як введення ітератора до класу System, який визначає, коли перейти до наступного відра. Стрибки будуть чисто прозорими для логіки.