-
-
Notifications
You must be signed in to change notification settings - Fork 95
Conceptos Basicos
(la version origina usa mucho la palabra query que traduje como consulta, espero no haberme equivocado) El primer paso después de crear la Raíz de composición vacía y una instancia de la clase EnginesRoot, sería identificar primero las entidades con las que desea trabajar. Vamos a empezar lógicamente desde la Entidad del Jugador . La entidad en Svelto.ECS no debe confundirse con los GameObject de Unity. Si tuviste la oportunidad de leer otros artículos relacionados con ECS, verá que en muchos de ellos, las entidades a menudo se describen como índices. Esta es probablemente la peor manera posible de introducir el concepto. Si bien esto también es cierto para Svelto.ECS, está bastante oculto. De hecho, quiero que el usuario de Svelto.ECS visualice, describa e identifique a cada entidad en términos del lenguaje de dominio de diseño de juegos . Una entidad en el código debe ser una entidad descrita en el documento de diseño del juego. Cualquier otra forma de definición de entidad resultará en una forma idónea para adaptar los viejos paradigmas a las necesidades de Svelto.ECS. Sige esta regla fundamental y no te equivocaras en la mayoría de los casos. Una entity class no existe per se en código, pero aún así debe definirla de una manera no abstracta.
El siguiente paso es pensar qué comportamientos dar a esta Entidad. Cada comportamiento siempre se modela dentro de un engine , no hay manera de agregar lógica en ninguna otra clase dentro de una aplicación Svelto.ECS. Para este propósito, podemos comenzar desde el movimiento del personaje del jugador y definir la clase PlayerMovementEngine . El nombre del engine debe ser muy específico, ya que cuanto más específico sea, mayor será la posibilidad de que el engine siga el principio de responsabilidad única. Nombrar clases correctamente en Svelto.ECS es de fundamental importancia. No se trata solo de comunicar claramente sus intenciones, sino que en realidad se trata más de hacerte pensar en su proposito.
Por la misma razón es igual de importante colocar tu engine dentro de un namespace muy específico. Si usa un namespace de acuerdo con la estructura de la carpeta, adápalo a la convención Svelto.ECS. El uso de namespaces muy especializados ayuda mucho a identificar errores de diseño de código cuando las entidades se usan dentro de namespaces no compatibles. Por ejemplo, no esperaría que se usara ninguna entidad enemiga dentro del namespace de un jugador, a menos que quiera romper las buenas reglas relacionadas con la modularidad y el desemparejamiento de objetos. La idea es que los objetos de un namespace específico se pueden usar solo dentro de ese namespace o un namespace madre. Mientras que con Svelto.ECS es mucho más difícil convertir su código en un plato espagueti, donde las dependencias se inyectan en todas partes y al azar, esta regla lo ayudará a llevar su código a un nivel aún mejor donde las dependencias se abstraen correctamente entre clases.
En Svelto.ECS, la abstracción se enfatiza en varios frentes, pero ECS promueve intrínsecamente la abstracción de los datos y de la lógica que maneja dichos datos. Las entidades se definen por sus datos, no por sus comportamientos. Los engines, en cambio, son el lugar donde colocar los comportamientos compartidos de las mismas entidades, de modo que los engines siempre puedan operar en un conjunto de entidades.
Svelto.ECS y el paradigma ECS en general permiten a los programadores lograr uno de los santos griales de la programación limpia, que es la encapsulación perfecta de la lógica . Los engines no deben tener funciones públicas. Las únicas funciones públicas que deben existir son las necesarias para implementar interfaces de framework. Esto naturalmente lleva a olvidarse de la inyección de dependencia y evita el código molesto que se deriva del uso de la inyección de dependencias sin inversión de control. Los engines NUNCA deben inyectarse en ningún otro engine o cualquier otro tipo de clase. Si piensas inyectar un engine, solo cometerías un error de diseño de código fundamental.
En comparación con los monobehaviours de Unity, los engines ya muestran el primer gran beneficio, que es la posibilidad de acceder a todos los estados de entidad de un tipo dado desde el mismo ámbito de código. Esto significa que el código puede usar fácilmente el estado de todas las entidades directamente desde el mismo lugar donde se ejecutará la lógica de la entidad compartida. Además, los engines separados pueden manejar las mismas entidades, de modo que un engine puede cambiar el estado de una entidad, mientras que otro engine puede leerlo, poniendo en comunicación a los dos engines a través de los mismos datos de la entidad. Se puede ver un ejemplo de esto con los engines PlayerGunShootingEngine y PlayerGunShootingFxsEngine . En este caso, los dos engines están en el mismo namespace, por lo que pueden compartir los mismos datos de entidad. PlayerGunShootingEngine determina si un objetivo de jugador (un enemigo) ha sido dañado y escribe la última posición de destino de IGunAttributesComponent (que es un componente de PlayerGunEntity ) . El PlayerGunShootFxsEngine maneja los efectos gráficos de la pistola y lee la posición del objetivo del jugador actualmente seleccionado . Este es un ejemplo de comunicación entre engines a través de encuestas de datos . Más adelante en este artículo, mostraré cómo permitir que los engines se comuniquen entre ellos a través de la inserción de datos (o enlace de datos) . Es bastante lógico que los engines pueden (y deben) nunca mantener estados.
Los engines no deben saber cómo interactuar con otros engines. La comunicación externa ocurre a través de la abstracción y Svelto.ECS resuelve la comunicación entre engines de tres maneras oficiales diferentes, pero hablaré de esto más adelante. Los mejores engines son los que ni siquiera necesitan activar ninguna forma de comunicación externa. Estos engines reflejan un comportamiento bien encapsulado y normalmente funcionan a través de un bucle lógico. Los bucles siempre se modelan con una tarea Svelto.Task dentro de las aplicaciones Svelto.ECS. Dado que el movimiento del jugador debe actualizarse en cada tic del physic, sería natural crear una tarea que se ejecute en cada physic update. Svelto.Tasks permite ejecutar todo tipo de IEnumerators en varios tipos de schedulers. En este caso, decidimos crear una tarea(task) en el PhysicScheduler que permita 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()
{ // Algunas suposiciones seguras aquí: asumo que la entidad de jugador está creada
// y se agrega en EnginesRoot cuando se ejecuta este código.
// Supongo que solo hay una entidad de jugador en el conjunto de entidades.
var _playerEntityViews = entityViewsDB.QueryEntityViews<PlayerEntityView>();
var playerEntityView = _playerEntityViews[0];
while (true)
{
Movement(playerEntityView);
Turning(playerEntityView);
yield return null; // ¡No olvides el yield o terminarás en un bucle infinito!
}
}
Las tareas de Svelto.Tasks se pueden ejecutar directamente a través de objetos ITaskRoutine. No hablaré mucho acerca de Svelto.Tasks ya habra otros artículos para ello. La razón por la que decidí usar una task routine en lugar de ejecutar la implementación de IEnumerator directamente es bastante discrecional. Quería demostrar que es posible iniciar el bucle cuando se agrega la entidad del jugador en el motor y detenerlo cuando se elimina. Sin embargo, para hacerlo, necesito saber cuándo se agrega y se elimina la entidad.
Svelto.ECS introduce callbacks de add y remove para saber cuándo se agregan o eliminan entidades específicas. Esto es algo único en Svelto.ECS, pero debe usarse con prudencia. A menudo he visto que estas devoluciones de llamada son objeto de abuso, ya que en muchos casos es suficiente para consultar entidades. Incluso mantener una referencia a una entidad como engine field debe ser visto como una excepción más que una regla.
Solo cuando estas callbacks necesitan ser explotadas, el motor debe heredar de SingleEntityViewEngine o MultiEntitiesViewEngine <EntityView1, ..., EntityViewN> . Nuevamente, el uso de estos debe ser escaso y de ninguna manera pretenden comunicar qué entidades manejará el engine.
Los engines implementan más comúnmente la interfaz IQueryingEntityViewEngine en su lugar. Esto permite acceder a la base de datos de entidades y recuperar datos de ella. Recuerde que siempre puede consultar cualquier entidad desde el interior de un engine, pero en el momento en que consulta una entidad que no es compatible con el namespace donde se encuentra el engine, sabe que ya está haciendo algo mal. Los engines nunca deben asumir que las entidades están disponibles y deben trabajar en un conjunto de entidades. El hecho de que el juego siempre tendrá un solo jugador no debe asumirse como lo estoy haciendo en el ejemplo de código. Un enfoque muy común sobre cómo consultar entidades se encuentra en el EnemyMovementEngine :
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 loop principal del engine se ejecuta directamente en el scheduler predefinido. Tick().Run() muestra la forma más corta de ejecutar un IEnumerator con Svelto.Tasks. El IEnumerador seguirá haciendo yield al siguiente frame hasta que se encuentre al menos un objetivo enemigo (enemy target). Como sabemos que siempre habrá un solo objetivo (otro supuesto que no es agradable), recojera el primero disponible. Mientras que el objetivo enemigo puede ser solo uno (¡aunque podría haber sido más!), Los enemigos son muchos y el motor se encarga de la lógica de movimiento de todos ellos. En este caso estoy haciendo trampa, ya que en realidad estoy usando el sistema de malla de Unity Nav, así que todo lo que tengo que hacer es configurar el destino NavMesh. Para ser honesto, nunca usé el código Unity NavMesh, así que ni siquiera estoy seguro de cómo funciona exactamente, este código se ha heredado de la demostración original de Survival.
Ten en cuenta que el componente nunca expone la dependencia navmesh de Unity directamente . El Entity component, como diré más adelante, siempre debe exponer tipos de valor. En este caso, esta regla también permite mantener el código testeable, ya que el field value type(?) navMeshDestination se podría implementar más adelante sin usar un código auxiliar de malla de Unity Nav.
Para concluir el párrafo relacionado con los engines, ten en cuenta que no existe un engine demasiado pequeño. Por lo tanto, no tengas miedo de escribir un engine, incluso por unas pocas líneas de código, después de todo, no puedes poner la lógica en ningún otro lugar y quieres que tus engines sigan la Regla de responsabilidad única.