OOP ECS vs Pure ECS

11

En primer lugar, soy consciente de que esta pregunta se vincula con el tema del desarrollo de juegos, pero he decidido preguntarla aquí, ya que realmente se trata de un problema de ingeniería de software más general.

Durante el mes pasado, he leído mucho sobre Entity-Component-Systems y ahora estoy bastante cómodo con el concepto. Sin embargo, hay un aspecto al que parece faltarle una 'definición' clara y diferentes artículos han sugerido soluciones radicalmente diferentes:

Esta es la cuestión de si un ECS debe romper la encapsulación o no. En otras palabras, es el ECS de estilo OOP (los componentes son objetos con estado y comportamiento que encapsulan los datos específicos para ellos) frente al ECS puro (los componentes son estructuras de estilo c que solo tienen datos públicos y los sistemas proporcionan la funcionalidad).

Tenga en cuenta que estoy desarrollando un Framework / API / Engine. Por lo tanto, el objetivo es que cualquiera que lo esté utilizando pueda extenderlo fácilmente. Esto incluye cosas como agregar un nuevo tipo de renderizado o componente de colisión.

Problemas con el enfoque OOP

  • Los componentes deben acceder a los datos de otros componentes. Por ejemplo, el método de dibujo del componente de representación debe acceder a la posición del componente de transformación. Esto crea dependencias en el código.

  • Los componentes pueden ser polimórficos, lo que introduce aún más complejidad. Por ejemplo, puede haber un componente de representación de sprites que anule el método de dibujo virtual del componente de representación.

Problemas con el enfoque puro

  • Dado que el comportamiento polimórfico (por ejemplo, para el renderizado) debe implementarse en algún lugar, simplemente se subcontrata en los sistemas. (por ejemplo, el sistema de representación de sprites crea un nodo de representación de sprites que hereda el nodo de representación y lo agrega al motor de representación)

  • La comunicación entre sistemas puede ser difícil de evitar. Por ejemplo, el sistema de colisión puede necesitar el cuadro delimitador que se calcula a partir de cualquier componente de procesamiento de hormigón que haya. Esto se puede resolver permitiéndoles comunicarse a través de datos. Sin embargo, esto elimina las actualizaciones instantáneas, ya que el sistema de procesamiento actualizaría el componente del cuadro delimitador y el sistema de colisión lo usaría. Esto puede conducir a problemas previos si el orden de llamar a las funciones de actualización del sistema no está definido. Existe un sistema de eventos que permite que los sistemas generen eventos a los que otros sistemas pueden suscribir sus controladores. Sin embargo, esto solo funciona para decirle a los sistemas qué hacer, es decir, funciones nulas.

  • Se necesitan banderas adicionales. Tome un componente de mapa de mosaico, por ejemplo. Tendría un tamaño, un tamaño de mosaico y un campo de lista de índice. El sistema de mapa de mosaico manejaría la matriz de vértices respectiva y asignaría las coordenadas de textura en función de los datos del componente. Sin embargo, volver a calcular el mosaico completo de cada cuadro es costoso. Por lo tanto, se necesitaría una lista para realizar un seguimiento de todos los cambios realizados para luego actualizarlos en el sistema. En la forma OOP, esto podría ser encapsulado por el componente de mapa de mosaico. Por ejemplo, el método SetTile () actualizaría la matriz de vértices cada vez que se llame.

Aunque veo la belleza del enfoque puro, realmente no entiendo qué tipo de beneficios concretos tendría sobre una POO más tradicional. Las dependencias entre los componentes aún existen, aunque están ocultas en los sistemas. También necesitaría muchas más clases para lograr el mismo objetivo. Esto me parece una solución un tanto sobredimensionada que nunca es algo bueno.

Además, no estoy tan interesado en el rendimiento, por lo que toda esta idea de diseño orientado a datos y fallas de efectivo no me importa realmente. Solo quiero una bonita arquitectura ^^

Aún así, la mayoría de los artículos y discusiones que leí sugieren el segundo enfoque. ¿POR QUÉ?

Animación

Por último, quiero hacer la pregunta de cómo manejaría la animación en un ECS puro. Actualmente he definido una animación como un functor que manipula una entidad basada en algún progreso entre 0 y 1. El componente de animación tiene una lista de animadores que tiene una lista de animaciones. En su función de actualización, aplica las animaciones que estén activas actualmente en la entidad.

Nota:

Acabo de leer esta publicación ¿El objeto de arquitectura del sistema de componentes de la entidad está orientado por definición? lo que explica el problema un poco mejor que yo. Aunque básicamente se trata del mismo tema, todavía no da ninguna respuesta sobre por qué el enfoque de datos puros es mejor.

Adrian Koch
fuente
1
Quizás una pregunta simple pero seria: ¿conoce las ventajas / desventajas de los ECS? Eso explica principalmente el "por qué".
Caramiriel
Bueno, entiendo la ventaja de usar componentes, es decir, composición en lugar de herencia para evitar el diamante de la muerte a través de la herencia múltiple, etc. El uso de componentes también permite manipular el comportamiento en tiempo de ejecución. Y son modulares. Lo que no entiendo es por qué se desea dividir datos y funciones. Mi implementación actual está en github github.com/AdrianKoch3010/MarsBaseProject
Adrian Koch
Bueno, no tengo suficiente experiencia con ECS para agregar una respuesta completa. Pero la composición no solo se usa para evitar el DoD; También puede crear entidades (únicas) en tiempo de ejecución que son difíciles de generar utilizando un enfoque OO. Dicho esto, dividir datos / procedimientos permite que los datos sean más fáciles de razonar. Puede implementar la serialización, guardar el estado, deshacer / rehacer y cosas así de una manera fácil. Dado que es fácil razonar sobre los datos, también es más fácil optimizarlos. Lo más probable es que pueda dividir las entidades en lotes (subprocesos múltiples) o incluso descargarlo en otro hardware para alcanzar su máximo potencial.
Caramiriel
"Puede haber un componente de renderizado de sprites que anule el método de dibujo virtual del componente de renderizado". Yo diría que ya no es ECS si lo hace / lo requiere.
wondra

Respuestas:

10

Esta es una pregunta difícil. Solo intentaré abordar algunas de las preguntas basadas en mis experiencias particulares (YMMV):

Los componentes deben acceder a los datos de otros componentes. Por ejemplo, el método de dibujo del componente de representación debe acceder a la posición del componente de transformación. Esto crea dependencias en el código.

No subestimes la cantidad y la complejidad (no el grado) de acoplamiento / dependencias aquí. Podrías mirar la diferencia entre esto (y este diagrama ya está ridículamente simplificado a niveles de juguete, y el ejemplo del mundo real tendría interfaces intermedias para aflojar el acoplamiento):

ingrese la descripción de la imagen aquí

... y esto:

ingrese la descripción de la imagen aquí

... o esto:

ingrese la descripción de la imagen aquí

Los componentes pueden ser polimórficos, lo que introduce aún más complejidad. Por ejemplo, puede haber un componente de representación de sprites que anule el método de dibujo virtual del componente de representación.

¿Entonces? El equivalente analógico (o literal) de un despacho virtual y virtual se puede invocar a través del sistema en lugar de que el objeto oculte su estado / datos subyacentes. El polimorfismo sigue siendo muy práctico y factible con la implementación "pura" de ECS cuando la vtable analógica o los punteros de función se convierten en "datos" para que el sistema los invoque.

Dado que el comportamiento polimórfico (por ejemplo, para el renderizado) debe implementarse en algún lugar, simplemente se subcontrata en los sistemas. (por ejemplo, el sistema de representación de sprites crea un nodo de representación de sprites que hereda el nodo de representación y lo agrega al motor de representación)

¿Entonces? Espero que esto no salga como sarcasmo (no es mi intención, aunque a menudo me han acusado de ello, pero desearía poder comunicar mejor las emociones a través del texto), pero el comportamiento polimórfico de "tercerización" en este caso no implica necesariamente un adicional costo a la productividad.

La comunicación entre sistemas puede ser difícil de evitar. Por ejemplo, el sistema de colisión puede necesitar el cuadro delimitador que se calcula a partir de cualquier componente de procesamiento de hormigón que haya.

Este ejemplo me parece particularmente extraño. No sé por qué un renderizador devolvería datos a la escena (generalmente considero que los renderizadores son de solo lectura en este contexto), o para que un renderizador descubra AABB en lugar de algún otro sistema para hacer esto tanto para el renderizador como para el renderizador. colisión / física (podría estar colgado en el nombre del "componente de representación" aquí). Sin embargo, no quiero obsesionarme demasiado con este ejemplo, ya que me doy cuenta de que ese no es el punto que estás tratando de hacer. Aún así, la comunicación entre sistemas (incluso en la forma indirecta de lectura / escritura en la base de datos central de ECS con sistemas que dependen bastante directamente de las transformaciones realizadas por otros) no debería ser frecuente, si es necesario. Ese'

Esto puede llevar a problemas previos si el orden de invocar las funciones de actualización del sistema no está definido.

Esto absolutamente debe ser definido. El ECS no es la solución final para reorganizar el orden de evaluación del procesamiento del sistema de cada sistema posible en la base de código y obtener exactamente el mismo tipo de resultados para el usuario final que trata con marcos y FPS. Esta es una de las cosas, al diseñar un ECS, que al menos sugeriría encarecidamente que se anticipara un tanto por adelantado (aunque con mucho espacio de respiración indulgente para cambiar de opinión más adelante, siempre que no altere los aspectos más críticos del orden de invocación / evaluación del sistema).

Sin embargo, volver a calcular el mosaico completo de cada cuadro es costoso. Por lo tanto, se necesitaría una lista para realizar un seguimiento de todos los cambios realizados para luego actualizarlos en el sistema. En la forma OOP, esto podría ser encapsulado por el componente de mapa de mosaico. Por ejemplo, el método SetTile () actualizaría la matriz de vértices cada vez que se llame.

No entendí bien este, excepto que es una preocupación orientada a los datos. Y no existen dificultades para representar y almacenar datos en un ECS, incluida la memorización, para evitar tales problemas de rendimiento (los más grandes con un ECS tienden a relacionarse con cosas como los sistemas que consultan instancias disponibles de tipos de componentes particulares, que es uno de los aspectos más desafiantes de la optimización de un ECS generalizado). El hecho de que la lógica y los datos estén separados en un ECS "puro" no significa que de repente tenga que volver a calcular cosas que de otro modo podría haber almacenado / memorizado en una representación OOP. Ese es un punto discutible / irrelevante a menos que haya pasado por alto algo muy importante.

Con el ECS "puro" todavía puede almacenar estos datos en el componente de mapa de mosaico. La única diferencia importante es que la lógica para actualizar esta matriz de vértices se movería a un sistema en algún lugar.

Incluso puede apoyarse en el ECS para simplificar la invalidación y eliminación de este caché de la entidad si crea un componente separado como TileMapCache. En ese momento, cuando se desea el caché pero no está disponible en una entidad con un TileMapcomponente, puede calcularlo y agregarlo. Cuando se invalida o ya no se necesita, puede eliminarlo a través del ECS sin tener que escribir más código específicamente para dicha invalidación y eliminación.

Las dependencias entre los componentes aún existen aunque están ocultas en los sistemas.

No hay dependencia entre componentes en un representante "puro" (no creo que sea correcto decir que los sistemas ocultan las dependencias aquí). Los datos no dependen de los datos, por así decirlo. La lógica depende de la lógica. Y un ECS "puro" tiende a promover que la lógica se escriba de una manera tal que dependa del subconjunto mínimo absoluto de datos y lógica (a menudo ninguno) que un sistema requiere para funcionar, lo cual es diferente a muchas alternativas que a menudo fomentan dependiendo de mucha más funcionalidad de la requerida para la tarea real. Si está utilizando el ECS puro, una de las primeras cosas que debe apreciar son los beneficios de desacoplamiento al tiempo que cuestiona simultáneamente todo lo que aprendió a apreciar en OOP sobre la encapsulación y específicamente la ocultación de información.

Al desacoplar me refiero específicamente a la poca información que sus sistemas necesitan para funcionar. Su sistema de movimiento ni siquiera necesita saber acerca de algo mucho más complejo como a Particleo Character(el desarrollador del sistema ni siquiera necesita saber que tales ideas de entidad existen en el sistema). Solo necesita saber acerca de los datos mínimos básicos, como un componente de posición, que podría ser tan simple como unos pocos flotantes en una estructura. Es incluso menos información y menos dependencias externas de lo que una interfaz pura IMotiontiende a llevar consigo. Se debe principalmente a este conocimiento mínimo que cada sistema requiere para trabajar, lo que hace que el ECS a menudo sea tan indulgente para manejar cambios de diseño muy imprevistos en retrospectiva sin enfrentar roturas de interfaz en cascada en todo el lugar.

El enfoque "impuro" que sugiere disminuye un poco ese beneficio ya que ahora su lógica no está localizada estrictamente en sistemas donde los cambios no causan roturas en cascada. La lógica ahora estaría centralizada hasta cierto punto en los componentes a los que acceden múltiples sistemas que ahora tienen que cumplir con los requisitos de interfaz de todos los diversos sistemas que podrían usarla, y ahora es como si cada sistema necesitara tener conocimiento (depender de) más información de lo estrictamente necesario para trabajar con ese componente.

Dependencias a los datos

Una de las cosas que es controvertida sobre el ECS es que tiende a reemplazar lo que de otro modo podrían ser dependencias de interfaces abstractas con solo datos sin procesar, y eso generalmente se considera una forma de acoplamiento menos deseable y más estricta. Pero en los tipos de dominios como los juegos donde ECS puede ser muy beneficioso, a menudo es más fácil diseñar la representación de datos por adelantado y mantenerla estable que diseñar lo que puede hacer con esos datos en algún nivel central del sistema. Eso es algo que he observado dolorosamente incluso entre veteranos experimentados en bases de códigos que utiliza más un enfoque de interfaz pura de estilo COM con cosas como IMotion.

Los desarrolladores siguieron encontrando razones para agregar, eliminar o cambiar funciones a esta interfaz central, y cada cambio fue espantoso y costoso porque tendería a romper cada clase que se implementaba IMotionjunto con todos los lugares del sistema que se usaban IMotion. Mientras tanto, todo el tiempo con tantos cambios dolorosos y en cascada, los objetos que se implementaron solo IMotionestaban almacenando una matriz de flotadores 4x4 y toda la interfaz solo se preocupaba por cómo transformar y acceder a esos flotadores; la representación de datos fue estable desde el principio, y se podría haber evitado mucho dolor si esta interfaz centralizada, tan propensa a cambiar con necesidades de diseño imprevistas, ni siquiera existiera en primer lugar.

Todo esto puede sonar casi tan desagradable como las variables globales, pero la naturaleza de cómo el ECS organiza estos datos en componentes recuperados explícitamente por tipo a través de sistemas lo hace así, mientras que los compiladores no pueden forzar nada como ocultar información, los lugares que acceden y mutan los datos son generalmente muy explícitos y lo suficientemente obvios como para seguir manteniendo invariantes de manera efectiva y predecir qué tipo de transformaciones y efectos secundarios ocurren de un sistema a otro (en realidad de maneras que podrían ser más simples y predecibles que OOP en ciertos dominios dado cómo el sistema se convierte en una especie de tubería plana).

ingrese la descripción de la imagen aquí

Por último, quiero hacer la pregunta de cómo manejaría la animación en un ECS puro. Actualmente he definido una animación como un functor que manipula una entidad basada en algún progreso entre 0 y 1. El componente de animación tiene una lista de animadores que tiene una lista de animaciones. En su función de actualización, aplica las animaciones que estén activas actualmente en la entidad.

Todos somos pragmáticos aquí. Incluso en gamedev probablemente obtendrás ideas / respuestas conflictivas. Incluso el ECS más puro es un fenómeno relativamente nuevo, territorio pionero, para el cual las personas no necesariamente han formulado las opiniones más fuertes sobre cómo pelar gatos. Mi reacción instintiva es un sistema de animación que incrementa este tipo de progreso de animación en los componentes animados para que se muestre el sistema de renderizado, pero que ignora tantos matices para la aplicación y el contexto en particular.

Con el ECS no es una bala de plata y todavía me encuentro con tendencias para entrar y agregar nuevos sistemas, eliminar algunos, agregar nuevos componentes, cambiar un sistema existente para recoger ese nuevo tipo de componente, etc. No entiendo todo bien la primera vez todavía. Pero la diferencia en mi caso es que no estoy cambiando nada central cuando no anticipo ciertas necesidades de diseño por adelantado. No estoy obteniendo el efecto ondulante de las roturas en cascada que me obligan a recorrer todo el lugar y cambiar tanto código para manejar alguna nueva necesidad que surge, y eso es bastante ahorro de tiempo. También me resulta más fácil para mi cerebro porque cuando me siento con un sistema en particular, no necesito saber / recordar mucho sobre otra cosa además de los componentes relevantes (que son solo datos) para trabajar en él.

Dragon Energy
fuente