Заголовок навмисно гіперболічний, і це може бути просто недосвідчення з малюнком, але ось мої міркування:
"Звичайний" або, мабуть, прямолінійний спосіб реалізації суб'єктів - це реалізація їх як об'єктів та підкласи загальної поведінки. Це призводить до класичної проблеми "є 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 подібні проблеми?