El título es intencionalmente hiperbólico y puede ser mi inexperiencia con el patrón, pero aquí está mi razonamiento:
La forma "habitual" o posiblemente directa de implementar entidades es implementarlas como objetos y subclasificar el comportamiento común. Esto lleva al clásico problema de "¿es EvilTreeuna subclase de Treeo Enemy?". Si permitimos la herencia múltiple, surge el problema del diamante. En cambio podríamos tirar de la funcionalidad combinada de Treey Enemymás arriba en la jerarquía que conduce a clases Dios, o podemos dejar intencionalmente el comportamiento en nuestros Treey Entityclases (haciéndolos interfaces en el caso extremo), de modo que la EvilTreepuede poner en práctica que en sí - que conduce a duplicación de código si alguna vez tenemos un SomewhatEvilTree.
Los sistemas de componentes de la entidad intentan resolver este problema dividiendo el objeto Treey Enemyen diferentes componentes, por ejemplo Position, Healthy AI, e implementan sistemas, como los AISystemque cambian la posición de una entidad de acuerdo con las decisiones de IA. Hasta ahora todo bien, pero ¿y si EvilTreepuede recoger un powerup y causar daño? Primero necesitamos a CollisionSystemy a DamageSystem(probablemente ya los tengamos). La CollisionSystemnecesidad de comunicarse con el DamageSystem: Cada vez que dos cosas chocan, CollisionSystemenvía un mensaje al DamageSystempara que pueda restar salud. El daño también está influenciado por los potenciadores, por lo que debemos almacenarlo en algún lugar. ¿Creamos un nuevo PowerupComponentque adjuntamos a las entidades? Pero entonces elDamageSystemnecesita saber sobre algo de lo que preferiría no saber nada: después de todo, también hay cosas que infligen daño que no pueden recoger potenciadores (por ejemplo, a Spike). ¿Permitimos PowerupSystemque se modifique un StatComponentque también se usa para cálculos de daños similares a esta respuesta ? Pero ahora dos sistemas acceden a los mismos datos. A medida que nuestro juego se vuelve más complejo, se convertiría en un gráfico de dependencia intangible donde los componentes se comparten entre muchos sistemas. En ese punto, podemos usar variables estáticas globales y deshacernos de toda la repetitiva.
¿Hay una manera efectiva de resolver esto? Una idea que tuve fue dejar que los componentes tengan ciertas funciones, por ejemplo, dar el StatComponent attack()que solo devuelve un número entero por defecto, pero se puede componer cuando ocurre un encendido:
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
Esto no resuelve el problema que attackdebe guardarse en un componente al que acceden varios sistemas, pero al menos podría escribir las funciones correctamente si tengo un lenguaje que lo soporte lo suficiente:
// 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
De esta manera, al menos garantizo el orden correcto de las diversas funciones agregadas por los sistemas. De cualquier manera, parece que me estoy acercando rápidamente a la programación reactiva funcional aquí, así que me pregunto si no debería haber usado eso desde el principio (solo he examinado FRP, por lo que puede estar equivocado aquí). Veo que ECS es una mejora sobre las jerarquías de clase complejas, pero no estoy convencido de que sea ideal.
¿Hay alguna solución para esto? ¿Hay alguna funcionalidad / patrón que me falta para desacoplar ECS de manera más limpia? ¿FRP es estrictamente más adecuado para este problema? ¿Estos problemas simplemente surgen de la complejidad inherente de lo que estoy tratando de programar? es decir, ¿FRP tendría problemas similares?