¡Hola, Hackernon! El siguiente tema que he elegido es ECS (Entity-Component-System) en el . Lo he dividido en dos partes para ayudarte a percibir toda la información de forma más accesible. desarrollo de Unity Te diré todo lo que sé sobre Entidad-Componente-Sistema y trataré de disipar varias ideas preconcebidas sobre este enfoque. Encontrará muchas palabras sobre las ventajas y desventajas de ECS, las peculiaridades de este enfoque, cómo hacerse amigo de él, posibles peligros y prácticas útiles. También analizaré brevemente los marcos ECS para Unity/C#. Este artículo será bueno para aquellos que quieran o comiencen a familiarizarse con ECS. Las personas que han probado ECS, espero, también podrán enfatizar algo nuevo para ellos mismos. Si crea juegos en cualquier idioma que no sea C#, aún puede encontrar útil este artículo. No habrá muestras de código ni historial del patrón, solo mi experiencia, razonamiento y observaciones :) ¿Qué es ECS (Entidad-Componente-Sistema)? Entity-Component-System es un patrón arquitectónico creado específicamente para el desarrollo de juegos. Es perfecto para describir un mundo virtual dinámico. Por sus peculiaridades, algunas personas lo consideran casi un nuevo paradigma de programación. ECS es un principio absoluto de Composición sobre Herencia. Puede ser un ejemplo particular de Diseño Orientado a Datos (DOD), pero depende de la interpretación del patrón por parte de una implementación particular. Vamos a descifrar el nombre de este patrón: : un objeto máximamente abstracto. Es un contenedor condicional de propiedades que definen lo que será esta Entidad. A menudo se representa como un identificador para acceder a los datos. Entidad : una propiedad con datos de objeto. Los componentes en ECS deben contener solo datos puros sin una sola gota de lógica. Sin embargo, algunos desarrolladores permiten varios getters y setters en los componentes. Aún así, creo que las utilidades estáticas son más adecuadas para estos fines. Componente - la lógica del procesamiento de datos. Los sistemas en ECS no deben contener datos, solo lógica de procesamiento de datos. Pero, de nuevo, algunos desarrolladores le permiten definir algún comportamiento auxiliar del sistema, por ejemplo, constantes o varios servicios auxiliares. Sistema Como ya se dio cuenta de lo anterior: ECS separa estrictamente los datos de la lógica. El comportamiento de un objeto no está determinado por las interfaces/contratos/API pública, como estamos acostumbrados en la programación clásica orientada a objetos (POO), sino por las propiedades asignadas al objeto con datos y lógica de procesamiento que existen por separado. En ECS, los datos lo definen todo. Esta es la propiedad principal que lo distingue de otros enfoques de desarrollo: todo son datos. Las propiedades, las características y los eventos de los objetos son solo datos en el mundo de ECS. La lógica es simplemente el procesamiento de canalización de todos estos datos. ¿Por qué se necesita ECS? Probablemente ya tenga una pregunta: "¿Por qué necesito ECS? ¿Para qué sirve?". Y para ayudarlo a decidir si leer más este artículo, le diré por qué me gusta ECS. Personalmente, me encanta ECS porque: Con ECS, simplemente se sienta y en lugar de luchar con la arquitectura del proyecto. No hay necesidad de construir jerarquías grandes y hermosas, pensar en muchas conexiones y preocuparse por "X no debería saber sobre Y". Al mismo tiempo, los principios de ECS lo protegen (no al 100%, por supuesto) de la situación desesperada causada por una mala arquitectura cuando el desarrollo del proyecto se vuelve muy doloroso. E incluso si algo salió mal, la refactorización en ECS no es un problema. Y esto , en mi opinión, es lo mejor de ECS. crea un juego en Unity El código en ECS es simple y claro. No necesita rastrear llamadas entre clases para comprender lo que hace un sistema en particular. Puede ver todo a la vez, especialmente si divide una función en sistemas, sistemas en métodos y no complica demasiado el código. Además, ECS simplifica enormemente la creación de perfiles. Puede ver de inmediato qué lógica (sistema) toma cuánto tiempo de cuadro. No necesita buscar el origen de los retrasos en la profundidad de las llamadas. Es fácil manipular la lógica. Agregar nueva lógica es prácticamente indoloro. Simplemente inserte un nuevo sistema en el lugar correcto sin temor a afectar directamente el resto del código (debe tenerse en cuenta que es posible la influencia indirecta a través de los datos). Puede usar la lógica común (sistemas) entre el cliente y el servidor sin ningún problema mientras mantiene los datos (componentes) utilizados. Puede reescribir fácilmente los sistemas, reemplazando los sistemas antiguos con los refactorizados sin afectar el resto del código. Si no le gusta el resultado, simplemente vuelva a encender el sistema anterior. El mismo mecanismo puede organizar fácilmente pruebas A/B. Todo gira en torno a los datos. Resulta ser tremendamente conveniente. Al manipular directamente los datos de las entidades, las posibilidades de la combinatoria son enormes. Puede usar datos para moldear una entidad en cualquier cosa. Y supongamos que el marco ofrece herramientas para ver datos sobre entidades. En ese caso, puede examinar los datos y su dinámica en cualquier entidad sin ejecutar un depurador para buscar en la memoria. ¿Ahora me entiendes? ¿Cómo trabajar con ECS? Aquí describiré en palabras simples cómo funciona el proceso de desarrollo con ECS en el ejemplo más simple. Haré esto de la manera más abstracta posible sin hacer referencia a un lenguaje de programación. Si ya tiene algo de experiencia con ECS, puede pasar directamente a la siguiente sección :) crear un objeto que se mueva en la dirección de un vector de movimiento dado. Tarea: Primero, definamos los datos que necesitamos para nuestro trabajo. Para nuestra tarea, necesitaremos la posición del objeto y el vector de movimiento dado. En el lenguaje ECS, estos serán: PositionComponent para almacenar el vector de posición Componente de movimiento para el vector de movimiento. El siguiente paso es describir la lógica. Vamos a crear un . En el método principal del sistema, dependiendo de la implementación, puede ser o algo más. Obtiene todas las entidades en ECS que tienen y . Cómo se puede hacer exactamente esto depende del marco, pero a menudo parece una especie de consulta SQL como . MovementSystem Run()/Execute()/Update() PositionComponent MovementComponent GetAllEntities().With<PositionComponent>().With<MovementComponent>() Y finalmente, simplemente crea una entidad (incluso diez piezas) con nuestros dos componentes y establece el vector de movimiento diferente de cero. Ahora, en cada llamada de (independientemente de dónde y cuándo lo llamemos), nuestro objeto cambiará de posición en la dirección del vector de movimiento dado. ¡Tarea cumplida! :) MovementSystem A menudo, los sistemas están integrados de alguna manera en el GameLoop del proyecto y mueven cada cuadro por el propio motor. Pero puede hacerlo a mano y de cualquier otra forma, porque es solo una llamada de método. Veamos qué posibilidades adicionales de desarrollo obtuvimos además de resolver el problema principal: Cualquiera de nuestros otros sistemas puede determinar si un objeto se está moviendo simplemente verificando la presencia de la propiedad MovementComponent. Cualquier otro sistema puede obtener el vector de movimiento para sus necesidades. Cualquiera de nuestros otros sistemas podrá especificar un vector de movimiento para cualquiera de nuestras entidades a voluntad. Si queremos, también podemos hacer que cualquier otra entidad se mueva simplemente colocando y sobre ella. Esto es muy útil al . PositionComponent MovementComponent crear juegos de Unity Ventajas de ECS en Unity En esta sección, discutiremos lo bueno y lo malo de ECS. Algunas de las características que se describen a continuación tienen dos caras de la moneda. Ambos son beneficiosos para el desarrollo e incómodos, creando limitaciones que a veces deben sortearse. Primero, analicemos las ventajas de ECS en Unity. Cohesión de código débil Esta es una propiedad beneficiosa para . Nos permite refactorizar y ampliar la base de código con relativa facilidad y sin romper fragmentos de código antiguos. Siempre podemos agregar un nuevo comportamiento utilizando datos antiguos sin interferir con la lógica anterior de ninguna manera. ECS logra este efecto porque los datos expresan todas las interacciones lógicas en Entity. Este es un objeto máximamente abstracto sin ninguna garantía, como algunos Objetos en C#/Java. los desarrolladores de juegos de Unity Sin embargo, debe recordar que en ECS, el orden de los cambios de datos juega un papel importante. Eventualmente puede afectar la complejidad de la refactorización y romper su lógica anterior o incluso crear errores de efectos secundarios desagradables. Perfecta modularidad y comprobabilidad de la lógica. Si toda la interacción se expresa en datos puros, nuestra lógica siempre está completamente desacoplada de la fuente de datos. Esto nos permite mover la lógica de un proyecto a otro y reutilizarla (manteniendo el formato de datos, por supuesto), así como ejecutar la lógica en cualquier dato de entrada para probar su funcionamiento. Es más difícil escribir código pobre ECS es menos exigente con la arquitectura porque establece el marco con el que es más difícil crear un diseño de código realmente malo. Al mismo tiempo, como se dijo anteriormente, podemos solucionar el problema de forma relativamente sencilla y con un impacto mínimo en el resto del código, incluso si ocurre un mal diseño de código. ECS nos permite pensar menos en "cómo encajar esta lógica en nuestra arquitectura sin romper nada" y agregar nuevas funciones. Combinatoria de propiedades Esta ventaja hace que ECS sea una excelente opción para describir mundos dinámicos. Imagínese: puede otorgar cualquier propiedad (y, por lo tanto, lógica) a cualquiera de sus entidades sin problemas. Si desea que la cámara tenga salud, puede colocar un en la cámara. Se dañará (si existe tal sistema). Coloque un en una entidad e inmediatamente comenzará a sufrir daños por quemaduras si tiene un . ¿Quieres que la casa se mueva bajo el control del jugador? No hay problema, solo usa . HealthComponent InFireComponent HealthComponent PlayerInputListenerComponent Un desarrollador experimentado dirá: "Ja, la mayoría de los patrones de Composición sobre Herencia pueden manejar esto. ¿Cómo es mejor ECS?". Mi respuesta es: "ECS le permite combinar propiedades no solo en términos de formación de entidades, sino también para crear una lógica específica al combinar múltiples propiedades (componentes) en la misma entidad". ¡Ni siquiera he mencionado la capacidad de agregar una lógica completamente nueva para datos antiguos sin tocar los componentes de la entidad! Es más fácil hacer cumplir una Responsabilidad Única Cuando tenemos una lógica completamente separada de los datos y no atada a ningún objeto/entidad, se vuelve más fácil controlar la partición de la lógica por su propósito en lugar de su lugar en la jerarquía. Cada sistema simplemente realiza alguna tarea específica única para él. A menudo, el código del sistema parece una única llamada de método para muchos componentes del mismo tipo. Como resultado, el código es mayormente fácil de leer y percibir. Perfilado más claro Al perfilar, podemos ver qué lógica y cuánto tiempo de fotograma lleva. Esto es posible gracias a sistemas separados con su lógica responsable del procesamiento. No necesitamos profundizar en la pila de llamadas para comprender qué lleva más tiempo. Inmediatamente podemos ver el culpable CharMovementSystem. Cabe señalar que esta ventaja depende del dispositivo del marco ECS porque el propio marco puede tener su pila de llamadas. ECS puede dar un buen impulso de rendimiento Mucha gente piensa que el buen rendimiento es la principal ventaja de ECS (gracias a la propaganda de Unity). Esto no es completamente cierto. La velocidad de ejecución del código es solo una buena ventaja que resulta de los principios del patrón: datos en un lugar, lógica en otro + SIMD (instrucción única, datos múltiples). Y si el marco sigue el DOD al implementar ECS y logra una buena localidad de datos, también obtenemos un código más amigable con el caché, lo que hará feliz a su procesador. El rendimiento final de ECS depende de muchos factores: cómo almacena los datos exactamente el marco, cómo filtra las entidades el marco, qué tan rápido acceden los sistemas a los datos y qué tan rápido funciona el código dentro de sus sistemas. Sin embargo, , ECS siempre será más rápido que el enfoque MonoBehaviour habitual, especialmente en grandes cantidades de datos. Pero no olvides que lo que importa en el rendimiento de tu juego no es tanto el patrón arquitectónico sino la complejidad algorítmica y el rendimiento del código que escribes. en el contexto del desarrollo de Unity Paralelización más fácil del procesamiento de datos Dado que la lógica está separada en un procesador de datos separado y los datos son en realidad una secuencia lineal, podemos paralelizar el procesamiento dentro de un sistema sin ningún problema. Esto es muy importante si el sistema procesa una gran cantidad de entidades simultáneamente y no se cruzan entre sí de ninguna manera. Puede ir aún más lejos y enviar a diferentes subprocesos la lógica que no se superpone con los datos modificados. Sin embargo, es mucho más difícil de controlar y monitorear. Aún así, habrá un cuello de botella en la sincronización con el hilo principal para preparar los datos. Además, puede resultar que la sobrecarga de preparación y distribución de datos entre subprocesos sea mayor que el tiempo de ejecución del código en sus sistemas. Por lo tanto, debe evaluar si vale la pena. Es muy fácil trabajar con datos limpios En casi todos los juegos de Unity, debemos guardar, cargar o serializar algo para enviarlo a través de la red. Esto es mucho más fácil cuando los datos están separados de la lógica. No hay necesidad de pensar, "¿Cómo debería esto entrar en datos privados..." y llamar a algunos métodos especiales para una serialización adecuada. Simplemente guarde/cargue los componentes necesarios en la entidad. Luego el sistema lo completará hasta el estado deseado si lo considera necesario. Puede cambiar los marcos de trabajo de ECS con la frecuencia que desee Los marcos ECS son similares entre sí porque los principios son los mismos. Un desarrollador que ha reconstruido su cerebro para ECS y ha entendido bien un marco una vez puede trabajar con otro marco ECS sin ningún problema. Aprender la API y las peculiaridades de un marco en particular solo llevará tiempo. Pero no habrá necesidad de reconstruir tu cabeza para el nuevo enfoque. Contras de ECS en Unity Como puede ver, ECS en Unity tiene muchas ventajas valiosas sobre otros patrones. Ahora analicemos las desventajas de ECS en Unity. Un umbral alto para desarrolladores experimentados de Unity Aunque el concepto ECS se puede describir en una oración, aprender a usarlo correctamente puede requerir mucha práctica. ECS requiere que olvide todo lo que sabía sobre diseño antes: todas sus jerarquías de herencia vertical, que el comportamiento de un objeto está determinado por su interfaz, que un objeto es algo concreto e inmutable, que un objeto puede tener un espacio privado y que la lógica puede que te llamen donde quieras. En ECS no todo es así. Es lo contrario de lo descrito anteriormente. Aquí todos los datos están abiertos, todas las entidades son abstractas y muy dinámicas, sus propiedades están en un plano y son accesibles para todos, la lógica funciona según el principio del transportador y el comportamiento de las entidades en general cambia sobre la marcha en función de los datos. La cohesión de código débil puede ser un problema Suponga que de repente necesita una interacción cercana entre 2 entidades concretas (por ejemplo, un cuerpo de oruga y una torreta de tanque). En ese caso, se enfrenta al problema de que las entidades son abstractas y no puede garantizar en el nivel del compilador que el cuerpo de la oruga estará en el otro extremo. Esto se interpondrá en el camino porque los juegos de Unity son un lugar donde hay muchas interacciones cercanas, y siempre desea tener una referencia directa con garantía de propiedades y comportamiento. Deberá verificar la presencia del componente y manejar de alguna manera su ausencia, acceder al componente desde la entidad para comenzar a interactuar con él, etc. Acceda a cualquier dato desde cualquier lugar El mundo ECS es una caja abierta de entidades con datos disponibles para todos los componentes. Al igual que la cohesión de código débil anterior, esto es tanto una ventaja como una desventaja de ECS. Por un lado, es tremendamente conveniente. No es necesario que descubra cómo eludir el marco autolimitante creado anteriormente en el proceso de diseño ("X no debe saber acerca de Y") y sacar al público datos previamente ocultos para resolver algún problema inmediato. Por otro lado, cualquier programador inexperto intentará cambiar datos de donde no debería estar. Pero por lo general, el trabajo en equipo implica confiar en el trabajo de los demás, así que confía pero verifica ;) Los sistemas funcionan exclusivamente en flujo, uno tras otro Al seguir correctamente los principios de ECS, no debe llamar a la lógica de un sistema dentro de otro sistema. Los sistemas no deberían ser conscientes de la existencia de los demás en absoluto. De lo contrario, causará una cohesión de código innecesaria y potencialmente dañará su proyecto. Sin embargo, esta limitación puede ser inconveniente y, en ocasiones, conducirá a varias soluciones alternativas que no violarán los principios de ECS. Si aún necesita llamar a algún código aquí y ahora, simplemente cree un objeto normal con métodos y colóquelo en un componente, no se torture. No funciona bien con la lógica recursiva. Este inconveniente es consecuencia del anterior. Debido a la falta de capacidad para llamar al código de sistemas fuera del subproceso y donde queramos, ECS hace que sea casi imposible crear código recursivo fuera de un sistema en particular. Como solución a esta deficiencia (también conocida como solución alternativa para cumplir con los principios de ECS), solo puedo proponerle que cree una estructura/sistema especializado que llamará a una lista específica de sistemas en un ciclo infinito siempre que se cumpla una condición particular. Quiero decir, siempre que haya entidades con un DoActionComponent. Si tiene soluciones alternativas más elegantes, me encantaría leer sobre ellas en los comentarios :) El orden de ejecución de los sistemas es crítico En ECS, es crucial comprender y controlar cómo los sistemas modifican sus datos. A menudo es posible pasar por alto el efecto de algún sistema en los datos con los que estamos trabajando y terminar con varios efectos secundarios no planificados. Por cierto, pueden ser complicados de rastrear (que es el siguiente inconveniente). Sin embargo, a menudo es posible, al escribir sistemas, diseñarlos de tal manera que no importe en qué orden se invocan los sistemas. Más difícil de depurar Este es un punto bastante controvertido, especialmente con los IDE inteligentes modernos. Debido a la falta de StackTrace profundo (tenemos una lógica en nuestros sistemas que no está vinculada a la entidad) y la imposibilidad de rastrear cómo y quién cambió los datos y el estado de la entidad, puede ser un desafío encontrar la razón por la cual su sistema de repente no funciona de la forma en que se pretendía. No es fácil entender qué condujo a una llamada en particular, aunque alguien simplemente agregó un componente a la entidad o hizo un ++ extra. En resumen, en ECS, sin herramientas de depuración, es difícil rastrear por qué y cómo cambiaron los datos en los componentes, especialmente cuando tiene miles de entidades y solo una problemática. Esto se puede remediar con las herramientas de depuración que pueden proporcionar los marcos. Pero es posible que no estén disponibles desde el primer momento y que tenga que escribirlos usted mismo o sufrir. Horrible opción para estructuras de datos, especialmente las jerárquicas. Implementar estructuras de datos con ECS es difícil, inconveniente y, en mi opinión, no tiene sentido. No digo que sea imposible (si te esfuerzas lo suficiente, todo es posible), pero será un camino espinoso sin muchos beneficios al final del camino, así que sé racional en tu elección. Enumeraré algunos problemas que interferirán al intentar realizar alguna estructura de datos en ECS: En ECS, se puede acceder a todos los datos desde cualquier lugar. Esto puede ser extremadamente peligroso para las estructuras de datos donde se requiere la máxima consistencia. Cualquier "cocodrilo" que pase puede cambiar cualquier dato interno para eludir su lógica, rompiendo completamente su estructura de datos. Si seguimos honestamente los principios de ECS, no podemos invocar la lógica de nuestra estructura de datos aquí y ahora, como suele ser necesario cuando trabajamos con ellos. Sin embargo, este punto se puede combatir con utilidades/extensiones estáticas. ECS es representativo de arquitecturas horizontales. Todos los datos que contiene se encuentran en un plano, casi siempre solo matrices unidimensionales de componentes. Esto lo hace difícil si su estructura de datos requiere verticalidad/jerarquía. No es raro que las estructuras de datos requieran referencias cruzadas entre elementos (jerarquía). Pero, como recordarán, todo gira en torno a una Entidad máximamente abstracta en ECS. Hace que sea un desafío trabajar porque no hay garantía de un elemento del tipo que necesitamos en el otro extremo. Como resultado, tendrá que manejarse por separado. La estructura de datos y sus elementos generalmente no necesitan cambiar el formato de datos en tiempo de ejecución, ni tampoco necesitan combinatoria. Son bastante rígidos. Cada entidad de estructura de datos puede terminar teniendo un solo componente. Suponga que todavía necesita una estructura de datos. En ese caso, le recomiendo que lo cree como un objeto separado con métodos y luego coloque este objeto en su componente y simplemente trabaje con él desde los sistemas como de costumbre. Más archivos y clases En , la cantidad de archivos en un proyecto crece más rápido que en el caso de un código similar en los enfoques clásicos. Al menos porque en lugar de 1 clase con datos y lógica, tiene dos clases: componente y sistema (aún puede ocultarlas en un archivo). A lo sumo, si hace que todos los componentes sean atómicos (1 componente - 1 campo), habrá muchísimos archivos... el enfoque ECS Código repetitivo Este inconveniente depende en gran medida de la implementación específica del marco ECS. En algunos marcos, debe escribir una gran cantidad de código técnico. En otros, el desarrollador trató de hacer la API más simple posible y minimizar la repetición. Pero, si lo compara con otros enfoques, casi siempre hay al menos una pequeña cantidad de código adicional que debe escribir. Me refiero a declarar componentes, obtener un filtro con los componentes necesarios, obtener entidades de él, obtener un componente de una entidad, etc. Pequeña conclusión Este es el final de la 1 parte. En la 2 parte, discutiré: Errores de novato en ECS Buenas prácticas en ECS Frameworks para trabajar con ECS en Unity/C# Si tienes alguna pregunta, ¡déjala en los comentarios!