¡Hola, Hackernon! Hoy sigo discutiendo el ECS (Entidad-Componente-Sistema) en el desarrollo de Unity. En la , analicé qué es ECS (Entity-Component-System), por qué se necesita ECS, cómo trabajar con ECS, las ventajas de ECS en Unity y los contras de ECS en . guía sobre ECS (Entity-Component-System) en Unity: parte 1 Unity En esta parte, me centraré en los errores de los principiantes y las buenas prácticas al usar ECS en el juego Unity. También cubriré ligeramente los marcos para Unity/C#. Errores de principiante al usar ECS en el juego Unity En esta sección, les contaré sobre los errores que las personas más experimentadas notaron en mi código anterior y que me impidieron desarrollar. También cubriré los errores que cometen muchos principiantes cuando comienzan a dominar ECS. Espero que esto te ayude a evitar algunos de los errores. Herencia de componentes e interfaces Heredar componentes y usar interfaces es un error generalizado que cometen los principiantes. Entonces, ¿por qué es malo para el desarrollo de Unity? 4 razones: La abstracción a nivel de componente no proporciona absolutamente ningún beneficio en comparación con la abstracción de ECS a través de datos Crea limitaciones para usted: le resultará más difícil ampliar dichos componentes y refinar la lógica asociada Conduce a una situación en la que un componente tiene su propia lógica, lo que, como recordamos, es una violación de los principios de ECS, que no debe hacer. Conduce a una necesidad irrazonable de heredar sistemas o hacer todo tipo de cambios de casos por tipos. Señalaré de inmediato que heredar sistemas no siempre es una buena idea, pero no hace nada terrible, a diferencia de la herencia de componentes. Entonces, si desea heredar componentes, no lo haga. Piensa de nuevo en cómo puedes resolver el problema de otra manera. Incapacidad para usar correctamente la abstracción ECS La abstracción de ECS es cuando simplemente coloca datos comunes (que deben heredarse en OOP) en un componente separado. Usted crea un "heredero" de dicho componente simplemente agregando un nuevo componente con los datos que necesita y filtrando entidades con e . Todo es elemental. Si tiene algunos datos comunes entre componentes/entidades, casi siempre puede ponerlos en un componente separado. Cuanto antes lo hagas, mejor. BaseComponent InheritorComponent Habilitar/deshabilitar sistemas para cambiar la lógica En ECS, el mundo y los sistemas que lo procesan son estáticos y siempre existen, pero las entidades y sus datos son muy dinámicos. Y si necesita deshabilitar alguna lógica, deshabilitar un sistema no es la solución correcta. A menudo no hay acceso a otros sistemas (y eso es algo bueno). Una opción mucho más práctica es crear algunos componentes de marcador. Dirá que la lógica del sistema no debería funcionar para una entidad con un marcador. Muchos recién llegados afirman: "Pero si no tengo entidades para el sistema, ¿por qué debería funcionar el sistema? ¿No sería mejor desactivarlo en aras de la optimización?". No, no es mejor. Si admite que puede que no haya entidades, es más fácil agregar hasta el principio del método principal del sistema. En la escala del , llamar a una función y comparar dos enteros es una gota en el océano que no afectará tu rendimiento de ninguna manera. if (entities.Length < 1) return; juego Unity Los únicos casos legítimos de deshabilitación de sistemas en tiempo de ejecución son las pruebas A/B y la depuración/prueba de sistemas específicos. Sin embargo, la mayoría de los marcos proporcionan herramientas para hacer esto no desde el código sino desde la ventana del editor. Haciendo de ECS un absoluto Cabe recordar que la programación orientada a objetos no está prohibida para los seguidores de ECS :D Cuando trabaje con ECS, no debe obsesionarse por completo con ECS y transferirle todo porque puede ser contraproducente. Además, como mencioné en las : no todas las estructuras de datos encajan bien en ECS. Entonces, es mejor hacer una clase OOP separada en tales casos. desventajas de ECS Algunos van más allá y crean algunos elementos del proyecto (por ejemplo, la interfaz de usuario) no de acuerdo con ECS, sino un poco apartados de cualquier otra forma conveniente y luego se conectan con ECS mediante algún puente. Además, toda la lógica adicional (cargar configuraciones, trabajar directamente con la red, guardar en un archivo) es más fácil de hacer OOP y trabajar con ella directamente desde el sistema deseado. desarrolladores de Unity Debe elegir qué hacer según el sentido común. ECS debe ayudar al desarrollo, no obstaculizarlo. Intentando portar el código existente a ECS sin ningún cambio Muy a menudo, los principiantes intentan transferir su código existente palabra por palabra a ECS. Esta no es una buena idea porque los enfoques de ECS para escribir código difieren de los patrones arquitectónicos tradicionales. El resultado de dicha migración suele ser un desastre de ECS y un código final muy pobre. Si necesita migrar código antiguo a ECS, la mejor opción es escribir la misma lógica desde cero en ECS. Todo lo que necesita hacer es usar su conocimiento y el código existente como guía. Uso de delegados/devoluciones de llamada o lógica reactiva en sistemas En ECS, puede ser peligroso tomar parte de la lógica de los sistemas y almacenarla en un componente para su uso posterior o para reaccionar instantáneamente a algún cambio (por ejemplo, un sistema que reacciona al agregar un componente en otro sistema). No solo agrega interconectividad innecesaria a los sistemas (se vuelven muy dependientes de las llamadas externas). También rompe nuestra hermosa canalización de procesamiento de datos al agregar lógica sobre la que tenemos poco control sobre las llamadas. Organización de archivos en carpetas por tipo Cuando comienza a trabajar con ECS, primero desea colocar archivos nuevos por tipo: componentes en la carpeta Componentes y sistemas en la carpeta Sistemas. Pero con la experiencia, me doy cuenta de que esta forma de clasificar está lejos de ser eficiente. No es fácil navegar con él para comprender qué componentes están relacionados con qué sistemas. La mejor opción es ordenar por funciones cuando todo lo relacionado con una función en particular está en una carpeta (quizás con una jerarquía interna de componentes/sistemas). Es decir, todos los componentes y sistemas relacionados con la salud y el daño estarán en la carpeta Salud. Esto permitirá mirar la carpeta para comprender el contexto de datos básicos de los sistemas que contiene y facilitar la navegación por el proyecto. Mejores prácticas para usar ECS en el juego Unity Anteriormente, probablemente haya leído exactamente lo que NO debe hacer al desarrollar juegos de Unity en ECS. Ahora hablemos de prácticas útiles y consejos sobre lo que puede hacer. Etiquetado de entidades con componentes de marcador En ECS, existe un concepto como componente de etiqueta. Es un componente sin campo que solo realiza la función de etiquetado de entidades. Puede considerarlo como una bandera booleana en una clase: está allí (verdadero) o no está (falso). Por ejemplo, tenemos mil unidades, una de las cuales controla un jugador. Puede marcarlo con un componente vacío. Esto le permitirá obtener solo las unidades de jugador en el filtro, así como comprender si está trabajando con una unidad regular o controlada por jugador cuando revisa todas las unidades a la vez. PlayerMarker Minimizar los lugares donde cambia el componente Cuantos menos lugares cambie un componente, mejor. En general, se trata simplemente de seguir el principio beneficioso de No repetirse (se lo recomiendo a todo el mundo). Las ventajas de practicar esto son muchas: Le permite comprender mejor el proceso de cambio de datos en su proyecto y simplifica la depuración si algo sale mal. Al actualizar la lógica de los cambios de datos, deberá actualizar menos código, idealmente solo en un lugar Simplemente hay menos posibilidades de permitir que ocurra un error de datos sobre la marcha Por ejemplo, en lugar de cambiar el HealthComponent en cada sistema que tenga daños, sería mejor crear un DamageSystem cuyo propósito sea hacer daño a las entidades con un HealthComponent. Olvidarse del postfijo del componente El componente postfix es muy útil para los principiantes porque les recuerda que "solo hay datos aquí". Pero con el tiempo, la necesidad de recordar desaparece y el código sigue siendo difícil de entender con el Componente omnipresente. Es por eso que me gustaría aconsejar: puede olvidarse con seguridad del sufijo Componente. No proporciona nada útil excepto, quizás, alguna simplificación de la búsqueda en IntelliSense. Es solo un consejo y tal vez incluso una cuestión de gusto, así que depende de ti cómo lo enfrentes :) Por ejemplo, se convierte simplemente en y el código se convierte en . En este caso, permanece sin cambios porque el sufijo lleva información útil en este caso, notando que no es un componente simple. HealthComponent Health entity.Has<Health>() PlayerMarker Reactividad retardada y componentes de cuadro único La reactividad en ECS puede ser dañina. Pero, ¿qué hacer cuando se requiere reactividad? La respuesta es la reactividad retardada. La reactividad retrasada es cuando en lugar de llamar a la lógica directamente en el momento del evento, crea datos de que el evento ha ocurrido y todos simplemente reaccionarán al evento en el momento que quieran. Puedo dibujar una analogía con la bandera sucia en OOP, donde cualquiera puede declarar un evento , pero la lógica reaccionará a ese evento cuando lo crea conveniente. SetDirty(true) En ECS, simplemente crea un componente con o sin datos (solo puede agregar un indicador booleano a un componente existente) que el sistema procesará cuando sea el momento. No es raro que tales componentes existan en el mundo para que un solo marco alerte a todos los sistemas pero no repita la lógica en el siguiente marco. La eliminación puede ser manejada por el sistema que genera el evento o por algún sistema separado que eliminará todos los componentes de tipo X donde desee. Por ejemplo, tiene un . Para decirle cuánto daño debe hacer, declara con la cantidad de daño y lo agrega a la entidad que debe recibir el daño. recorre todas las entidades con y , daña la entidad, elimina y crea un que informa a todos los sistemas después de de la entidad dañada. Al final del marco, el sistema individual elimina el para que los sistemas no vuelvan a procesar este marcador en el siguiente marco. DamageSystem MakeDamageComponent DamageSystem HealthComponent MakeDamageComponent MakeDamageComponent DamagedEntityMarker DamageSystem DamagedEntityMarker Solicitudes/Eventos como API para sistemas Desarrollando la idea de componentes de un solo marco, podemos usarlos para expresar una especie de API para sistemas. Deberíamos distinguir los componentes de solicitud para solicitudes externas y los componentes de evento para notificar a todos sobre el evento. El propio sistema puede controlar el ciclo de vida de ambos componentes. Es posible eliminar solicitudes inmediatamente después de procesar y limpiar eventos antes de lanzar nuevos eventos. Depende de usted cómo nombrarlos exactamente y si desea agregar el sufijo Solicitudes/Eventos. Por ejemplo, tienes el del párrafo anterior. Puede expresarle una solicitud de daño usando el componente y usar el componente para notificar a otros sistemas. La lógica dentro del sistema es la siguiente: borre todos del último cuadro, dañe todas las entidades con una solicitud, elimine la solicitud y agregue el componente . DamageSystem MakeDamageRequest DamagedEntityEvent DamagedEntityEvent DamagedEntityEvent Almacenamiento de una referencia a otra entidad dentro de un componente Probablemente tenga una pregunta "¿Cómo construir enlaces entre entidades en ECS? ¿Es necesario marcar entidades con componentes y luego buscarlos en un bucle?". Por supuesto que no. Todo es mucho más simple y más común: solo guardamos la referencia. La única diferencia es que la referencia no es a un componente de otra entidad que nos interesa, sino a la entidad misma. Entonces, agrega un campo con la entidad (o cualquier forma en que su marco almacene entidades) al componente. Antes de usarlo, verifica que la entidad esté viva y tenga el componente deseado. Luego obtienes este componente y trabajas con él como quieras. Por ejemplo, puede hacer no directamente en la entidad sino ejecutarlo como un evento de entidad separado con referencia a la entidad de destino. Para hacer esto, agrega el campo a y vuelve a trabajar en . Ahora debe pasar por todas las solicitudes, verificar que el objetivo sea una entidad viva con un , obtener el y causar daños. MakeDamageRequest Entity target MakeDamageRequest DamageSystem HealthComponent HealthComponent Ejecutar ahora también se verá diferente. En lugar de agregar el componente directamente a la entidad, crea una nueva entidad con y especifica el . De esta manera, a expensas de la conveniencia del filtrado, puede desencadenar varios eventos de daño diferentes para una sola entidad . MakeDamageRequest MakeDamageRequest target target Mover la lógica repetitiva a StaticUtils/Extensions Con el tiempo, comienza a notar que está ejecutando la misma lógica en diferentes sistemas. Por lo general, es una señal de que es hora de hacer un nuevo sistema: D Pero sucede que la lógica repetida es secundaria, relacionada con uno o dos componentes/entidades específicas, y su resultado se utiliza para diferentes propósitos. Digamos, una interpretación especial de los datos en un componente. Algunos desarrolladores permiten declarar dicha lógica adicional directamente en el componente (en forma de captadores, por ejemplo). Pero para evitar violar ECS, sugiero otra opción: utilidades estáticas (o Extensiones en C#) que llamamos desde los sistemas. Por ejemplo, tenemos un . En su interior se ubica un color del equipo. Es posible que sea necesario comprobar que dos entidades pertenecen al mismo equipo en varios sistemas. Entonces creamos una clase estática y un método en ella , que describe la lógica recurrente de comparar equipos para dos entidades. InTeamComponent TeamUtils IsInSameTeam(Entity, Entity) Agrupación de sistemas por el momento de ejecución Como ya sabe, el orden en que se llama a los sistemas es fundamental en ECS. Por lo tanto, es conveniente agrupar los sistemas en el nivel superior por el orden en que se llaman en un marco. Por ejemplo, los primeros sistemas que se llamarán en un marco podrían ser todos los sistemas relacionados con la entrada. Recopilarán la entrada del usuario y la prepararán en formato ECS. El segundo en línea será un grupo de sistemas con lógica de juego, que interpretará los datos de entrada a su manera y actualizará el mundo ECS. Y, por último, podemos tener un grupo de sistemas responsables del renderizado o simplemente varias cosas adicionales que deberían llamarse después de toda la lógica del juego. Separación de características principales en ensamblajes separados Este enfoque separará las funciones entre sí y controlará sus dependencias. En un mundo ideal, no deberían superponerse en absoluto. El orden entre las características no debe ser importante. También debe haber un ensamblaje central donde se deben ubicar los componentes que necesitan todas las características para funcionar. ¿Debo dividir componentes/sistemas en partes más pequeñas en ECS? Esta pregunta es muy interesante ya que los desarrolladores de Unity con diferentes experiencias tienen diferentes opiniones al respecto. Pero intentaré cubrir ambas respuestas para ayudarlo a comprender sus necesidades. Sí, siempre es necesario dividir componentes Este enfoque en la organización ECS se puede llamar atómico. El grado superior de este enfoque es que cada componente tiene un solo campo. Esto nos permitirá alcanzar el apogeo de la combinatoria del proyecto y eliminar la necesidad de refactorizar en nombre de la abstracción ECS. Ya no podemos pensar en "cómo combinar entidades con la propiedad X". Desventajas de dividir siempre los componentes en ECS: La cantidad de clases y archivos aumentará, lo que puede generar confusión en proyectos grandes si no presta la debida atención a la organización del proyecto. La cantidad de componentes (en general o por entidad) puede afectar el rendimiento de su marco Es más difícil entender qué es una entidad por un montón de propiedades (que se pueden resolver con un marcador con un nombre normal) Ahora pasemos a la segunda opinión. No, debe dividir los componentes solo cuando sea necesario Este principio se llama "No dividirse antes de tiempo". Este es el principio al que me adhiero personalmente. Cuando necesita dividir datos, la métrica es simple: ¿estos datos se usan/planean usar en otro lugar además de este componente? Si no, no hay motivo para dedicarle tiempo. puede adoptar un enfoque similar para particionar la lógica en sistemas. Desventajas de dividir componentes en ECS solo cuando sea necesario: Todavía habrá que dedicar tiempo a la abstracción de ECS Menos libertad para diseñar entidades Depende de ti elegir tu lado :) Marcos para Unity/C# Si es un principiante, asumiré que primero está pensando en aprender Unity DOTS. Pero quiero advertirte. Unity DOTS no es una buena opción para principiantes porque es enorme y complejo. No hay tanta documentación y gente con experiencia como nos gustaría. Además, no es muy compatible con el antiguo código de Unity (todo esto puede cambiar después de publicar este artículo). Si te gusta Unity Editor y ya estás acostumbrado a colgar componentes directamente en GameObjects, entonces es tu mejor opción. Ofrece API simple, estrecha integración con Unity Editor (puede funcionar fuera de Unity) y un trabajo conveniente con MonoBehaviour. Es simple y conveniente. Todo lo que necesitas para trabajar con Unity está en él. Simplemente lo instalas y trabajas con él. Su principal desventaja es que requiere un pagado para trabajar en Unity por completo. Morpeh Inspector Odin Entitas y DOTS (Unity ECS): ¿deberías dominarlos? Y ahora una breve revisión de los marcos de trabajo de Unity/C# que he conocido personalmente y las ventajas y desventajas que he notado. Vale la pena señalar que todo lo que se describe a continuación puede cambiar desde la publicación de este artículo, por lo que es mejor verificar los marcos usted mismo. Entitas Es el marco ECS más antiguo para Unity/C# y sigue siendo el más popular. Es el que se ve con más frecuencia en las ofertas de trabajo con ECS. Pros de las entidades: Great WorldViewer en el editor de Unity Estilo de código fluido (gracias a la generación de código) buena documentacion Comunidad muy grande Muchos proyectos exitosos en él. Soporte obligatorio del desarrollador (gracias a AssetStore) Se puede usar fuera de Unity en C# puro Entitas contras: Rendimiento deficiente en relación con otros marcos ECS (pero aún mejor que MonoBehaviour) Muchas asignaciones, que afectan negativamente a GC Requiere generación de código para cada cambio en la estructura del componente En proyectos grandes, la API se hincha mucho debido a la generación de código La versión de Github no permite llamar a la generación de código en errores de compilación, mientras que la versión de AssetStore sí. Debes dominarlo, al menos en un nivel básico. PUNTOS (Unidad ECS) Creo que no necesita presentación. DOTS no es un marco, sino una plataforma completa (pila de tecnología) con Unity ECS como marco interno. Me gustaría señalar que la siguiente es una experiencia algo desactualizada. Admito que un DOTS actualizado podría eliminar los problemas que se describen a continuación. Ventajas de DOTS (Unity ECS): Una plataforma completa para el desarrollo en ECS Creado por desarrolladores de motores con la integración más estrecha posible en el editor Admite trabajos y ráfagas Junto con Jobs y Burst, logra el máximo rendimiento entre los marcos ECS. Tiene una excelente biblioteca de red con predicciones NetCode Mecanismo de subescenas Contras de DOTS (Unity ECS): Trabajo en progreso, lo que lleva a romper cambios de código antiguos y errores nuevos Documentación débil, que a menudo no se mantiene al día con los cambios, debe leer el código fuente Requiere escribir más código técnico que los análogos El código es difícil de leer y está lejos de ser conciso No funciona más rápido que las soluciones de código abierto sin Burst/Jobs (y en algunos casos más lento) No encaja bien con el antiguo código de Unity DOTS es esencialmente un tiempo de ejecución, y no toda la funcionalidad anterior se ha portado a DOTS. Esto limita las posibilidades de uso. Conclusión sobre ECS en Unity Como habrá notado en la gran lista de desventajas, ECS no es una panacea. Esta solución arquitectónica, como cualquier otra, tiene sus ventajas y desventajas que hay que afrontar si se opta por desarrollar con este patrón arquitectónico. Por lo tanto, la elección de usar ECS en sus proyectos o no depende totalmente de usted. Le recomiendo que al menos intente hacer un pequeño proyecto en ECS para comprender si le gusta este enfoque o no. También te recomiendo que le eches un vistazo a este , donde hay respuestas a muchas preguntas relacionadas con ECS. Encontrará enlaces a informes, una lista de marcos para diferentes idiomas, así como ejemplos de juegos y programas que usan ECS. repositorio Desde mi punto de vista personal, ECS parece una excelente opción para crear juegos de Unity. El desarrollo en él, para mí personalmente, es un placer: estás desarrollando un juego, no tratando de descubrir cómo integrar código nuevo en un sistema antiguo sin romper nada. Vale la pena tener en cuenta que la usabilidad del desarrollo de ECS se ve muy afectada por la elección del marco, así que pruebe diferentes opciones y elija con cuidado. Basado en mi experiencia, tiendo a pensar que ECS (o su variación) es el futuro del desarrollo de juegos interactivos. Y no solo porque (y tal vez incluso Epic) lo hayan elegido como su enfoque principal, sino simplemente porque ECS tiene beneficios ventajosos en el contexto del desarrollo de juegos. Unity Technologies Y, en general, es un enfoque práctico que parece incómodo al principio, pero vale la pena a largo plazo.