/// <summary> /// Before to start, let's review some of the Svelto.ECS terms: /// - Entity: /// it must be a real and concrete entity that you can explain in terms of game design. The name of each /// entity should reflect a specific concept from the game design domain /// - Engines (Systems): /// Where all the logic lies. Engines operates on Entity Components /// - IEntityComponent: /// It's an Entity Component which can be used with Pure ECS /// - IEntityViewComponent: /// structs implementing this are used to wrap Objects that come from OOP libraries. You will never use it unless /// you are forced to mix your ECS code with OOP code because of external libraries or platforms. These special /// "Hybrid" component can hold only interfaces /// - Implementors: /// The EntityViewComponent exposed interfaces must be implemented by Implementors that are actually the /// Objects you need to wrap. /// - EntityDescriptors: /// Gives a way to formalise your Entity, it also defines the components that must /// be generated once the Entity is built /// </summary> void CompositionRoot(UnityContext contextHolder) { //the UnitySumbmissionEntityViewScheduler is the scheduler that is used by the EnginesRoot to know //when to submit the entities. Custom ones can be created for special cases. var unityEntitySubmissionScheduler = new UnityEntitiesSubmissionScheduler("survival"); //The Engines Root is the core of Svelto.ECS. You shouldn't inject the EngineRoot, //therefore the composition root class must hold a reference or it will be garbage collected. _enginesRoot = new EnginesRoot(unityEntitySubmissionScheduler); //The EntityFactory can be injected inside factories (or engine acting as factories) to build new entities //dynamically var entityFactory = _enginesRoot.GenerateEntityFactory(); //The entity functions is a set of utility operations on Entities, including removing an entity. I couldn't //find a better name so far. var entityFunctions = _enginesRoot.GenerateEntityFunctions(); var entityStreamConsumerFactory = _enginesRoot.GenerateConsumerFactory(); //wrap non testable unity static classes, so that can be mocked if needed (or implementation can change in general, without changing the interface). IRayCaster rayCaster = new RayCaster(); ITime time = new Time(); //GameObjectFactory allows to create GameObjects without using the Static method GameObject.Instantiate. //While it seems a complication it's important to keep the engines testable and not coupled with hard //dependencies var gameObjectFactory = new GameObjectFactory(); //Player related engines. ALL the dependencies must be solved at this point through constructor injection. var playerShootingEngine = new PlayerGunShootingEngine(rayCaster, time); var playerMovementEngine = new PlayerMovementEngine(rayCaster); var playerAnimationEngine = new PlayerAnimationEngine(); var playerDeathEngine = new PlayerDeathEngine(entityFunctions, entityStreamConsumerFactory); var playerInputEngine = new PlayerInputEngine(); var playerGunShootingFXsEngine = new PlayerGunShootingFXsEngine(entityStreamConsumerFactory); //Spawner engines are factories engines that can build entities var playerSpawnerEngine = new PlayerSpawnerEngine(gameObjectFactory, entityFactory); var restartGameOnPlayerDeath = new RestartGameOnPlayerDeathEngine(); //Player engines _enginesRoot.AddEngine(playerMovementEngine); _enginesRoot.AddEngine(playerAnimationEngine); _enginesRoot.AddEngine(playerShootingEngine); _enginesRoot.AddEngine(playerInputEngine); _enginesRoot.AddEngine(playerGunShootingFXsEngine); _enginesRoot.AddEngine(playerDeathEngine); _enginesRoot.AddEngine(playerSpawnerEngine); _enginesRoot.AddEngine(restartGameOnPlayerDeath); //Factory is one of the few OOP patterns that work very well with ECS. Its use is highly encouraged var enemyFactory = new EnemyFactory(gameObjectFactory, entityFactory); //Enemy related engines var enemyAnimationEngine = new EnemyChangeAnimationOnPlayerDeath(); var enemyDamageFX = new EnemySpawnEffectOnDamage(entityStreamConsumerFactory); var enemyAttackEngine = new EnemyAttackEngine(time); var enemyMovementEngine = new EnemyMovementEngine(); //Spawner engines are factories engines that can build entities var enemySpawnerEngine = new EnemySpawnerEngine(enemyFactory, entityFunctions); var enemyDeathEngine = new EnemyDeathEngine(entityFunctions, entityStreamConsumerFactory, time , new WaitForSubmissionEnumerator( unityEntitySubmissionScheduler)); //enemy engines _enginesRoot.AddEngine(enemySpawnerEngine); _enginesRoot.AddEngine(enemyAttackEngine); _enginesRoot.AddEngine(enemyMovementEngine); _enginesRoot.AddEngine(enemyAnimationEngine); _enginesRoot.AddEngine(enemyDeathEngine); _enginesRoot.AddEngine(enemyDamageFX); //abstract engines var applyDamageEngine = new ApplyDamageToDamageableEntitiesEngine(entityStreamConsumerFactory); var cameraFollowTargetEngine = new CameraFollowingTargetEngine(time); var deathEngine = new DispatchKilledEntitiesEngine(); //abstract engines (don't need to know the entity type) _enginesRoot.AddEngine(applyDamageEngine); _enginesRoot.AddEngine(deathEngine); _enginesRoot.AddEngine(cameraFollowTargetEngine); //hud and sound engines var hudEngine = new HUDEngine(entityStreamConsumerFactory); var damageSoundEngine = new DamageSoundEngine(entityStreamConsumerFactory); var scoreEngine = new UpdateScoreEngine(entityStreamConsumerFactory); //other engines _enginesRoot.AddEngine(damageSoundEngine); _enginesRoot.AddEngine(hudEngine); _enginesRoot.AddEngine(scoreEngine); var unsortedEngines = new SurvivalUnsortedEnginesGroup(new FasterList <IStepEngine>( new IStepEngine[] { playerMovementEngine, playerInputEngine, playerGunShootingFXsEngine, playerSpawnerEngine, playerAnimationEngine, enemySpawnerEngine, enemyMovementEngine, cameraFollowTargetEngine, hudEngine, restartGameOnPlayerDeath } )); var unsortedDamageEngines = new DamageUnsortedEngines(new FasterList <IStepEngine>( new IStepEngine[] { applyDamageEngine, damageSoundEngine, deathEngine } )); //Svelto ECS doesn't provide a tick system, hence it doesn't provide a solution to solve the order of execution //However it provides some option if you want to use them like the SortedEnginesGroup. _enginesRoot.AddEngine(new TickEnginesGroup(new FasterList <IStepEngine>(new IStepEngine[] { unsortedEngines , playerShootingEngine , enemyDamageFX , enemyAttackEngine , unsortedDamageEngines , playerDeathEngine , enemyDeathEngine , scoreEngine }))); BuildGUIEntitiesFromScene(contextHolder, entityFactory); }
/// <summary> /// Before to start a review of Svelto.ECS terminologies: /// - Entity: /// it must be a real and concrete entity that you can explain /// in terms of game design. The name of each entity should reflect /// a specific concept from the game design domain /// - IComponents: /// Components must be seen as data holders. There are implementing /// exceptions, but logically it must be still see as a group /// of readable or writeable data. /// In Svelto.ECS components are always interfaces declaring /// Setters and Getters of ValueTypes. DispatchOnSet /// and DispatchOnChange must not be seen as events, but /// as pushing of data instead of data polling, similar /// to the concept of DataBinding. /// - Implementors: /// Being components interfaces, they must be implemented through /// Implementors. The relation Implementors to Components /// is not 1:1 so that you can, if logic, group several /// components in one implementor. This allows to easily /// share data between components. Implementors also act /// as bridge between the platform and Svelto.ECS. /// Since Components can hold only value types, Implementors /// are the objects that can interact directly with the platform /// objects, I.E.: RigidBody, Transform and so on. /// Note: IComponents must hold only valuetypes for /// code design purposes and not optmization purposes. /// The reason is that all the logic must lie in the engines /// so Components cannot hold references to instances that can /// expose functions with logic. /// - Engines: /// Where all the logic lies. Engines operates on EntityViews /// - EntityViews: /// EntityViews maps EntityComponents. The Engines can't /// access directly to each entity (as a single set of components), but /// through a component sets defined by EntityView. /// They act as a component filters and expose only the entity components /// that the Engine is interested in. /// EntityViews are actually defined by the need of the Engine so they /// come together with the engine and in the same namespace of the engine. /// - EntityStructs: /// In order to write Data Oriented Cache Friendly code, Svelto.ECS /// also support EntityStructs. Please check other examples to /// understand how to use them. However know that this kind of /// optimizations is very limited to special circumstances /// so the flexibility of EntityViews is most of the times what you need. /// - EntityDescriptors: /// Gives a way to formalize your Entity in svelto.ECS, it also /// groups the EntityViews that must be generated once the /// Entity is built /// </summary> void SetupEnginesAndEntities() { //The Engines Root is the core of Svelto.ECS. You must NEVER inject the EngineRoot //as it is, therefore the composition root must hold a reference or it will be //GCed. //the UnitySumbmissionEntityViewScheduler is the scheduler that is used by the EnginesRoot to know //when to inject the EntityViews. You shouldn't use a custom one unless you know what you //are doing or you are not working with Unity. _enginesRoot = new EnginesRoot(new UnitySumbmissionEntityViewScheduler()); //Engines root can never be held by anything else than the context itself to avoid leaks //That's why the EntityFactory and EntityFunctions are generated. //The EntityFactory can be injected inside factories (or engine acting as factories) //to build new entities dynamically _entityFactory = _enginesRoot.GenerateEntityFactory(); //The entity functions is a set of utility operations on Entities, including //removing an entity. I couldn't find a better name so far. var entityFunctions = _enginesRoot.GenerateEntityFunctions(); //GameObjectFactory allows to create GameObjects without using the Static //method GameObject.Instantiate. While it seems a complication //it's important to keep the engines testable and not //coupled with hard dependencies references (read my articles to understand //how dependency injection works and why solving dependencies //with static classes and singletons is a terrible mistake) GameObjectFactory factory = new GameObjectFactory(); //the ISequencer is one of the 3 official ways available in Svelto.ECS //to communicate. They are mainly used for two specific cases: //1) specify a strict execution order between engines (engine logic //is executed horizontally instead than vertically, I will talk about this //in my articles). 2) filter a data token passed as parameter through //engines. The ISequencer is also not the common way to communicate //between engines Sequencer playerDamageSequence = new Sequencer(); Sequencer enemyDamageSequence = new Sequencer(); Sequencer enemyWaveSequence = new Sequencer(); Sequencer enemySpawnSequence = new Sequencer(); Sequencer bonusHealthSequence = new Sequencer(); Sequencer bonusAmmoSequence = new Sequencer(); Sequencer bonusHealthSpawnSequence = new Sequencer(); Sequencer bonusAmmoSpawnSequence = new Sequencer(); Sequencer ammoUpdateSequence = new Sequencer(); Sequencer pushPowerSequence = new Sequencer(); //wrap non testable unity static classes, so that //can be mocked if needed. IRayCaster rayCaster = new RayCaster(); ITime time = new Time(); //Player related engines. ALL the dependecies must be solved at this point //through constructor injection. var playerHealthEngine = new HealthEngine(playerDamageSequence); var playerShootingEngine = new PlayerGunShootingEngine(enemyDamageSequence, ammoUpdateSequence, rayCaster, time); var playerMovementEngine = new PlayerMovementEngine(rayCaster, time); var playerAnimationEngine = new PlayerAnimationEngine(); var playerDeathEngine = new PlayerDeathEngine(entityFunctions); //Enemy related engines var enemyAnimationEngine = new EnemyAnimationEngine(); //HealthEngine is a different object for the enemy because it uses a different sequence var enemyHealthEngine = new HealthEngine(enemyDamageSequence); var enemyAttackEngine = new EnemyAttackEngine(playerDamageSequence, time); var enemyMovementEngine = new EnemyMovementEngine(); var enemySpawnerEngine = new EnemySpawnerEngine(enemyWaveSequence, enemySpawnSequence, factory, _entityFactory); var enemyDeathEngine = new EnemyDeathEngine(entityFunctions, time); var enemyWaveEngine = new EnemyWaveEngine(enemyWaveSequence); //bonus engines var bonusHealthEngine = new BonusHealthEngine(bonusHealthSequence); var bonusCollectedEngine = new BonusCollectedEngine(entityFunctions); var bonusAnimationEngine = new BonusAnimationEngine(); var bonusHealthSpawnerEngine = new BonusHealthSpawnerEngine(bonusHealthSpawnSequence, factory, _entityFactory); var bonusAmmoSpawnerEngine = new BonusAmmoSpawnerEngine(bonusAmmoSpawnSequence, factory, _entityFactory); var bonusAmmoEngine = new BonusAmmoEngine(bonusAmmoSequence); var bonusSoundEngine = new BonusSoundEngine(); //powers engines var pushPowerEngine = new PushPowerEngine(pushPowerSequence, time); var powerSoundEngine = new PowerSoundEngine(); //hud and sound engines var hudEngine = new HUDEngine(time); var enemyCountEngine = new EnemyHUDEngine(); var damageSoundEngine = new DamageSoundEngine(); var scoreEngine = new ScoreEngine(); //The ISequencer implementaton is very simple, but allows to perform //complex concatenation including loops and conditional branching. #region "Setting Sequences" playerDamageSequence.SetSequence( new Steps //sequence of steps, this is a dictionary! { { //first step enemyAttackEngine, //this step can be triggered only by this engine through the Next function new To //this step can lead only to one branch { //this is the only engine that will be called when enemyAttackEngine triggers Next() new IStep[] { playerHealthEngine } } }, { //second step playerHealthEngine, //this step can be triggered only by this engine through the Next function new To //once the playerHealthEngine calls step, all these engines will be called at once //depending by the condition a different set of engines will be triggered. The //order of the engines triggered is guaranteed. { //these engines will be called when the Next function is called with the DamageCondition.damage set { DamageCondition.Damage, new IStep[] { hudEngine, damageSoundEngine } }, //these engines will be called when the Next function is called with the DamageCondition.dead set { DamageCondition.Dead, new IStep[] { hudEngine, damageSoundEngine, playerMovementEngine, playerAnimationEngine, enemyAnimationEngine, playerDeathEngine } } } } } ); enemyDamageSequence.SetSequence( new Steps { { playerShootingEngine, new To { //in every case go to enemyHealthEngine new IStep[] { enemyHealthEngine } } }, { enemyHealthEngine, new To { { DamageCondition.Damage, new IStep[] { enemyAnimationEngine, damageSoundEngine } }, { DamageCondition.Dead, new IStep[] { scoreEngine, enemyMovementEngine, enemyAnimationEngine, enemySpawnerEngine, enemyCountEngine, damageSoundEngine, enemyDeathEngine } }, } } } ); enemyWaveSequence.SetSequence( new Steps { { enemySpawnerEngine, new To { enemyWaveEngine, } }, { enemyWaveEngine, new To { { WaveCondition.Next, new IStep[] { enemySpawnerEngine, enemyCountEngine, hudEngine } }, { WaveCondition.Last, new IStep[] { enemySpawnerEngine, enemyCountEngine, hudEngine } }, { WaveCondition.Stop, new IStep[] { hudEngine } }, } } } ); enemySpawnSequence.SetSequence( new Steps { { enemySpawnerEngine, new To { enemyMovementEngine, } } } ); bonusHealthSequence.SetSequence( new Steps { { bonusHealthEngine, new To { { new IStep[] { playerHealthEngine, hudEngine, bonusSoundEngine, bonusAnimationEngine, bonusCollectedEngine } } } } } ); bonusHealthSpawnSequence.SetSequence( new Steps { { bonusHealthSpawnerEngine, new To { bonusSoundEngine, } } } ); bonusAmmoSpawnSequence.SetSequence( new Steps { { bonusAmmoSpawnerEngine, new To { bonusSoundEngine, } } } ); ammoUpdateSequence.SetSequence( new Steps { { playerShootingEngine, new To { hudEngine, } } } ); bonusAmmoSequence.SetSequence( new Steps { { bonusAmmoEngine, new To { { new IStep[] { playerShootingEngine, hudEngine, bonusSoundEngine, bonusAnimationEngine, bonusCollectedEngine } } } } } ); pushPowerSequence.SetSequence( new Steps { { pushPowerEngine, new To { { PowerCondition.Start, new IStep[] { powerSoundEngine, hudEngine } }, { PowerCondition.Stop, new IStep[] { powerSoundEngine } }, } } } ); #endregion //Mandatory step to make engines work //Player engines _enginesRoot.AddEngine(playerMovementEngine); _enginesRoot.AddEngine(playerAnimationEngine); _enginesRoot.AddEngine(playerShootingEngine); _enginesRoot.AddEngine(playerHealthEngine); _enginesRoot.AddEngine(new PlayerInputEngine()); _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine()); //enemy engines _enginesRoot.AddEngine(enemySpawnerEngine); _enginesRoot.AddEngine(enemyWaveEngine); _enginesRoot.AddEngine(enemyAttackEngine); _enginesRoot.AddEngine(enemyMovementEngine); _enginesRoot.AddEngine(enemyAnimationEngine); _enginesRoot.AddEngine(enemyHealthEngine); _enginesRoot.AddEngine(enemyDeathEngine); //bonus engines _enginesRoot.AddEngine(bonusHealthEngine); _enginesRoot.AddEngine(bonusAmmoEngine); _enginesRoot.AddEngine(bonusCollectedEngine); _enginesRoot.AddEngine(bonusAnimationEngine); _enginesRoot.AddEngine(bonusHealthSpawnerEngine); _enginesRoot.AddEngine(bonusAmmoSpawnerEngine); //powers engines _enginesRoot.AddEngine(powerSoundEngine); _enginesRoot.AddEngine(pushPowerEngine); //other engines _enginesRoot.AddEngine(new CameraFollowTargetEngine(time)); _enginesRoot.AddEngine(damageSoundEngine); _enginesRoot.AddEngine(bonusSoundEngine); _enginesRoot.AddEngine(hudEngine); _enginesRoot.AddEngine(scoreEngine); _enginesRoot.AddEngine(enemyCountEngine); }