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?
design-patterns
architecture
mvc
game-development
applications
Andrew De Andrade
fuente
fuente
Respuestas:
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.
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:
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í:
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:
Algunas desventajas:
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.
Con este tipo de enfoque, la
transform
función analógica anterior se parecía a esta forma: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:
Algunas desventajas:
IMotion
siempre 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.QueryInterface
funció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.Respuesta pragmática: composición
Una de las cosas que estábamos notando antes (o al menos yo estaba) que causaba problemas fue que
IMotion
podrí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 *.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
IMotion
completamente abstracto y apátrida a menos que viéramos un beneficio al tener una gran variedad de implementaciones. En nuestro caso,IMotion
tener 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 implementadorIMotion
a usar la misma implementación y estado asociado para que no lo hagamos ' t objetivos duplicados.Las interfaces se volvieron así más amplias
Behaviors
asociadas con una entidad.IMotion
simplemente se convertiría en unMotion
"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:
Lo evolucionamos a algo más como esto:
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.
QueryInterface
se volvió más comoQueryBehavior
. 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.Algunos pros:
Motion
implementación muy central y obvia , por ejemplo, y no dispersarse en cien subtipos.Algunas desventajas:
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
IRenderable
componente abstracto , asociaríamos un objeto con un concretoMesh
oPointSprites
componente. El sistema de renderizado sabría cómo renderizarMesh
yPointSprites
componentes y buscaría entidades que proporcionen dichos componentes y los dibuje. En otras ocasiones, teníamos varios renderables comoSceneLabel
ese que descubrimos que necesitábamos en retrospectiva, por lo que adjuntamos unSceneLabel
en esos casos a entidades relevantes (posiblemente además de aMesh
). 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:
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:
Algunas desventajas:
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 .
fuente
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.
fuente
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.
fuente