0 Raiz de composicion (composition root)
lobotomista redigerade denna sida 5 år sedan

La clase Main es la raíz de composición de la aplicación . Una raíz de composición es donde se crean e inyectan las dependencias. Una raíz de composición pertenece al contexto, pero un contexto puede tener más de una raíz de composición. Por ejemplo, una factory es una raíz de composición. Además, una aplicación puede tener más de un contexto, pero este es un escenario avanzado y no forma parte de este ejemplo.

Antes de comenzar a indagar en el código, introduzcamos los primeros términos del lenguaje de dominio Svelto.ECS. ECS es un acrónimo de Entity Component System. La infraestructura de ECS se ha analizado abundantemente con varios artículos escritos por muchos autores, pero si bien, los conceptos básicos son comunes, las implementaciones difieren mucho. Por encima de todo, no hay una manera estándar de resolver los varios problemas derivados del uso de código orientado a ECS. Ahí es donde puse la mayor parte de mi esfuerzo, pero esto es algo de lo que hablaré más adelante o en los próximos artículos. En el corazón de la teoría están los conceptos de entidad , componentes (de las entidades) y sistemas . Si bien entiendo por qué la palabra Sistema se ha usado históricamente, inicialmente la encontré no intuitiva para el propósito, por lo que engine(Motor) es sinónimo de Sistema y puede usarla indistintamente de acuerdo con sus preferencias.

La clase EnginesRoot es el núcleo de Svelto.ECS. Con élla es posible registrar los motores y construir todas las entidades del juego. No tiene mucho sentido crear motores dinámicamente, por lo que todos los engines(motores) deben agregarse en la instancia EnginesRoot de la misma composition root donde se creó. Por razones similares, una instancia de EnginesRoot nunca debe inyectarse y los motores no se pueden eliminar una vez agregados.

Necesitamos al menos una raíz de composición para poder crear e inyectar dependencias donde sea necesario. Sí, es posible tener incluso más de un EnginesRoot por aplicación, pero esto tampoco es parte de este artículo, que trataré de mantener lo más simple posible. Así es como se ve una raíz de composición con inyección de dependencias y creación de motores:

 SetupEnginesAndEntities ()
             {
                 // La engine root es el núcleo de Svelto.ECS.  NUNCA debes inyectar el EngineRoot
                 // tal como está, por lo tanto, la raíz de la composición debe contener una referencia o será
                 // removida por el recolector de basura.
                 // UnitySumbmissionEntityViewScheduler es el planificador que utiliza EnginesRoot para saber
                 // cuando inyectar las EntityViews.  Nunca deberías usar uno personalizado a menos que sepas lo que
                 // estás haciendo o no estás trabajando con Unity.
                 _enginesRoot = new EnginesRoot ( new UnitySumbmissionEntityViewScheduler ());
                 // La enginesroot nunca puede ser mantenida por otra cosa mas que el context en si, para evitar fugas.
                 // Es por eso que se generan EntityFactory y EntityFunctions.
                 // El EntityFactory se puede inyectar dentro de las factorys (o engines actúando como factory)
                 // para construir nuevas entidades dinámicamente
                 _entityFactory = _enginesRoot.GenerateEntityFactory ();
                 // Las funciones de entidad son un conjunto de operaciones de utilidad en Entidades, que incluyen
                 // eliminar una entidad.  No pude encontrar un nombre mejor hasta ahora.
                 var entityFunctions = _enginesRoot.GenerateEntityFunctions ();
                 // GameObjectFactory permite crear GameObjects sin usar el Static
                 // method GameObject.Instantiate.  Si bien parece una complicación.
                 // es importante mantener los engines verificables y no
                 // junto con las referencias de dependencias difíciles. 
                 GameObjectFactory factory = new GameObjectFactory ();
                 // El patrón de observador es una de las 3 formas oficiales disponibles.
                 // en Svelto.ECS para comunicarse.  Específicamente, los observadores deberían
                 // ser usados para comunicarse entre motores en casos muy específicos
                 // ya que no es la solución preferida, Y para comunicarse entre
                 // motores y código heredado / código de terceros.
                 // Utilízalos con cuidado y escasamente.
                 // Los observadores y observables deben ser nombrados de acuerdo a donde son
                 // utilizados.  El observador y el observable estan desemparejados para permitir que cada objeto
                 // pueda ser utilizado en un contexto relativo que promueve la separación de responsabilidades.
                 // La forma preferida de comunicarse entre los motores es a través de
                 // los componentes de la entidad en sí mismos.  DispatchOnSet y DispatchOnChange
                 // deberían ser capaces de cubrir la mayoría de los problemas de comunicación
                 // entre motores.
                 var enemyKilledObservable = new EnemyKilledObservable ();
                 var scoreOnEnemyKilledObserver = new ScoreOnEnemyKilledObserver ( enemyKilledObservable );
                 // el ISequencer es una de las 3 formas oficiales disponibles en Svelto.ECS
                 // para comunicarse.  Se utilizan principalmente para dos casos específicos:
                 // 1) especifique un orden de ejecución estricto entre motores (la lógica del motor
                 // se ejecuta horizontalmente en lugar de verticalmente)
                 // 2) filtrar un data token pasado como parámetro a través
                 // motores.  El ISequencer tampoco es la forma común de comunicarse.
                 // entre motores
                 Sequencer playerDamageSequence = new Sequencer ();
                 Sequencer enemyDamageSequence = new Sequencer ();
                 // envolver clases estáticas de unidad no testeables, para que
                 // se les puedan hacer mock tests si es necesario.
                 IRayCaster rayCaster = new RayCaster ();
                 ITime time = new Others.Time();
                 // Motores relacionados con jugadores.  TODAS las dependencias deben quedar resueltas para este punto.
                 // a través de la inyección de constructores.
                 var playerHealthEngine = new HealthEngine(entityFunctions, playerDamageSequence);
                var playerShootingEngine = new PlayerGunShootingEngine(enemyKilledObservable, enemyDamageSequence, rayCaster, time);
                var playerMovementEngine = new PlayerMovementEngine(rayCaster, time);
                var playerAnimationEngine = new PlayerAnimationEngine();

                 // Motores relacionados con el enemigo.
                 var enemyAnimationEngine = new EnemyAnimationEngine();
                var enemyHealthEngine = new HealthEngine(entityFunctions, enemyDamageSequence);
                var enemyAttackEngine = new EnemyAttackEngine(playerDamageSequence, time);
                var enemyMovementEngine = new EnemyMovementEngine();
                var enemySpawnerEngine = new EnemySpawnerEngine(factory, _entityFactory);
                 // hud y motores de sonido
                 var hudEngine = new HUDEngine ( time );
                 var damageSoundEngine = new DamageSoundEngine ();
                 // La implementación de ISequencer es muy simple, pero permite realizar
                 // concatenación compleja incluyendo bucles y bifurcaciones condicionales.
                 playerDamageSequence.SetSequence(
                    new Steps // secuencia de pasos, este es un diccionario!
                     { 
                         { // primer paso
                             enemyAttackEngine , // este paso solo puede ser activado por este motor a través de la función Next
                             new To // este paso puede llevar solo a una rama
                             { 
                                 playerHealthEngine , // este es el único motor que se llamará cuando enemyAttackEngine active a Next()
                             }  
                         }
                         { // segundo paso
                             playerHealthEngine , // este paso solo puede ser activado por este motor a través de la función Siguiente
                             new To // este paso puede bifurcarse en dos caminos
                             { 
                                 { DamageCondition.damage, new IStep[] { hudEngine , damageSoundEngine }}, // estos motores se activarán cuando se llame a la función Next con el conjunto DamageCondition.damage
                                 { DamageCondition.dead, new IStep[] { hudEngine , damageSoundEngine , 
                                     playerMovementEngine , playerAnimationEngine , enemyAnimationEngine }}, // se llamará a estos motores cuando se llame a la función Next con el conjunto DamageCondition.dead
                             }  
                         }  
                     }
                 );
enemyDamageSequence.SetSequence(
                    new Steps
                    { 
                        { 
                            playerShootingEngine, 
                            new To
                            { 
                                enemyHealthEngine,
                            }  
                        },
                        { 
                            enemyHealthEngine, 
                            new To
                            { 
                                {  DamageCondition.damage, new IStep[] { enemyAnimationEngine, damageSoundEngine }  },
                                {  DamageCondition.dead, new IStep[] { enemyMovementEngine, 
                                    enemyAnimationEngine, playerShootingEngine, enemySpawnerEngine, damageSoundEngine } },
                            }  
                         }  
                     }
                 );
                 // Paso obligatorio para hacer funcionar los motores.
                 // motores de jugador
                 _enginesRoot.AddEngine(playerMovementEngine);
                _enginesRoot.AddEngine(playerAnimationEngine);
                _enginesRoot.AddEngine(playerShootingEngine);
                _enginesRoot.AddEngine(playerHealthEngine);
                _enginesRoot.AddEngine(new PlayerInputEngine());
                _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine());
                 // motores enemigos
                 _enginesRoot.AddEngine(enemySpawnerEngine);
                _enginesRoot.AddEngine(enemyAttackEngine);
                _enginesRoot.AddEngine(enemyMovementEngine);
                _enginesRoot.AddEngine(enemyAnimationEngine);
                _enginesRoot.AddEngine(enemyHealthEngine);
                 // otros motores
                 _enginesRoot.AddEngine(new CameraFollowTargetEngine(time));
                _enginesRoot.AddEngine(damageSoundEngine);
                _enginesRoot.AddEngine(hudEngine);
                _enginesRoot.AddEngine(new ScoreEngine(scoreOnEnemyKilledObserver));
             } 

Este código es parte del ejemplo de supervivencia que ahora está bien comentado (en ingles) y sigue casi todas las reglas de buenas prácticas que sugiero usar, incluyendo mantener la plataforma lógica de los motores independiente y verificable. Los comentarios le ayudarán a comprender la mayor parte del mismo, pero un proyecto de este tamaño puede ser duro de tragar si es nuevo en Svelto.