Заголовок навмисно гіперболічний, і це може бути просто недосвідчення з малюнком, але ось мої міркування:
"Звичайний" або, мабуть, прямолінійний спосіб реалізації суб'єктів - це реалізація їх як об'єктів та підкласи загальної поведінки. Це призводить до класичної проблеми "є EvilTree
підкласом Tree
або Enemy
?". Якщо дозволити багаторазове успадкування, виникає алмазна проблема. Ми могли б замість цього витягнути комбіновану функціональність Tree
і Enemy
вдосконалити ієрархію, що веде до класів Бога, або ми можемо навмисно залишити поведінку в наших Tree
і Entity
класах (створюючи їх інтерфейси в крайньому випадку), щоб EvilTree
міг реалізувати саме це - що призводить до дублювання коду, якщо у нас коли-небудь є SomewhatEvilTree
.
Entity-Component Systems намагаються вирішити цю проблему, розділивши Tree
і Enemy
об'єкт на різні компоненти - скажімо Position
, Health
та AI
- та впровадивши такі системи, як, наприклад, AISystem
що змінює позицію Entitiy відповідно до рішень про інтелектуальне управління. Поки що добре, але що робити, якщо EvilTree
можна забрати живлення та завдати шкоди? Спочатку нам потрібні a CollisionSystem
і a DamageSystem
(ми, мабуть, їх уже маємо). В CollisionSystem
потреби спілкуватися з DamageSystem
: Кожен раз , коли дві речі зіштовхнути CollisionSystem
посилає повідомлення DamageSystem
здоров'я , щоб він міг вичитати. На пошкодження також впливають живлення, тому нам потрібно зберігати їх десь. Чи створюємо ми нове, PowerupComponent
яке ми приєднуємо до сутностей? Але тодіDamageSystem
потрібно знати про щось, про що він швидше нічого не знає - зрештою, є також речі, які завдають шкоди, які не можуть забрати бонуси (наприклад, a Spike
). Чи дозволяємо ми PowerupSystem
модифікувати a, StatComponent
який також використовується для розрахунків пошкоджень, подібних до цієї відповіді ? Але тепер дві системи мають доступ до одних і тих же даних. У міру того, як наша гра стає складнішою, вона стала би нематеріальною графіком залежності, де компоненти поділяються між багатьма системами. У цей момент ми можемо просто використовувати глобальні статичні змінні та позбутися всіх котлів.
Чи є ефективний спосіб вирішити це? Однією з ідей у мене було дозволити компонентам виконувати певні функції, наприклад, дати ті, StatComponent
attack()
які просто повертають ціле число за замовчуванням, але вони можуть бути складені, коли відбувається живлення:
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
Це не вирішує проблему, яка attack
повинна бути збережена в компоненті, до якого мають доступ декілька систем, але принаймні я можу правильно ввести функції, якщо у мене є мова, яка достатньо підтримує її:
// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup
// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage
// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity
Таким чином я гарантую принаймні правильне впорядкування різних функцій, що додаються системами. Так чи інакше, здається, що я швидко наближаюсь до функціонального реактивного програмування, тому я запитую себе, чи не слід було б це використовувати з самого початку (я тільки що заглянув у FRP, тому я можу помилятися тут). Я бачу, що ECS - це покращення порівняно зі складною ієрархією класів, але я не переконаний, що це ідеально.
Чи є рішення щодо цього? Чи є функціональність / зразок, який мені не вистачає для більш чіткого відключення ECS? FRP просто суворо підходить для цієї проблеми? Чи виникають ці проблеми лише з притаманної складності того, що я намагаюся запрограмувати; тобто чи мали б FRP подібні проблеми?