Entity Component System
Традиційні ігрові архітектури використовують ієрархії успадкування, які погано масштабуються й неефективно працюють із кешем процесора. ECS перевертає підхід: замість об'єктів, що мають поведінку, ми маємо таблиці даних (компоненти), які перетворюють функції (системи). Результат може бути в 10–100 разів швидшим для великих світів.
1. Проблема ООП
Класичні ієрархії ігрових об'єктів виглядають так: Entity → Actor → Pawn → Character → FPSCharacter. Проблеми за масштабування:
- Ромбоподібне успадкування та крихкі ієрархії руйнуються, коли потрібен «NPC-привид» (твердий? видимий? зі ШІ? зі звуком?)
- Промахи кешу: ітерування 10 000 сутностей для оновлення фізики означає перехід за вказівниками до розкиданих ділянок RAM. Сучасні процесори простоюють, чекаючи заповнення кешу.
- Зв'язаність функцій: додавання нової можливості потребує зміни базових класів або додавання віртуальних функцій, за які платять усі підкласи.
Підхід ООП
- GameObject з усіма можливими полями
- Віртуальні методи update() для кожного типу
- Успадкування для «видів» об'єктів
- Виділення пам'яті для кожного об'єкта з переходами за вказівниками
- Можливість = підклас або прапорець
Підхід ECS
- Сутність = просто ідентифікатор (ціле число)
- Компоненти = чисті структури даних
- Системи = функції над наборами компонентів
- Щільне масивне сховище для кожного типу компонента
- Можливість = додати/видалити компонент
2. Основні поняття
- Сутність (Entity): унікальний цілочисловий ідентифікатор (64-бітний). Не містить даних. Слугує ключем для пошуку пов'язаних компонентів.
- Компонент (Component): проста структура даних (без методів, без віртуальних функцій). Приклади:
Position{x,y,z},Velocity{dx,dy,dz},Health{value, max}. - Система (System): функція, що запитує сутності з певним набором компонентів і перетворює їх. Приклад: «для всіх сутностей із Position і Velocity оновити position += velocity * dt».
- Світ (World): контейнер, що керує ідентифікаторами сутностей, сховищем компонентів і плануванням систем.
3. Сховище архетипів
Домінівна стратегія зберігання — архетипи: групувати сутності, що мають точно однаковий набір компонентів, у суміжні масиви. Архетип визначається своїм набором компонентів (наприклад, {Position, Velocity, Health} → архетип A).
Усі компоненти сутностей у архетипі зберігаються у щільних масивах. Ітерація системи = послідовний доступ до пам'яті → чудова продуктивність кешу. Додавання компонента до сутності переміщує її до іншого архетипу.
Альтернатива — розріджені множини (sparse sets): для кожного типу компонента підтримується відображення розріджений→щільний. Краще для дуже великих просторів ідентифікаторів сутностей або частих змін архетипів. Flecs використовує архетипи; bitECS використовує розріджені множини; обидва можуть бути дружніми до кешу.
4. Системи та запити
Система оголошує запит: обов'язкові, необов'язкові та виключені типи компонентів. Рушій ECS повертає відповідні сутності. Системи можуть бути:
- Послідовними: PhysicsSystem, RenderSystem, AISystem виконуються по черзі за кадр.
- Паралельними: системи з наборами компонентів, що не перетинаються, можуть виконуватися паралельно (аналіз залежностей даних). Це критично для системи завдань DOTS/Unity.
- Реактивними: спрацьовують при додаванні/видаленні компонентів (корисно для логіки ініціалізації/очищення).
5. Ефективність кешу
Сучасні процесори у 10–100 разів швидші за затримку доступу до RAM. Рядок кешу — 64 байти. Випадкові переходи за вказівниками вбивають продуктивність; послідовний доступ до масиву максимізує повторне використання рядка кешу.
- Розкладка SoA (структура масивів): кожен тип компонента — окремий масив. Ітерація лише по Position[]: читає Position[0..N-1] послідовно — ідеально. Velocity повністю ігнорується (не зачіпається).
- Розкладка AoS (масив структур): кожен елемент містить усі компоненти. Ітерація лише по Position означає зачіпання кожного компонента — марна пропускна здатність.
Архетиповий ECS природно дає SoA на кожен компонент у кожному архетипі. Проста фізична система, що ітерує 100 000 сутностей, може виконатися за ~1 мс (дружньо до кешу) проти ~10 мс із ООП-об'єктами з переходами за вказівниками.
6. Зв'язки та теги
Сучасні фреймворки ECS виходять за межі плоских компонентів:
- Теги: компоненти нульового розміру, що використовуються для фільтрації.
addComponent(eid, IsEnemy). Запит [Position, IsEnemy] знаходить лише ворожі сутності. - Зв'язки: пари (відношення, ціль).
ChildOf(parent),LikedBy(friend). Уможливлюють ієрархії, просторове індексування та семантичні графи без успадкування. - Префаби: шаблони сутностей. Успадковують компоненти від сутності-шаблону; перевизначають окремі поля.
7. Реалізації
- Flecs (C/C++): повнофункціональний, багата мова запитів, зв'язки, скриптинг. Використовується в AAA-іграх. Відкритий код (MIT).
- Unity DOTS / Entities: архетиповий ECS, інтегрований із системою завдань C# Unity та компілятором Burst. Необхідний для 10 000+ динамічних об'єктів.
- EnTT (C++): лише заголовкові файли, сховище на розріджених множинах, широко вживаний в інді- та середньорівневих іграх. Простий API.
- bitECS (JavaScript): розріджені множини на основі TypedArray, мінімальний API. Використовується в браузерних іграх і проєктах на Three.js.
- Bevy ECS (Rust): архетиповий, безпека запитів на етапі компіляції, вбудоване паралельне планування. Ядро ігрового рушія Bevy.