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 EvilTree
una subclase de Tree
o Enemy
?". Si permitimos la herencia múltiple, surge el problema del diamante. En cambio podríamos tirar de la funcionalidad combinada de Tree
y Enemy
más arriba en la jerarquía que conduce a clases Dios, o podemos dejar intencionalmente el comportamiento en nuestros Tree
y Entity
clases (haciéndolos interfaces en el caso extremo), de modo que la EvilTree
puede 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 Tree
y Enemy
en diferentes componentes, por ejemplo Position
, Health
y AI
, e implementan sistemas, como los AISystem
que cambian la posición de una entidad de acuerdo con las decisiones de IA. Hasta ahora todo bien, pero ¿y si EvilTree
puede recoger un powerup y causar daño? Primero necesitamos a CollisionSystem
y a DamageSystem
(probablemente ya los tengamos). La CollisionSystem
necesidad de comunicarse con el DamageSystem
: Cada vez que dos cosas chocan, CollisionSystem
envía un mensaje al DamageSystem
para 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 PowerupComponent
que adjuntamos a las entidades? Pero entonces elDamageSystem
necesita 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 PowerupSystem
que se modifique un StatComponent
que 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 attack
debe 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?
fuente
Respuestas:
ECS arruina completamente la ocultación de datos. Esta es una compensación del patrón.
ECS es excelente para desacoplar. Un buen ECS permite que un sistema de movimiento declare que funciona en cualquier entidad que tenga un componente de velocidad y posición, sin tener que preocuparse por qué tipos de entidad existen o qué otros sistemas acceden a estos componentes. Esto es al menos equivalente en el poder de desacoplamiento para que los objetos del juego implementen ciertas interfaces.
Dos sistemas que acceden a los mismos componentes es una característica, no un problema. Es totalmente esperado, y no combina sistemas de ninguna manera. Es cierto que los sistemas tendrán un gráfico de dependencia implícito, pero esas dependencias son inherentes al mundo modelado. Decir que el sistema de daños no debería tener la dependencia implícita del sistema de encendido es afirmar que los poderes no afectan el daño, y eso probablemente sea incorrecto. Sin embargo, aunque existe la dependencia, los sistemas no están acoplados : puede eliminar el sistema de encendido del juego sin afectar el sistema de daños, porque la comunicación se realizó a través del componente de estadísticas y fue completamente implícita.
La resolución de estas dependencias y sistemas de pedidos se puede hacer en una única ubicación central, similar a cómo funciona la resolución de dependencias en un sistema DI. Sí, un juego complejo tendrá un gráfico complejo de sistemas, pero esta complejidad es inherente, y al menos está contenida.
fuente
Casi no hay forma de evitar el hecho de que un sistema necesita acceder a múltiples componentes. Para que algo como un VelocitySystem funcione, probablemente necesitará acceso a VelocityComponent y PositionComponent. Mientras tanto, RenderingSystem también necesita acceder a estos datos. No importa lo que haga, en algún momento el sistema de renderizado necesita saber dónde renderizar el objeto y VelocitySystem necesita saber dónde mover el objeto.
Lo que necesita para esto es la explicidad de las dependencias. Cada sistema debe ser explícito sobre qué datos leerá y en qué datos escribirá. Cuando un sistema desea obtener un componente en particular, debe poder hacerlo solo de manera explícita . En su forma más simple, simplemente tiene los componentes para cada tipo que requiere (por ejemplo, RenderSystem necesita RenderComponents y PositionComponents) como sus argumentos y devuelve lo que haya cambiado (por ejemplo, RenderComponents).
Puede ordenar en tal diseño. Nada dice que para ECS sus sistemas deben ser independientes del orden o algo así.
El uso de este diseño de sistema de componente de entidad y FRP no es mutuamente exclusivo. De hecho, los sistemas pueden verse como nada más que sin estado, simplemente realizando transformaciones de datos (los componentes).
FRP no resolvería el problema de tener que usar la información que necesita para realizar alguna operación.
fuente