Название намеренно гиперболично, и это может быть просто моя неопытность с шаблоном, но вот мои рассуждения:
«Обычный» или, возможно, простой способ реализации сущностей состоит в том, чтобы реализовать их как объекты и создать подклассы общего поведения. Это приводит к классической проблеме «является EvilTreeподклассом Treeили Enemy?». Если мы разрешаем множественное наследование, возникает проблема с алмазом. Вместо этого мы могли бы использовать объединенную функциональность иерархии, которая ведет к классам Бога, Treeи Enemyдалее продвигаться вверх, или мы можем намеренно исключить поведение в наших классах Treeи Entityклассах (делая их интерфейсами в крайнем случае), чтобы они EvilTreeмогли реализовать это сами - что приводит к дублирования кода , если мы когда - либо иметь SomewhatEvilTree.
Entity-компонентные системы пытаются решить эту проблему путем деления Treeи Enemyобъекта на различные компоненты - скажем Position, Healthи AI- и внедрить системы, такие как , AISystemчто изменяет положение Entitiy давал в соответствии с решениями AI. Пока все хорошо, но что, если EvilTreeможно подобрать бонусы и нанести урон? Сначала нам нужны a CollisionSystemи a DamageSystem(они, вероятно, уже есть). В CollisionSystemпотребности общаться с DamageSystem: Каждый раз , когда две вещи столкнуть CollisionSystemпосылает сообщение DamageSystemздоровья , чтобы он мог вычитать. Повреждения также зависят от бонусов, поэтому мы должны их где-то хранить. Создаем ли мы новое, PowerupComponentкоторое мы прикрепляем к сущностям? Но тогдаDamageSystemему нужно знать о чем-то, о чем он скорее ничего не знает - в конце концов, есть вещи, которые наносят урон, которые не могут поднять бонусы (например, а Spike). Разрешаем ли мы PowerupSystemмодифицировать объект, 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 похожие проблемы?