Hola Habr! Os presento la traducción de la wiki del proyecto
Svelto.ECS escrita por Sebastiano Mandalà.
Svelto.ECS es el resultado de muchos años de investigación y aplicación de principios SOLIDOS en el desarrollo de juegos en Unity. Esta es una de las muchas implementaciones del patrón ECS disponible para C # con varias características únicas introducidas para abordar las deficiencias del patrón en sí.
Primer vistazo
La forma más fácil de ver las características básicas de Svelto.ECS es descargar
Vanilla Example . Si desea asegurarse de que sea fácil de usar, le mostraré un ejemplo:
Desafortunadamente, no es posible comprender rápidamente la teoría detrás de este código, que puede parecer simple pero confuso al mismo tiempo. Para comprender esto, debe dedicar tiempo a leer el "muro de texto" y probar los ejemplos anteriores.
Introduccion
Recientemente, he estado discutiendo
mucho sobre
Svelto.ECS con varios programadores más o menos experimentados. Recopilé muchos comentarios e hice muchas notas que usaré como punto de partida para mis próximos artículos, donde hablaré más sobre teoría y buenas prácticas. Un pequeño spoiler: me di cuenta de que cuando comienzas a usar Svelto.ECS, el mayor obstáculo es
cambiar el paradigma de programación . Es sorprendente cuánto tengo que escribir para explicar los nuevos conceptos introducidos por Svelto.ECS, en comparación con la pequeña cantidad de código escrito para desarrollar el marco. De hecho, si bien el marco en sí es muy simple y liviano, la transición de OOP con el uso activo de la herencia o los componentes habituales de Unity al "nuevo" diseño modular y de acoplamiento flexible que Svelto.ECS ofrece para usar evita que las personas se adapten al marco.
Svelto.ECS se utiliza activamente en
Freejam (nota del traductor: el autor es el director técnico de esta empresa). Como siempre puedo explicar a mis colegas los conceptos básicos del marco, les lleva menos tiempo entender cómo trabajar con él. Aunque Svelto.ECS es lo más duro posible, los malos hábitos son difíciles de superar, por lo que los usuarios tienden a abusar de cierta flexibilidad que les permite adaptar el marco a los paradigmas "antiguos" con los que se sienten cómodos. Esto puede conducir al desastre debido a malentendidos o distorsiones de los conceptos que subyacen a la lógica del marco. Es por eso que tengo la intención de escribir tantos artículos como sea posible, especialmente porque estoy seguro de que el paradigma ECS es la mejor solución en este momento para escribir código efectivo y mantenible para grandes proyectos que cambian y vuelven a funcionar muchas veces durante varios años.
Robocraft y
Cardlife son prueba de esto.
No voy a hablar mucho sobre las teorías que subyacen a este artículo. Solo le recordaré por qué me negué a usar el
contenedor IoC y comencé a usar exclusivamente el marco ECS: el contenedor IoC es una herramienta muy peligrosa si se usa sin comprender la esencia misma de la inversión de control. Como puede ver en mis artículos anteriores, distingo entre la inversión del control de creación (Inversión del control de creación) y la inversión del control de flujo (Inversión del control de flujo). Invertir el control de flujo es como el principio de Hollywood: "No nos llame, lo llamaremos". Esto significa que las dependencias inyectadas nunca deben usarse directamente a través de métodos públicos, ya que al hacerlo simplemente usa el contenedor IoC como sustituto de cualquier otra forma de inyección global, como singleton. Sin embargo, si el contenedor de IoC se usa sobre la base de la Inversión de la Administración (IoC), básicamente todo se reduce a reutilizar el patrón "Método de plantilla" para implementar gerentes que se usan solo para registrar los objetos que administran. En el contexto real de las inversiones de control de flujo, los gerentes siempre son responsables de administrar las entidades. ¿Esto parece un patrón de ECS? Por supuesto Basado en este razonamiento, tomé el patrón ECS y desarrollé un marco rígido basado en él, y su uso equivale a aplicar el nuevo paradigma de programación.
Composición Raíz y Motores Raíz
La clase principal es la raíz de composición de la aplicación. La raíz de la composición es el lugar donde se crean e implementan las dependencias (hablé mucho sobre esto en mis artículos). Una raíz de composición pertenece a un contexto, pero un contexto puede tener más de una raíz de composición. Por ejemplo, la Fábrica es la raíz de la composición. Una aplicación puede tener más de un contexto, pero este es un escenario avanzado, y en este ejemplo no lo consideraremos.
Antes de sumergirnos en el código, conozcamos las primeras reglas del lenguaje Svelto.ECS. ECS es la abreviatura Entity Component System. La infraestructura de ECS ha sido bien analizada en artículos por muchos autores, pero si bien los conceptos básicos son comunes, las implementaciones varían ampliamente. En primer lugar, no existe una forma estándar de resolver algunos problemas que surgen cuando se utiliza código orientado a ECS. Es con respecto a este tema que hago la mayor parte de mis esfuerzos, pero hablaré sobre esto más adelante o en los siguientes artículos. La teoría se basa en los conceptos de Esencia, Componentes (entidades) y Sistemas. Aunque entiendo por qué la palabra Sistema se usó históricamente, desde el principio no lo consideré lo suficientemente intuitivo para este propósito, por lo que utilicé el Motor como sinónimo del Sistema y usted, según sus preferencias, puede usar uno de estos términos.
La clase EnginesRoot es el núcleo de Svelto.ECS. Con su ayuda, puedes registrar motores y diseñar toda la esencia del juego. Crear motores dinámicamente no tiene mucho sentido, por lo que todos deben agregarse a la instancia de EnginesRoot desde la misma raíz de la composición donde se creó. Por razones similares, una instancia de EnginesRoot nunca se debe implementar, y los motores no se deben eliminar después de que se hayan agregado.
Para crear e implementar dependencias, necesitamos al menos una raíz de la composición. Sí, en una aplicación puede existir más de un EnginesRoot, pero no trataremos esto en el artículo actual, que trato de simplificar lo más posible. Así es como se ve la raíz de la composición con la creación del motor y la inyección de dependencia:
void SetupEnginesAndEntities() {
Este código es del ejemplo Survival, que ahora se comenta y cumple con casi todas las reglas de buenas prácticas que propongo aplicar, incluido el uso de lógica de motor probada e independiente de la plataforma. Los comentarios lo ayudarán a comprender la mayoría de ellos, pero un proyecto de este tamaño puede ser difícil de entender si es nuevo en Svelto.
Entidades
El primer paso después de crear la raíz vacía de la composición y una instancia de la clase EnginesRoot es identificar primero los objetos con los que desea trabajar. Es lógico comenzar con Entity Player. La esencia de Svelto.ECS no debe confundirse con el Unity Game Object (GameObject). Si lee otros artículos relacionados con ECS, podría ver que en muchos de ellos, las entidades a menudo se describen como índices. Esta es probablemente la peor forma de introducir el concepto de ECS. Aunque es cierto para Svelto.ECS, está oculto en él. Quiero que el usuario de Svelto.ECS represente, describa e identifique cada entidad en términos del lenguaje del Dominio del diseño del juego. La entidad en el código debe ser el objeto descrito en el documento de diseño del juego. Cualquier otra forma de definición de entidad conducirá a una forma descabellada de adaptar sus puntos de vista antiguos a los principios de Svelto.ECS. Sigue esta regla fundamental y no te equivocarás. La clase de entidad en sí no existe en el código, pero aún no debe definirla de manera abstracta.
Motores
El siguiente paso es pensar en qué comportamiento preguntar a las Entidades. Cada comportamiento siempre se modela dentro del motor; no puede agregar lógica a ninguna otra clase dentro de la aplicación Svelto.ECS. Podemos comenzar moviendo el personaje del jugador y definir la clase
PlayerMovementEngine . El nombre del motor debe estar muy enfocado, porque cuanto más específico sea, más probable es que el motor siga la Regla de responsabilidad única. El nombre apropiado de la clase en Svelto.ECS es fundamental. Y el objetivo no es solo mostrar claramente sus intenciones, sino también ayudarlo a "verlas" usted mismo.
Por la misma razón, es importante que su motor se encuentre en un espacio de nombres muy especializado. Si define espacios de nombres de acuerdo con la estructura de carpetas, adáptese a los conceptos de Svelto.ECS. El uso de espacios de nombres específicos ayuda a detectar errores de diseño cuando las entidades se usan dentro de espacios de nombres incompatibles. Por ejemplo, no se supone que se usará ningún objeto enemigo dentro del espacio de nombres del jugador, a menos que el objetivo sea romper las reglas asociadas con la modularidad y el acoplamiento débil de los objetos. La idea es que los objetos de un espacio de nombres en particular solo se pueden usar dentro de él o en el espacio de nombres padre. Usar Svelto.ECS es mucho más difícil de convertir su código en espagueti, donde las dependencias se inyectan de derecha a izquierda, y esta regla lo ayudará a elevar el nivel de calidad del código aún más cuando las dependencias se abstraen correctamente entre clases.
En Svelto.ECS, la abstracción avanza unas pocas líneas, pero ECS esencialmente ayuda a extraer datos de la lógica que debería procesar los datos. Las entidades están determinadas por sus datos, no por su comportamiento. En este caso, los motores es un lugar donde puede colocar el comportamiento conjunto de entidades idénticas para que los motores siempre puedan trabajar con un conjunto de entidades.
Svelto.ECS y el paradigma ECS permiten que el codificador logre uno de los santos griales de la programación pura, que es la encapsulación ideal de la lógica. Los motores no deben tener funciones públicas. Las únicas funciones públicas que deben existir son aquellas que son necesarias para implementar las interfaces del marco. Esto lleva a olvidar la inyección de dependencia y ayuda a evitar el código incorrecto que ocurre cuando se usa la inyección de dependencia sin inversión de control. Los motores NUNCA deben estar integrados en ningún otro motor o cualquier otro tipo de clase. Si cree que desea implementar el motor, simplemente comete un error fundamental en el diseño del código.
En comparación con Unity MonoBehaviours, los motores ya muestran la primera gran ventaja, que es la capacidad de acceder a todos los estados de entidades de este tipo desde la misma área de código. Esto significa que el código puede usar fácilmente el estado de todos los objetos directamente desde el mismo lugar donde se ejecutará la lógica del objeto común. Además, los motores individuales pueden procesar los mismos objetos para que el motor pueda cambiar el estado del objeto, mientras que el otro motor puede leerlo, utilizando efectivamente dos motores para la comunicación a través de los mismos datos de entidad. Se puede ver un ejemplo mirando los
motores PlayerGunShootingEngine y
PlayerGunShootingFxsEngine . En este caso, dos motores están en el mismo espacio de nombres, por lo que pueden compartir los mismos datos de entidad.
PlayerGunShootingEngine determina si un jugador (enemigo) ha sido dañado y escribe el valor
lastTargetPosition del componente
IGunAttributesComponent (que es un componente
PlayerGunEntity ).
PlayerGunShootFxsEngine procesa los efectos gráficos del arma y lee la posición del objetivo seleccionado por el jugador. Este es un ejemplo de interacción entre motores a través del sondeo de datos. Más adelante en este artículo, mostraré cómo permitir que un mecanismo se comunique entre ellos
empujando datos (empuje de datos) o
enlace de datos (enlace de datos) . Lógicamente, los motores nunca deberían almacenar estado.
Los motores no necesitan saber cómo interactuar con otros motores. La comunicación externa se produce a través de la abstracción, y Svelto.ECS resuelve la conexión entre los motores de tres formas oficiales diferentes, pero hablaré de esto más adelante. Los mejores motores son aquellos que no requieren comunicaciones externas. Estos motores reflejan un comportamiento bien encapsulado y generalmente funcionan a través de un bucle lógico. Los bucles siempre se modelan utilizando tareas Svelto.Task dentro de las aplicaciones Svelto.ECS. Dado que el movimiento del jugador debe actualizarse en cada tic físico, sería natural crear una tarea para realizar en cada tic físico. Svelto.Tasks le permite ejecutar cada tipo de
IEnumerator en varios tipos de planificadores. En este caso, decidimos crear una tarea en
PhysicScheduler , que le permite actualizar la posición del jugador:
public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() {
Las tareas de Svelto.Tasks se pueden realizar directamente o mediante objetos
ITaskRoutine . No hablaré mucho sobre Svelto. Tareas aquí, ya que escribí otros artículos para ello. La razón por la que decidí usar la rutina de tareas en lugar de iniciar la implementación de IEnumerator directamente es bastante discrecional. Quería mostrar que puedes iniciar un ciclo cuando el objeto de un jugador se agrega al motor y detenerlo cuando se elimina.
Sin embargo, para esto necesita saber cuándo se agrega y elimina un objeto.Svelto.ECS introduce agregar y quitar devoluciones de llamada para saber cuándo se agregan o eliminan ciertas entidades. Esto es algo único en Svelto.ECS, pero este enfoque debe usarse con prudencia. A menudo vi que estas devoluciones de llamada están siendo abusadas, ya que en muchos casos son suficientes para consultar entidades. Incluso tener una referencia de entidad como un campo de motor debe considerarse más como una excepción que como una regla.Solo cuando se utilicen estas devoluciones de llamada, el motor debe heredarse de SingleEntityViewEngine o de MultiEntitiesViewEngine <EntityView1, ..., EntityViewN>. Una vez más, el uso de estos datos debería ser raro, y de ninguna manera tienen la intención de informar qué objetos procesará el motor.Los motores a menudo implementan la interfaz IQueryingEntityViewEngine . Esto le permite acceder y extraer datos de una base de datos de entidades. Recuerde que siempre puede solicitar un objeto desde el interior del motor, pero en el momento en que solicita una entidad que es incompatible con el espacio de nombres donde se encuentra el motor, debe comprender que ya está haciendo algo mal. Los motores nunca deberían suponer que las entidades son accesibles, y deberían funcionar en un conjunto de objetos. No debe suponerse que siempre habrá un solo jugador en el juego, como hago en el ejemplo del código. En EnemyMovementEngine Hay un enfoque muy general sobre cómo solicitar objetos: public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } }
En este caso, el ciclo del motor principal comienza directamente en el planificador predefinido. Marque (). Ejecutar ()muestra la forma más corta de iniciar IEnumerator con Svelto.Tasks. IEnumerator continuará cediendo al siguiente cuadro hasta que se encuentre al menos un objetivo enemigo. Como sabemos que siempre habrá un solo objetivo (otra mala suposición), elijo el primero disponible. Si bien el objetivo de Enemy Target puede ser solo uno (¡aunque podría haber más!), Hay muchos enemigos y, sin embargo, el motor se encarga de la lógica del movimiento para todos. En este caso, hice trampa, ya que en realidad uso el Sistema de malla Unity Nav, así que todo lo que tengo que hacer es configurar el destino en NavMesh. Honestamente, nunca usé el código Unity NavMesh, por lo que ni siquiera estoy seguro de cómo funciona, este código solo se hereda de la demostración original de Survival.Tenga en cuenta que un componente nunca proporciona directamente una dependencia de Navmesh Unity. El componente Entidad, como lo analizaré más adelante, siempre debe exponer los tipos de valor. En este caso, esta regla también le permite mantener el código bajo control, ya que el tipo de valor del campo navMeshDestination puede implementarse más tarde sin usar Unity Nav Mesh.Para completar el párrafo sobre motores, tenga en cuenta que no existe un motor demasiado pequeño. Por lo tanto, no tenga miedo de escribir un motor que contenga varias líneas de código, porque no puede escribir lógica en otro lugar y necesita que sus motores sigan la regla de responsabilidad uniforme.Representaciones de entidades
Antes de eso, introdujimos el concepto del Motor y la definición abstracta de la Esencia, ahora definamos qué es la Representación de la esencia. Debo admitir que de los 5 conceptos en los que se basa Svelto.ECS, las Vistas de entidad son probablemente las más confusas. Anteriormente llamado Nodo (un nombre tomado del marco ECS Ash ), me di cuenta de que el nombre "Nodo" no significaba nada. EntityView también puede ser engañoso porque los programadores típicamente asociados con la representación concepto que emana de la plantilla de controlador de vista modelo(Model View Controller), sin embargo, Svelto.ECS usa View, porque EntityView es como el motor ve a Entity. Me gusta describirlo así porque parece lo más natural, pero también podría llamarlo EntityMap, porque EntityView muestra los componentes de la entidad a los que debe acceder el motor. Este esquema de conceptos Svelto.ECS debería ayudar un poco:
sugiero comenzar con el motor, y ahora estamos en el lado derecho de este esquema. Cada motor tiene su propio conjunto de EntityViews. El motor puede reutilizar EntityViews compatibles con el espacio de nombres, pero la mayoría de las veces el Motor define sus EntityViews. Al motor no le importa si la entidad Player está realmente definida, indica el hecho de que necesita PlayerEntityViewpara el trabajo Escribir el código depende de las necesidades del motor, no debe crear una entidad y su campo antes de comprender cómo usarlos. En un escenario más complejo, el nombre EntityView podría ser aún más específico. Por ejemplo, si tuviéramos que escribir motores complejos para manejar la lógica del jugador y representar gráficos del jugador (o animaciones, etc.), podríamos tener PlayerPhysicEngine con PlayerPhysicEntityView , así como PlayerGraphicEngine con PlayerGraphicEntityView o PlayerAnimationEngine con PlayerAnimationEntityView . Se pueden usar nombres más específicos, como PlayerPhysicMovementEngine o PlayerPhysicJumpEngine (etc.)Componentes
Nos dimos cuenta de que los motores modelan el comportamiento de un conjunto de datos de entidad, y entendemos que los motores no usan entidades directamente, sino que usan componentes de entidad a través de representaciones de entidades. Nos dimos cuenta de que EntityView es una clase que puede contener SOLAMENTE componentes públicos de entidades. También sugerí que los componentes de la entidad siempre son interfaces, así que démos una mejor definición: lasentidades son una colección de datos, y los componentes de la entidad son una forma de acceder a esos datos. Si aún no lo ha notado, definir los componentes de la entidad como interfaces es otra característica bastante única de Svelto.ECS. Por lo general, los componentes en otros marcos son objetos. El uso de interfaces en su lugar puede reducir significativamente el código. Si sigues el principio" Principio de segregación de interfaz", habiendo escrito pequeñas interfaces de componentes, incluso con una propiedad cada una, notará que ha comenzado a reutilizar interfaces de componentes dentro de diferentes entidades. En nuestro ejemplo, ITransformComponent se reutiliza en muchas representaciones de entidades. El uso de componentes como interfaces también les permite implementar los mismos objetos, lo que en muchos casos simplifica la relación entre entidades que ven la misma entidad usando diferentes representaciones de las entidades (o lo mismo, si es posible).Por lo tanto, en Svelto.ECS, el componente de entidad es siempre una interfaz, y esta interfaz se usa solo a través del campo EntityView dentro del motor. La interfaz del componente de entidad es implementada por el llamado«». , .Los componentes siempre deben almacenar tipos significativos, y los campos son siempre propiedades. Solo se pueden hacer excepciones para escribir setters y getters como métodos para usar la palabra clave ref cuando se necesita optimización. Esto no significa que el código esté orientado a datos, pero le permitirá crear código para pruebas, ya que la lógica del motor no debe procesar enlaces a dependencias externas. Además, esto evita que los codificadores hagan trampa en el marco y usen funciones públicas (¡que pueden incluir lógica!) De objetos aleatorios. La única razón por la que podía sentir la necesidad de usar enlaces dentro de las interfaces de los componentes de la entidad era para tratar con dependencias de terceros, como los objetos de Unity. Sin embargo, el ejemplo de supervivencia muestra cómo manejar esto,dejando el código de prueba del motor sin tener que preocuparse por las dependencias de Unity.Aquí es donde Entity Descriptors viene al rescate para armar todo. Sabemos que los motores pueden acceder a los datos de la entidad a través de componentes que se almacenan en las vistas de la entidad. Sabemos que los motores son clases, EntityView son clases que contienen solo entidades componentes y que los componentes son interfaces. Aunque di una definición abstracta de la Esencia, no hemos visto una sola clase que realmente represente la Esencia. Esto corresponde al concepto de objetos que son identificadores dentro del sistema ECS moderno. Sin embargo, sin la definición correcta de Entidades, esto obligará a los codificadores a identificar Entidades con Representaciones de entidades, lo que sería catastróficamente incorrecto. Las representaciones de entidades es la forma en que varios motores pueden ver la misma entidad,pero no son entidades. La Entidad misma siempre debe considerarse como un conjunto de datos definidos a través de los Componentes de la entidad, pero incluso esta es una definición débil. Una instancia de EntityDescriptor permite que el codificador determine correctamente sus Entidades, independientemente de los motores que las procesen. Por lo tanto, en el caso de Entity Player, necesitamosPlayerEntityDescriptor . Esta clase se usará para crear Entidades, y aunque lo que realmente hace es algo completamente diferente, el hecho de que el usuario pueda escribir BuildEntity <PlayerEntityDescriptor> () ayuda a visualizar Entidades muy fácilmente para construir y comunicar intenciones a otros. codificadoresSin embargo, lo que EntityDescriptor realmente hace es crear una lista de EntityViews. En las primeras etapas de desarrollo del marco, permití a los codificadores crear esta lista de EntityViews manualmente, lo que condujo a un código muy feo porque ya no podía visualizar lo que realmente estaba sucediendo. Asíes como se ve PlayerEntityDescriptor : using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } }
Los descriptores de entidad (y los implementadores) son las únicas clases que pueden usar identificadores de múltiples espacios de nombres. En este caso, PlayerEntityDescriptor define una lista de EntityViews para instanciar e inyectar en el motor al crear PlayerEntity.EntityDescriptorHolder
EntityDescriptorHolder es una extensión para Unity y solo debe usarse en ciertos casos. Lo más común es la creación de un tipo de polimorfismo que almacena información sobre entidades para construir un GameObject de Unity. Por lo tanto, se puede usar el mismo código para crear varios tipos de entidades. Por ejemplo, en Robocraft, utilizamos una única fábrica de cubos que construye todos los cubos que componen las máquinas. El tipo de cubo para ensamblar se almacena en la prefabricada del propio cubo. Esto es bueno siempre que los implementadores sean iguales entre los cubos o se encuentren en GameObject como MonoBehaviour's. Es preferible crear Entidades directamente, así que use EntityDescriptorHolders solo cuando comprenda correctamente los principios de Svelto.ECS, de lo contrario, existe el riesgo de abuso. Esta función del ejemplo muestra cómo usar la clase: void BuildEntitiesFromScene(UnityContext contextHolder) {
Tenga en cuenta que en este ejemplo estoy usando una función BuildEntity no genérica menos preferida . Explicaré esto. En este caso, los implementadores son las clases MonoBehaviour adjuntas al GameObject. Esta no es una buena práctica. Debería haber eliminado este código del ejemplo, pero dejado para mostrarle este caso especial. Los implementadores, como veremos más adelante, ¡deberían ser clases MonoBehaviours solo cuando sea necesario!Imitadores
Antes de crear nuestra esencia, definamos el último concepto en Svelto.ECS, que es el Empalador . Como sabemos, los componentes de la entidad son siempre interfaces, y las interfaces C # deben implementarse. Un objeto que implementa estas interfaces se denomina "implementador". Los implementadores tienen varias características importantes:- La capacidad de desatar el número de objetos a ensamblar del número de componentes de entidad necesarios para determinar los datos de la entidad.
- La capacidad de intercambiar datos entre diferentes Componentes, dado que los Componentes proporcionan datos a través de propiedades, las diferentes propiedades de un Componente pueden devolver el mismo campo de implementación.
- Capacidad para crear un stub de componente de interfaz para un componente de entidad. Esto es importante para dejar el código del motor probado.
- Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :
public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; }
, , . , .Creación de entidad
Supongamos que creamos nuestros motores , los agregamos a EnginesRoot , creamos sus vistas de entidad , que necesitan componentes como interfaces que se implementarán dentro de los implementadores . Es hora de crear nuestra primera Esencia. Una entidad siempre se crea a través de una instancia de Entity Factory creada por EnginesRoot a través de la función GenerateEntityFactory . A diferencia de una instancia de EnginesRoot, una instancia de IEntityFactory se puede implementar y transferir. Los objetos se pueden construir dentro de la raíz de la composición o dinámicamente dentro de las fábricas, por lo que para este último caso, debe pasar un IEntityFactory a través de un parámetro.IEntityFactory .
PreallocateEntitySlots BuildMetaEntity ,
BuildEntity BuildEntityInGroup .
BuildEntityInGroup , Survival , ,
BuildEntity :
IEnumerator IntervaledTick() {
Recuerde leer todos los comentarios en este ejemplo, lo ayudarán a comprender mejor los conceptos de Svelto.ECS. Debido a la simplicidad del ejemplo, no utilizo BuildEntityInGroup , que se usa en proyectos más complejos. En Robocraft, cada motor que procesa la lógica de los cubos funcionales procesa la lógica de TODOS los cubos funcionales de este tipo en particular en el juego. Sin embargo, a menudo es necesario saber a qué vehículo pertenecen los cubos, por lo que usar un grupo para cada máquina ayudará a dividir los cubos del mismo tipo en máquinas, donde la ID de la máquina es la ID del grupo. Esto nos permite implementar cosas interesantes, como ejecutar una tarea Svelto.Tasks en una máquina dentro del mismo motor, que puede funcionar en paralelo usando subprocesos múltiples.Este código muestra un problema importante que puedo cubrir con más detalle en los siguientes artículos ... del comentario (si no lo ha leído):nunca cree Impulsores MonoBehaviour solo para el almacenamiento de datos. Los datos siempre deben recuperarse a través de la capa de servicio, independientemente de la fuente de datos. Los beneficios son numerosos, incluido el hecho de que para cambiar la fuente de datos solo necesita cambiar el código de servicio. En este simple ejemplo, no uso la capa de Servicio, pero en general la idea es clara. También tenga en cuenta que solo subo datos una vez para cada inicio de aplicación, fuera del bucle principal. Siempre puedes usar este truco si los datos que necesitas nunca cambian.Inicialmente, leía datos directamente de MonoBehaviour, como haría un buen codificador perezoso. Esto me hizo crear un implementador de serializador de solo lectura MonoBehaviore. Esto es aceptable si no queremos abstraer la fuente de datos, pero es mucho mejor serializar la información en un archivo json y leerla a pedido del servicio que leer esta información del Componente de la entidad.Comunicación en Svelto.ECS
Un problema cuya solución nunca ha sido estandarizada por ninguna implementación de ECS es la comunicación entre sistemas. Este es otro lugar donde pensé mucho, y Svelto.ECS lo resuelve de dos maneras nuevas. La tercera forma es utilizar el patrón Observador / Observado estándar, aceptable en casos muy específicos y específicos.DispatchOnSet / DispatchOnChange
Anteriormente vimos cómo permitir que los motores intercambien datos a través de componentes de la entidad mediante el sondeo de datos. DispatchOnSet y DispatchOnChange son las únicas referencias (tipos no significativos) que pueden ser devueltas por las propiedades de los componentes de la entidad, pero el tipo del parámetro genérico T debe ser un tipo significativo. Los nombres de las funciones suenan como un despachador de eventos, pero en su lugar deben considerarse como métodos para enviar los datos, en lugar de encuestas de datos, que es un poco como el enlace de datos. Eso es todo, a veces los datos de sondeo son inconvenientes, no queremos sondear una variable en cada cuadro cuando sabemos que los datos rara vez cambian. DispatchOnSet y DispatchOnChangeno se puede iniciar sin cambiar los datos, esto nos permite considerarlos como un mecanismo de enlace de datos en lugar de un evento regular. Tampoco hay una función de inicio para llamar; en cambio, el valor de los datos en poder de estas clases debe establecerse o cambiarse. No hay grandes ejemplos en el código de supervivencia, pero puede ver cómo funciona el campo booleano targetHit de IGunHitTargetComponent . La diferencia entre DispatchOnSet y DispatchOnChange es que este último activa el evento solo cuando los datos realmente cambian, y el primero siempre.Secuenciador
Los motores ideales están completamente encapsulados, y puede escribir la lógica de este motor como una secuencia de instrucciones utilizando Svelto.Tasks e IEnumerators. Sin embargo, esto no siempre es posible, ya que en algunos casos los motores deben enviar eventos a otros motores. Esto generalmente se hace a través de los datos de la entidad, especialmente usando DispatchOnSet y DispatchOnChangesin embargo, como en el caso de las Entidades "dañadas" en el ejemplo, una serie de Motores independientes y no relacionados actúan sobre él. En otros casos, desea que la secuencia sea estricta en el orden en que se llamaron los motores, como en el ejemplo en el que quiero que siempre ocurra la muerte para este último. En este caso, la secuencia no solo es muy fácil de usar, ¡sino que también es muy conveniente! La refactorización de secuencias es muy simple. Por lo tanto, utilice Tareas Ivelumerator Svelto para motores "verticales" y secuencias para lógica "horizontal" entre motores.Observador / Observado
Dejé la oportunidad de usar este patrón específicamente para casos en los que el código heredado o el código que no usa Svelto.ECS debería interactuar con los motores Svelto.ECS. Para otros casos, debe usarse con extrema precaución, ya que existe la posibilidad de abuso del patrón, ya que es familiar para la mayoría de los codificadores nuevos en Svelto.ECS, y los secuenciadores son generalmente la mejor opción.