Я б запропонував почати з того, щоб прочитати 3 великих брехні Майка Актона, оскільки ви порушуєте дві з них. Я серйозно, це змінить спосіб оформлення коду: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Отже, що ви порушуєте?
Брехня 3 - Код важливіший за дані
Ви говорите про введення залежності, яке може бути корисним у деяких (і лише деяких) випадках, але завжди повинно дзвонити в великий дзвінок, якщо ви його використовуєте, особливо в розробці ігор! Чому? Тому що це часто непотрібна абстракція. А абстракції в неправильних місцях жахливі. Отже, у вас є гра. У грі є менеджери для різних компонентів. Компоненти всі визначені. Тому складіть клас десь у вашому головному коді циклу ігор, який "має" менеджери. Подібно до:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
Дайте йому кілька функцій отримання для отримання кожного класу менеджерів (getBulletManager ()). Можливо, цей клас сам по собі є Singleton або він доступний з одного (у вас, певно, десь є центральний ігровий синглтон). Немає нічого поганого в чітко визначених жорстких даних і поведінці.
Не створюйте ManagerManager, який дозволяє реєструвати менеджерів за допомогою ключа, який можна отримати за допомогою цього ключа іншими класами, які хочуть використовувати менеджер. Це чудова система і дуже гнучка, але тут говорити про гру. Ви точно знаєте , які системи в грі. Чому робите вигляд, як ні? Тому що це система для людей, які вважають, що код важливіший за дані. Вони скажуть "Код гнучкий, дані просто заповнюють його". Але код - це лише дані. Система, яку я описав, набагато простіша, надійніша, простіша в обслуговуванні та набагато гнучкіша (наприклад, якщо поведінка одного менеджера відрізняється від інших менеджерів, вам доведеться змінити лише кілька рядків замість того, щоб переробляти всю систему)
Брехня №2 - Код повинен бути розроблений навколо моделі світу
Отже, у вас є сутність в ігровому світі. Суб'єкт господарювання має ряд компонентів, що визначають його поведінку. Таким чином, ви створюєте клас Entity зі списком об'єктів Component та функцією Update (), яка викликає функцію Update () кожного компонента. Правильно?
Ні. :) Це розробка навколо моделі світу: у вашій грі є куля, тож ви додаєте клас Bullet. Потім ви оновлюєте кожну кулю і переходите до наступної. Це абсолютно знищить вашу ефективність, і це дасть вам жахливу складну кодову базу з повторюваним кодом скрізь і не має логічної структуризації подібного коду. (Ознайомтеся з моєю відповіддю тут, щоб отримати більш детальне пояснення того, чому традиційний дизайн OO смокче, або шукайте дизайн, орієнтований на дані)
Давайте подивимося на ситуацію без нашої упередженості ОО. Ми хочемо наступного, не менш і менш (зверніть увагу, немає необхідності складати клас для сутності чи об'єкта):
- У вас є купа утворень
- Суб'єкти господарювання складаються з ряду компонентів, які визначають поведінку суб'єкта господарювання
- Ви хочете оновлювати кожен компонент у грі кожен кадр, бажано контрольованим способом
- Окрім ідентифікації компонентів як належних разом, організація сама не повинна робити. Це посилання / ідентифікатор для декількох компонентів.
А давайте розглянемо ситуацію. Ваша компонентна система буде оновлювати поведінку кожного об'єкта в грі, кожен кадр. Це, безумовно, критична система вашого двигуна. Тут важлива продуктивність!
Якщо ви знайомі з архітектурою комп’ютера або з орієнтованим на дані дизайном, ви знаєте, як досягаються найкращі показники: щільно упакована пам'ять та групування виконання коду. Якщо ви виконаєте фрагменти коду A, B і C так: ABCABCABC, ви не отримаєте такої ж продуктивності, як при виконанні його так: AAABBBCCC. Це не лише тому, що кеш інструкцій та даних буде використовуватися ефективніше, а й тому, що якщо ви виконайте всі "A" один за одним, є багато місця для оптимізації: видалення дублікату коду, попередній розрахунок даних, якими користується всі "А" і т.д.
Отже, якщо ми хочемо оновити всі компоненти, не давайте робити їх класами / об’єктами з функцією оновлення. Не будемо називати цю функцію оновлення для кожного компонента в кожній сутності. Це рішення "ABCABCABC". Згрупуємо всі однакові оновлення компонентів разом. Тоді ми можемо оновити всі A-компоненти, а потім B та ін. Що нам потрібно для цього?
По-перше, нам потрібні менеджери компонентів. Для кожного типу компонентів у грі нам потрібен клас менеджера. Він має функцію оновлення, яка буде оновлювати всі компоненти цього типу. Він має функцію створення, яка додасть новий компонент цього типу та функцію видалення, яка знищить вказаний компонент. Можуть бути й інші допоміжні функції для отримання та встановлення даних, характерних для цього компонента (наприклад: встановлення 3D-моделі для компонентної моделі). Зауважте, що менеджер певним чином являє собою чорний ящик для зовнішнього світу. Ми не знаємо, як зберігаються дані кожного компонента. Ми не знаємо, як оновлюється кожен компонент. Нам все одно, доки компоненти поводяться як слід.
Далі нам потрібна сутність. Ви могли б зробити це класом, але це навряд чи потрібно. Суб'єкт може бути не що інше, як унікальний цілий ідентифікатор або штриховий рядок (так само ціле число). Коли ви створюєте компонент для Entity, ви передаєте ідентифікатор як аргумент менеджеру. Коли ви хочете видалити компонент, ви знову передаєте ідентифікатор. Можуть бути деякі переваги в тому, що додавати до Субстанції трохи більше даних, а не просто робити їх ідентифікатором, але це будуть лише допоміжні функції, оскільки, як я перераховував у вимогах, все поведінка сутності визначається самими компонентами. Це ваш двигун, тому робіть те, що має для вас сенс.
Що нам потрібно - це менеджер суб'єктів господарювання. Цей клас або генерує унікальні ідентифікатори, якщо ви використовуєте рішення, призначене лише для ідентифікації, або його можна використовувати для створення / управління об'єктами Entity. Він також може зберігати список усіх об'єктів у грі, якщо вам це потрібно. Entity Manager може бути центральним класом вашої компонентної системи, зберігаючи посилання на всіх ComponentManagers у вашій грі та викликаючи їх функції оновлення в потрібному порядку. Таким чином, весь цикл ігор повинен зробити, це викликати EntityManager.update (), і вся система чудово відокремлена від решти вашого двигуна.
Ось такий погляд з пташиного польоту, давайте подивимось, як працюють менеджери компонентів. Ось що вам потрібно:
- Створення компонентних даних під час виклику create (entitidID)
- Видалити дані компонента при виклику видалення (EntityID)
- Оновити всі дані (застосовно) компонентів, коли викликається update () (тобто не всі компоненти потребують оновлення кожного кадру)
Останній - це визначення поведінки / логіки компонентів і повністю залежить від типу компонента, який ви пишете. AnimationComponent оновлює дані анімації на основі кадру, в якому знаходиться. DragableComponent оновить лише компонент, який перетягується мишкою. PhysicsComponent буде оновлювати дані у фізичній системі. Тим не менше, оскільки ви оновлюєте всі компоненти одного типу за один раз, ви можете зробити деякі оптимізації, які неможливі, коли кожен компонент є окремим об'єктом з функцією оновлення, яку можна було викликати в будь-який час.
Зауважте, що я досі ніколи не закликав створити клас XxxComponent для зберігання компонентних даних. Це залежить від вас. Вам подобається дизайн орієнтований на дані? Потім структуруйте дані в окремі масиви для кожної змінної. Вам подобається об'єктно-орієнтований дизайн? (Я б не рекомендував це, він все одно знищить вашу ефективність у багатьох місцях) Потім створіть об’єкт XxxComponent, який буде зберігати дані кожного компонента.
Чудова річ у менеджерах - інкапсуляція. Зараз інкапсуляція - одна з найбільш жахливо зловживаних філософій у світі програмування. Ось як це слід використовувати. Тільки менеджер знає, які дані компонентів зберігаються де, як працює логіка компонента. Існує кілька функцій для отримання / встановлення даних, але це все. Ви можете переписати весь менеджер та його основні класи, а якщо не змінити загальнодоступний інтерфейс, ніхто навіть не помітить. Змінили двигун фізики? Просто перепишіть PhysicsComponentManager і закінчите.
Тоді є ще одне останнє: спілкування та обмін даними між компонентами. Зараз це складно, і не існує рішення, яке відповідає всім розмірам. Ви можете створити функції get / set у менеджерах, щоб, наприклад, компонент зіткнення отримав позицію з компонента позиції (тобто PositionManager.getPosition (entitid)). Ви можете використовувати систему подій. Ви можете зберігати деякі спільні дані в суті (найгірше рішення на мою думку). Ви можете використовувати (це часто використовується) система обміну повідомленнями. Або використовувати комбінацію декількох систем! У мене немає часу та досвіду, щоб зайнятися кожною з цих систем, але пошук у Google та stackoverflow - ваші друзі.