¿Es razonable crear aplicaciones (no juegos) usando una arquitectura de sistema de entidad componente?

24

Sé que cuando se compilan aplicaciones (nativas o web) como las de Apple AppStore o Google Play, es muy común usar una arquitectura Model-View-Controller.

Sin embargo, ¿es razonable crear también aplicaciones utilizando la arquitectura Component-Entity-System común en los motores de juego?

Andrew De Andrade
fuente
1
Echa un vistazo a la arquitectura de la mesa de luz: chris-granger.com/2013/01/24/the-ide-as-data
Hakan Deryal

Respuestas:

39

Sin embargo, ¿es razonable crear también aplicaciones utilizando la arquitectura Component-Entity-System común en los motores de juego?

Para mi, absolutamente. Trabajo en efectos visuales y estudié una amplia variedad de sistemas en este campo, sus arquitecturas (incluido CAD / CAM), ansiosas de SDK y cualquier documento que me diera una idea de los pros y los contras de las decisiones arquitectónicas aparentemente infinitas que podría hacerse, incluso los más sutiles no siempre tienen un impacto sutil.

VFX es bastante similar a los juegos en que hay un concepto central de una "escena", con ventanas que muestran los resultados renderizados. También tiende a haber una gran cantidad de procesamiento central en bucle que gira constantemente alrededor de esta escena en contextos de animación, donde puede haber física, emisores de partículas que generan partículas, mallas animadas y renderizadas, animaciones de movimiento, etc., y finalmente renderizarlas. todo al usuario al final.

Otro concepto similar a los motores de juego, al menos muy complejos, era la necesidad de un aspecto de "diseñador" en el que los diseñadores pudieran diseñar escenas de manera flexible, incluida la capacidad de realizar alguna programación ligera propia (guiones y nodos).

Descubrí, a lo largo de los años, que ECS se ajustaba mejor. Por supuesto, eso nunca está completamente divorciado de la subjetividad, pero diría que parece que da la menor cantidad de problemas. Solucionó muchos más problemas importantes con los que siempre estábamos luchando, a la vez que solo nos dio algunos nuevos problemas menores a cambio.

OOP tradicional

Los enfoques OOP más tradicionales pueden ser realmente sólidos cuando tiene una comprensión firme de los requisitos de diseño por adelantado, pero no de los requisitos de implementación. Ya sea a través de un enfoque de interfaz múltiple más plano o un enfoque ABC jerárquico más anidado, tiende a cimentar el diseño y hace que sea más difícil cambiar mientras hace que la implementación sea más fácil y segura. Siempre existe la necesidad de inestabilidad en cualquier producto que pase de una versión única, por lo que los enfoques de OOP tienden a sesgar la estabilidad (dificultad de cambio y la falta de razones para el cambio) hacia el nivel de diseño, y la inestabilidad (facilidad de cambio y razones para el cambio) al nivel de implementación.

Sin embargo, frente a la evolución de los requisitos del usuario final, tanto el diseño como la implementación pueden necesitar cambios frecuentes. Es posible que encuentre algo extraño, como una fuerte necesidad del usuario final de la criatura analógica que necesita ser tanto vegetal como animal al mismo tiempo, invalidando por completo todo el modelo conceptual que construyó. Los enfoques normales orientados a objetos no lo protegen aquí, y a veces pueden hacer que cambios inesperados y que rompan el concepto sean aún más difíciles. Cuando se trata de áreas muy críticas para el rendimiento, las razones de los cambios de diseño se multiplican aún más.

La combinación de múltiples interfaces granulares para formar la interfaz conforme de un objeto puede ayudar mucho a estabilizar el código del cliente, pero no ayuda a estabilizar los subtipos que a veces podrían disminuir la cantidad de dependencias del cliente. Puede tener una interfaz utilizada solo por una parte de su sistema, por ejemplo, pero con mil subtipos diferentes implementando esa interfaz. En ese caso, mantener los subtipos complejos (complejos porque tienen muchas responsabilidades de interfaz diferentes que cumplir) puede convertirse en una pesadilla en lugar de que el código los use a través de una interfaz. OOP tiende a transferir complejidad al nivel de objeto, mientras que ECS lo transfiere al nivel de cliente ("sistemas"), y eso puede ser ideal cuando hay muy pocos sistemas pero un montón de "objetos" ("entidades") conformes.

ingrese la descripción de la imagen aquí

Una clase también posee sus datos de forma privada y, por lo tanto, puede mantener invariantes por sí sola. Sin embargo, hay invariantes "gruesos" que en realidad pueden ser difíciles de mantener cuando los objetos interactúan entre sí. Para que un sistema complejo en su conjunto esté en un estado válido, a menudo necesita considerar una gráfica compleja de objetos, incluso si sus invariantes individuales se mantienen adecuadamente. Los enfoques tradicionales de estilo OOP pueden ayudar a mantener invariantes granulares, pero en realidad pueden dificultar el mantenimiento de invariantes amplios y gruesos si los objetos se enfocan en las facetas adolescentes del sistema.

Ahí es donde este tipo de enfoques o variantes de ECS de construcción de bloques de lego pueden ser tan útiles. Además, dado que los sistemas tienen un diseño más grueso que el objeto habitual, resulta más fácil mantener ese tipo de invariantes gruesos a la vista de pájaro del sistema. Muchas interacciones de objetos pequeños se convierten en un gran sistema que se enfoca en una tarea amplia en lugar de pequeños objetos pequeños que se enfocan en pequeñas tareas pequeñas con un gráfico de dependencia que cubriría un kilómetro de papel.

Sin embargo, tuve que mirar fuera de mi campo, en la industria del juego, para aprender sobre ECS, aunque siempre tuve una mentalidad orientada a los datos. Además, curiosamente, casi me dirigí hacia ECS por mi cuenta, solo iterando e intentando encontrar mejores diseños. Sin embargo, no llegué hasta el final y me perdí un detalle crucial, que es la formalización de la parte de "sistemas", y el aplastamiento de los componentes hasta los datos sin procesar.

Intentaré analizar cómo terminé estableciéndome en ECS y cómo terminó resolviendo todos los problemas con las iteraciones de diseño anteriores. Creo que eso ayudará a resaltar exactamente por qué la respuesta aquí podría ser un "sí" muy fuerte, que ECS es potencialmente aplicable mucho más allá de la industria del juego.

Arquitectura de fuerza bruta de los años 80

La primera arquitectura en la que trabajé en la industria de efectos visuales tenía un largo legado que ya había pasado una década desde que me uní a la compañía. Fue una fuerza bruta que codificó C en su totalidad (no una inclinación en C, ya que me encanta C, pero la forma en que se usaba aquí fue realmente cruda). Un segmento en miniatura y demasiado simplista se parecía a dependencias como esta:

ingrese la descripción de la imagen aquí

Y este es un diagrama enormemente simplificado de una pequeña parte del sistema. Cada uno de estos clientes en el diagrama ("Representación", "Física", "Movimiento") obtendría algún objeto "genérico" a través del cual verificaría un campo de tipo, así:

void transform(struct Object* obj, const float mat[16])
{
    switch (obj->type)
    {
        case camera:
            // cast to camera and do something with camera fields
            break;
        case light:
            // cast to light and do something with light fields
            break;
        ...
    }
}

Por supuesto, con un código significativamente más feo y complejo que este. A menudo, se llamarían funciones adicionales desde estos casos de conmutador que recursivamente realizarían el cambio una y otra vez. Este diagrama y código podrían parecerse a ECS-lite, pero no hubo una fuerte distinción entre entidad y componente (" ¿ este objeto es una cámara?", No "¿este objeto proporciona movimiento?"), Y no se formalizó el "sistema" ( solo un montón de funciones anidadas que van por todas partes y mezclan responsabilidades). En ese caso, casi todo era complicado, cualquier función era potencial para que ocurriera un desastre.

Nuestro procedimiento de prueba aquí a menudo tenía que verificar cosas como mallas separadas de otros tipos de elementos, incluso si les sucedía lo mismo a ambos, ya que la naturaleza de la fuerza bruta de la codificación aquí (a menudo acompañada de una gran cantidad de copiar y pegar) a menudo hacía es muy probable que, de lo contrario, la misma lógica exacta pueda fallar de un tipo de elemento al siguiente. Intentar extender el sistema para manejar nuevos tipos de elementos era bastante inútil, a pesar de que había una necesidad expresada por parte del usuario, ya que era demasiado difícil cuando estábamos luchando tanto para manejar los tipos de elementos existentes.

Algunos pros:

  • Uhh ... supongo que no requiere experiencia en ingeniería. Este sistema no requiere ningún conocimiento incluso de conceptos básicos como el polimorfismo, es una fuerza totalmente bruta, por lo que supongo que incluso un principiante podría comprender parte del código, incluso si un profesional en depuración apenas puede mantenerlo.

Algunas desventajas:

  • Pesadilla de mantenimiento. Nuestro equipo de marketing realmente sintió la necesidad de alardear de que solucionamos más de 2000 errores únicos en un ciclo de 3 años. Para mí, eso es algo de lo que debería avergonzarse porque tuvimos tantos errores en primer lugar, y ese proceso probablemente solo reparó alrededor del 10% del total de errores que aumentaban en número todo el tiempo.
  • Sobre la solución más inflexible posible.

Arquitectura COM de los años 90

La mayor parte de la industria de efectos visuales utiliza este estilo de arquitectura de lo que he reunido, leyendo documentos sobre sus decisiones de diseño y mirando sus kits de desarrollo de software.

Puede que no sea exactamente COM en el nivel ABI (algunas de estas arquitecturas solo podrían tener complementos escritos usando el mismo compilador), pero comparte muchas características similares con consultas de interfaz realizadas en objetos para ver qué interfaces admiten sus componentes.

ingrese la descripción de la imagen aquí

Con este tipo de enfoque, la transformfunción analógica anterior se parecía a esta forma:

void transform(Object obj, const Matrix& mat)
{
    // Wrapper that performs an interface query to see if the 
    // object implements the IMotion interface.
    MotionRef motion(obj);

    // If the object supported the IMotion interface:
    if (motion.valid())
    {
        // Transform the item through the IMotion interface.
        motion->transform(mat);
        ...
    }
}

Este es el enfoque que adoptó el nuevo equipo de esa antigua base de código, para eventualmente refactorizar. Y fue una mejora dramática sobre el original en términos de flexibilidad y facilidad de mantenimiento, pero aún había algunos problemas que abordaré en la siguiente sección.

Algunos pros:

  • Dramáticamente más flexible / extensible / mantenible que la solución anterior de fuerza bruta.
  • Promueve una fuerte conformidad con muchos principios de SOLID al hacer que cada interfaz sea completamente abstracta (sin estado, sin implementación, solo interfaces puras).

Algunas desventajas:

  • Un montón de repeticiones. Nuestros componentes tenían que publicarse a través de un registro para crear instancias de objetos, las interfaces que soportaban requerían tanto heredar ("implementar" en Java) la interfaz como proporcionar algún código para indicar qué interfaces estaban disponibles en una consulta.
  • Lógica duplicada promovida por todas partes como resultado de las interfaces puras. Por ejemplo, todos los componentes que se implementaron IMotionsiempre tendrían exactamente el mismo estado y la misma implementación para todas las funciones. Para mitigar esto, comenzaríamos a centralizar las clases base y la funcionalidad auxiliar en todo el sistema para las cosas que tenderían a implementarse de forma redundante de la misma manera para la misma interfaz, y posiblemente con herencia múltiple detrás del capó, pero fue bastante desordenado bajo el capó a pesar de que el código del cliente lo tenía fácil.
  • Ineficiencia: las sesiones de vtune a menudo mostraban la QueryInterfacefunción básica casi siempre apareciendo como un punto de acceso medio a superior, y ocasionalmente incluso el punto de acceso n. ° 1. Para mitigar eso, haríamos cosas como mostrar partes de la caché de la base de código en una lista de objetos que ya se sabe que admitenIRenderable, pero eso aumentó significativamente la complejidad y los costos de mantenimiento. Del mismo modo, esto fue más difícil de medir, pero notamos algunas ralentizaciones definitivas en comparación con la codificación de estilo C que estábamos haciendo antes, cuando cada interfaz requería un despacho dinámico. Cosas como las predicciones erróneas de las sucursales y las barreras de optimización son difíciles de medir fuera de una pequeña faceta del código, pero los usuarios generalmente notaron la capacidad de respuesta de la interfaz de usuario y cosas así empeoraron al comparar las versiones anteriores y más recientes del software de lado a lado. lado para áreas donde la complejidad algorítmica no cambió, solo las constantes.
  • Todavía era difícil razonar sobre la corrección en un nivel de sistema más amplio. Aunque fue significativamente más fácil que el enfoque anterior, aún era difícil comprender las complejas interacciones entre los objetos en todo este sistema, especialmente con algunas de las optimizaciones que comenzaron a ser necesarias en su contra.
  • Tuvimos problemas para corregir nuestras interfaces. Aunque puede que solo haya un lugar amplio en el sistema que use una interfaz, los requisitos del usuario final cambiarían con las versiones, y terminaríamos teniendo que hacer cambios en cascada en todas las clases que implementan la interfaz para acomodar una nueva función agregada a la interfaz, por ejemplo, a menos que hubiera una clase base abstracta que ya estuviera centralizando la lógica bajo el capó (algunos de estos se manifestarían en medio de estos cambios en cascada con la esperanza de no hacerlo repetidamente una y otra vez).

ingrese la descripción de la imagen aquí

Respuesta pragmática: composición

Una de las cosas que estábamos notando antes (o al menos yo estaba) que causaba problemas fue que IMotionpodría implementarse en 100 clases diferentes pero con la misma implementación y estado asociados. Además, solo sería utilizado por un puñado de sistemas como renderizado, movimiento de fotogramas clave y física.

Entonces, en tal caso, podríamos tener una relación de 3 a 1 entre los sistemas que usan la interfaz a la interfaz, y una relación de 100 a 1 entre los subtipos que implementan la interfaz a la interfaz.

La complejidad y el mantenimiento estarían drásticamente sesgados para la implementación y el mantenimiento de 100 subtipos, en lugar de 3 sistemas cliente de los que dependen IMotion. Esto cambió todas nuestras dificultades de mantenimiento al mantenimiento de estos 100 subtipos, no a los 3 lugares que usan la interfaz. Actualización de 3 lugares en el código con pocos o ningún "acoplamiento eferente indirecto" (como en dependencias de él pero indirectamente a través de una interfaz, no una dependencia directa), no es gran cosa: actualizar 100 lugares de subtipo con una gran cantidad de "acoplamientos eferentes indirectos" , gran cosa *.

* Me doy cuenta de que es extraño e incorrecto atornillar la definición de "acoplamientos eferentes" en este sentido desde una perspectiva de implementación, simplemente no he encontrado una mejor manera de describir la complejidad de mantenimiento asociada cuando tanto la interfaz como las implementaciones correspondientes de cien subtipos debe cambiar.

Así que tuve que esforzarme mucho, pero propuse que intentáramos ser un poco más pragmáticos y relajar la idea de "interfaz pura". No tenía sentido para mí hacer algo IMotioncompletamente abstracto y apátrida a menos que viéramos un beneficio al tener una gran variedad de implementaciones. En nuestro caso, IMotiontener una gran variedad de implementaciones en realidad se convertiría en una pesadilla de mantenimiento, ya que no queríamos variedad. En cambio, estábamos iterando para tratar de hacer una implementación de movimiento único que fuera realmente buena contra los requisitos cambiantes del cliente, y a menudo trabajábamos mucho alrededor de la idea de la interfaz pura tratando de obligar a cada implementador IMotiona usar la misma implementación y estado asociado para que no lo hagamos ' t objetivos duplicados.

Las interfaces se volvieron así más amplias Behaviorsasociadas con una entidad. IMotionsimplemente se convertiría en un Motion"componente" (cambié la forma en que definimos "componente" de COM a uno donde está más cerca de la definición habitual, de una pieza que constituye una entidad "completa").

En lugar de esto:

class IMotion
{
public:
    virtual ~IMotion() {}
    virtual void transform(const Matrix& mat) = 0;
    ...
};

Lo evolucionamos a algo más como esto:

class Motion
{
public:
    void transform(const Matrix& mat)
    {
        ...
    }
    ...

private:
    Matrix transformation;
    ...
};

Esta es una violación flagrante del principio de inversión de dependencia para comenzar a pasar de lo abstracto a lo concreto, pero para mí ese nivel de abstracción solo es útil si podemos prever una necesidad genuina en algún futuro, más allá de una duda razonable y no ejercer escenarios ridículos de "qué pasaría si" completamente separados de la experiencia del usuario (lo que probablemente requeriría un cambio de diseño de todos modos), para tal flexibilidad.

Entonces comenzamos a evolucionar hacia este diseño. QueryInterfacese volvió más como QueryBehavior. Además, comenzó a parecer inútil usar la herencia aquí. Usamos composición en su lugar. Los objetos se convirtieron en una colección de componentes cuya disponibilidad podría consultarse e inyectarse en tiempo de ejecución.

ingrese la descripción de la imagen aquí

Algunos pros:

  • En nuestro caso, fue mucho más fácil de mantener que el anterior sistema de estilo COM de interfaz pura. Las sorpresas imprevistas, como un cambio en los requisitos o las quejas sobre el flujo de trabajo, podrían acomodarse más fácilmente con una Motionimplementación muy central y obvia , por ejemplo, y no dispersarse en cien subtipos.
  • Dio un nivel completamente nuevo de flexibilidad del tipo que realmente necesitábamos. En nuestro sistema anterior, dado que la herencia modela una relación estática, solo pudimos definir efectivamente nuevas entidades en tiempo de compilación en C ++. No pudimos hacerlo desde el lenguaje de secuencias de comandos, por ejemplo, con el enfoque de composición, podríamos agrupar nuevas entidades sobre la marcha en tiempo de ejecución simplemente uniéndoles componentes y agregándolos a una lista. Una "entidad" convertida en un lienzo en blanco sobre el cual podríamos simplemente juntar un collage de lo que necesitáramos sobre la marcha, con sistemas relevantes que reconocen y procesan automáticamente estas entidades como resultado.

Algunas desventajas:

  • Todavía estábamos teniendo dificultades en el departamento de eficiencia y mantenibilidad en las áreas críticas de rendimiento. Cada sistema terminaría queriendo almacenar en caché los componentes de las entidades que proporcionaban estos comportamientos para evitar recorrerlos repetidamente y verificar lo que estaba disponible. Cada sistema que exigía rendimiento haría esto de manera ligeramente diferente, y era propenso a un conjunto diferente de errores al no actualizar esta lista almacenada en caché y posiblemente una estructura de datos (si alguna forma de búsqueda estaba involucrada, como el descarte de frustum o el trazado de rayos) en algunos evento oscuro de cambio de escena, p. ej.
  • Todavía había algo incómodo y complejo que no pude identificar en relación con todos estos pequeños objetos granulares de comportamiento y simples. Todavía generamos muchos eventos para tratar las interacciones entre estos objetos de "comportamiento" que a veces eran necesarios, y el resultado fue un código muy descentralizado. Cada pequeño objeto era fácil de comprobar para su corrección y, tomados individualmente, a menudo eran perfectamente correctos. Sin embargo, todavía se sentía como si estuviéramos tratando de mantener un ecosistema masivo compuesto de pequeñas aldeas y tratando de razonar sobre lo que todos hacen individualmente y suman para formar un todo. La base de código de los 80 de estilo C parecía una megalópolis épica y superpoblada que definitivamente era una pesadilla de mantenimiento,
  • Pérdida de flexibilidad con la falta de abstracción, pero en un área en la que nunca encontramos una necesidad genuina, por lo que difícilmente sea una estafa práctica (aunque definitivamente al menos teórica).
  • Preservar la compatibilidad ABI siempre fue difícil, y esto lo hizo más difícil al requerir datos estables y no solo una interfaz estable asociada con un "comportamiento". Sin embargo, podríamos agregar fácilmente nuevos comportamientos y simplemente desaprobar los existentes si fuera necesario un cambio de estado, y eso podría decirse que es más fácil que hacer volteretas por debajo de las interfaces en el nivel de subtipo para manejar problemas de versiones.

Un fenómeno que ocurrió fue que, dado que perdimos la abstracción sobre estos componentes de comportamiento, tuvimos más de ellos. Por ejemplo, en lugar de un IRenderablecomponente abstracto , asociaríamos un objeto con un concreto Mesho PointSpritescomponente. El sistema de renderizado sabría cómo renderizar Meshy PointSpritescomponentes y buscaría entidades que proporcionen dichos componentes y los dibuje. En otras ocasiones, teníamos varios renderables como SceneLabelese que descubrimos que necesitábamos en retrospectiva, por lo que adjuntamos un SceneLabelen esos casos a entidades relevantes (posiblemente además de a Mesh). El implemento del sistema de representación se actualizaría para saber cómo representar las entidades que las proporcionaron, y ese fue un cambio bastante fácil de hacer.

En este caso, una entidad compuesta de componentes también podría usarse como un componente para otra entidad. Construiríamos las cosas de esa manera conectando bloques de lego.

ECS: sistemas y componentes de datos sin procesar

Ese último sistema fue tan lejos como lo hice yo solo, y todavía lo estábamos bastardeando con COM. Parecía querer convertirse en un sistema de componente de entidad, pero no estaba familiarizado con él en ese momento. Estaba mirando ejemplos de estilo COM que saturaban mi campo, cuando debería haber estado buscando motores de juegos AAA en busca de inspiración arquitectónica. Finalmente comencé a hacer eso.

Lo que me faltaba eran varias ideas clave:

  1. La formalización de "sistemas" para procesar "componentes".
  2. Los "componentes" son datos en bruto en lugar de objetos de comportamiento compuestos juntos en un objeto más grande.
  3. Las entidades no son más que un ID estricto asociado a una colección de componentes.

Finalmente dejé esa compañía y comencé a trabajar en un ECS como una empresa (aún trabajando en ello mientras agotaba mis ahorros), y ha sido el sistema más fácil de administrar con diferencia.

Lo que noté con el enfoque ECS fue que resolvió los problemas con los que todavía estaba luchando anteriormente. Lo más importante para mí, era que estábamos manejando "ciudades" de tamaño saludable en lugar de pequeñas aldeas con interacciones complejas. No fue tan difícil de mantener como una "megalópolis" monolítica, demasiado grande en su población para administrarla de manera efectiva, pero no fue tan caótica como un mundo lleno de pequeñas aldeas que interactúan entre sí donde solo piensan en las rutas comerciales en entre ellos formaron un gráfico de pesadilla. ECS destilaba toda la complejidad hacia voluminosos "sistemas", como un sistema de renderizado, una "ciudad" de tamaño saludable pero no una "megalópolis superpoblada".

Los componentes que se convirtieron en datos en bruto me parecieron realmente extraños al principio, ya que rompen incluso el principio básico de ocultación de información de OOP. Fue una especie de desafío uno de los valores más importantes que apreciaba sobre OOP, que era su capacidad para mantener invariantes que requerían encapsulación y ocultación de información. Pero comenzó a no ser una preocupación, ya que rápidamente se hizo evidente lo que estaba sucediendo con solo una docena de sistemas amplios que transformaban esos datos en lugar de que dicha lógica se dispersara en cientos de miles de subtipos que implementaban una combinación de interfaces. Tiendo a pensar en ello como si fuera un estilo OOP, excepto en los lugares donde los sistemas proporcionan la funcionalidad y la implementación que acceden a los datos, los componentes proporcionan los datos y las entidades proporcionan los componentes.

Se hizo aún más fácil , contra-intuitivamente, razonar sobre los efectos secundarios causados ​​por el sistema cuando solo había un puñado de sistemas voluminosos que transformaban los datos en pasos amplios. El sistema se volvió mucho más "plano", mis pilas de llamadas se volvieron menos profundas que nunca para cada hilo. Podría pensar en el sistema a ese nivel de supervisor y no encontrar sorpresas extrañas.

Del mismo modo, incluso simplificó las áreas críticas de rendimiento con respecto a la eliminación de esas consultas. Dado que la idea de "Sistema" se hizo muy formal, un sistema podía suscribirse a los componentes que le interesaban y simplemente recibir una lista en caché de entidades que satisfacen ese criterio. Cada individuo no tuvo que administrar esa optimización de almacenamiento en caché, se centralizó en un solo lugar.

Algunos pros:

  • Parece resolver solo casi todos los problemas arquitectónicos importantes que encontré en mi carrera sin sentirme atrapado en un rincón del diseño cuando encuentro necesidades imprevistas.

Algunas desventajas:

  • A veces todavía me cuesta entenderlo, y no es el paradigma más maduro o bien establecido, incluso dentro de la industria del juego, donde las personas discuten exactamente qué significa y cómo hacer las cosas. Definitivamente no es algo que podría haber hecho con el antiguo equipo con el que trabajé, que consistía en miembros profundamente enganchados a la mentalidad de estilo COM o la mentalidad de estilo C de los años 80 de la base de código original. Donde me confundo a veces es como modelar relaciones de estilo gráfico entre componentes, pero siempre he encontrado una solución que no resultó horrible después, donde puedo hacer que un componente dependa de otro ("este movimiento el componente depende de este otro como padre, y el sistema usará la memorización para evitar hacer repetidamente los mismos cálculos de movimiento recursivo ", por ejemplo)
  • ABI sigue siendo difícil, pero hasta ahora incluso me aventuraría a decir que es más fácil que el enfoque de interfaz pura. Es un cambio de mentalidad: la estabilidad de los datos se convierte en el único foco para ABI, en lugar de la estabilidad de la interfaz, y de alguna manera es más fácil lograr la estabilidad de los datos que la estabilidad de la interfaz (por ejemplo: no hay tentaciones de cambiar una función solo porque necesita un nuevo parámetro. Ese tipo de cosas suceden dentro de implementaciones de sistemas gruesos que no rompen ABI).

ingrese la descripción de la imagen aquí

Sin embargo, ¿es razonable crear también aplicaciones utilizando la arquitectura Component-Entity-System común en los motores de juego?

De todos modos, diría absolutamente "sí", con mi ejemplo personal de efectos visuales siendo un candidato fuerte. Pero eso sigue siendo bastante similar a las necesidades de los juegos.

No lo he puesto en práctica en áreas más remotas completamente ajenas a las preocupaciones de los motores de juegos (VFX es bastante similar), pero me parece que muchas más áreas son buenas candidatas para un enfoque ECS. Tal vez incluso un sistema GUI sería adecuado para uno, pero todavía uso un enfoque más OOP allí (pero sin una herencia profunda a diferencia de Qt, por ejemplo).

Es un territorio ampliamente inexplorado, pero me parece adecuado siempre que sus entidades puedan estar compuestas de una rica combinación de "rasgos" (y exactamente qué conjunto de rasgos proporcionan que estén sujetos a cambios), y donde haya un puñado de generalizados. sistemas que procesan entidades que tienen los rasgos necesarios.

Se convierte en una alternativa muy práctica en esos casos a cualquier escenario en el que podría verse tentado a usar algo como herencia múltiple o una emulación del concepto (mixins, por ejemplo) solo para producir cientos o más combos en una jerarquía de herencia profunda o cientos de combos de clases en una jerarquía plana que implementa una combinación específica de interfaces, pero donde sus sistemas son pocos (docenas, por ejemplo).

En esos casos, la complejidad de la base de código comienza a sentirse más proporcional a la cantidad de sistemas en lugar de la cantidad de combinaciones de tipos, ya que cada tipo ahora es solo una entidad que compone componentes que no son más que datos sin procesar. Los sistemas GUI se ajustan naturalmente a este tipo de especificaciones donde pueden tener cientos de posibles tipos de widgets combinados de otros tipos de bases o interfaces, pero solo un puñado de sistemas para procesarlos (sistema de diseño, sistema de representación, etc.). Si un sistema GUI usara ECS, probablemente sería mucho más fácil razonar sobre la corrección del sistema cuando un puñado de estos sistemas proporciona toda la funcionalidad en lugar de cientos de diferentes tipos de objetos con interfaces heredadas o clases base. Si un sistema GUI usara ECS, los widgets no tendrían funcionalidad, solo datos. Solo un puñado de sistemas que procesan entidades de widgets tendrían funcionalidad. La forma en que se manejarían los eventos reemplazables para un widget está fuera de mi alcance, pero solo en base a mi experiencia limitada hasta el momento, no he encontrado un caso en el que ese tipo de lógica no pueda transferirse centralmente a un sistema dado de una manera que, en en retrospectiva, produjo una solución mucho más elegante de lo que esperaba.

Me encantaría verlo empleado en más campos, ya que fue un salvavidas en el mío. Por supuesto, no es adecuado si su diseño no se descompone de esta manera, desde entidades que agregan componentes hasta sistemas gruesos que procesan esos componentes, pero si encajan naturalmente en este tipo de modelo, es lo más maravilloso que he encontrado hasta ahora .

Thomas Owens
fuente
1) ¿Qué hizo su programa VFX de ejemplo desde la perspectiva de un usuario? 2) ¿En qué proyecto de ECS estás trabajando ahora? ♥ ¡Gracias por escribir esto! ♥
cachorro
1
Explicación muy completa, gracias. Siento que estoy llegando a muchas de las mismas conclusiones que usted con respecto a cómo el ECS aplicable está más allá de los juegos; en mi caso, GUI específicamente complejas. Definitivamente, se siente realmente extraño al principio ir tan en contra de lo que generalmente se hace (las jerarquías de herencia profundas son especialmente prominentes en los marcos de IU), pero es alentador ver a otros que encuentran que este enfoque es más efectivo.
Danny Yaroslavski
1
Gracias por este gran ... artículo! Para una GUI basada en componentes, recomendaría mirar la UGUI de Unity3d. Es increíblemente flexible y ampliable en comparación con los basados ​​en herencia como CocoaTouch.
Ivan Mir
16

La arquitectura de sistema de entidad componente para motores de juegos funciona para juegos debido a la naturaleza del software del juego y sus características únicas y requisitos de calidad. Por ejemplo, las entidades proporcionan un medio uniforme para abordar y trabajar con cosas en el juego, que pueden ser drásticamente diferentes en su propósito y uso, pero deben ser procesados, actualizados o serializados / deserializados por el sistema de manera uniforme. Al incorporar un modelo de componentes en esta arquitectura, les permite mantener una estructura central simple, al tiempo que agrega más características y funcionalidades según sea necesario, con un bajo acoplamiento de código. Existen varios sistemas de software diferentes que podrían beneficiarse de las características de este diseño, como aplicaciones CAD, códecs A / V,

TL; DR: los patrones de diseño solo funcionan bien cuando el dominio del problema es lo suficientemente adecuado para las características y desventajas que imponen en el diseño.

Shotgun Ninja
fuente
8

Si el dominio del problema se adapta bien a él, ciertamente.

Mi trabajo actual involucra una aplicación que necesita soportar una variedad de capacidades dependiendo de un montón de factores de tiempo de ejecución. El uso de entidades basadas en componentes para desacoplar todas esas capacidades y permitir la extensibilidad y la capacidad de prueba de forma aislada ha sido idílico para nosotros.

editar: mi trabajo implica proporcionar conectividad a hardware propietario (en C #). Dependiendo de qué factor de forma sea el hardware, qué firmware está instalado en él, qué nivel de servicio ha adquirido el cliente, etc., etc. necesitamos proporcionar diferentes niveles de funcionalidad al dispositivo. Incluso algunas características que tienen la misma interfaz tienen implementaciones diferentes según la versión del dispositivo.

Las bases de código anteriores aquí han tenido interfaces muy amplias con muchas no implementadas. Algunos han tenido muchas interfaces delgadas que luego fueron compuestas estáticamente en una clase beasty. Algunos simplemente usaron cadenas -> diccionarios de cadenas para modelarlo. (Tenemos muchos departamentos que piensan que pueden hacerlo mejor)

Todos estos tienen sus deficiencias. Las interfaces anchas son un dolor y medio para burlarse / probar de manera efectiva. Agregar nuevas características significa cambiar la interfaz pública (y todas las implementaciones existentes). Muchas interfaces delgadas condujeron a un código de consumo muy feo, pero dado que terminamos pasando un gran objeto gordo, las pruebas aún sufrieron. Además, las interfaces delgadas no administraban bien sus dependencias. Los diccionarios de cadenas tienen los problemas habituales de análisis y existencia, así como también problemas de rendimiento, legibilidad y facilidad de mantenimiento.

Lo que estamos usando ahora es una entidad muy delgada que tiene sus componentes descubiertos y compuestos basados ​​en información de tiempo de ejecución. Las dependencias se realizan de forma declarativa y se resuelven automáticamente mediante el marco de componentes principales. Los componentes en sí pueden probarse de forma aislada, ya que funcionan directamente con sus dependencias, y los problemas con las dependencias faltantes se encuentran temprano, y en una ubicación en lugar del primer uso de la dependencia. Se pueden colocar componentes nuevos (o de prueba) y no afecta a ningún código existente. Los consumidores solicitan a la entidad una interfaz para el componente, por lo que somos libres de jugar con las diversas implementaciones (y cómo las implementaciones se asignan a los datos de tiempo de ejecución) con relativa libertad.

Para una situación como esta en la que la composición del objeto y sus interfaces pueden incluir un subconjunto (muy variado) de componentes comunes, funciona muy bien.

Telastyn
fuente
1
Suponiendo que se le permita, ¿puede proporcionar más detalles sobre su trabajo actual? Tengo curiosidad por saber de qué manera el CES ha sido idílico para lo que está construyendo.
Andrew De Andrade
¿Hay algún artículo, artículo o blog sobre tu experiencia? Además, me gustaría tener más detalles técnicos al respecto :)
user1778770
@ user1778770 - no disponible públicamente, no. ¿Qué tipo de preguntas tenías?
Telastyn
Bueno, comencemos con algo simple, ¿su concepto abarca toda la pila de aplicaciones (por ejemplo, de negocios a frontend)? o solo una capa de un solo caso de uso?
user1778770
@ user1778770: en mi implementación, las entidades / componentes existen en una capa. Pueden existir diferentes entidades en diferentes capas, pero a menudo no son 1: 1 (o de lo contrario las capas no proporcionan ningún beneficio).
Telastyn