08/04/2019 Note: this article is quite old (pre SECS 2.5) but since I ported the example to Svelto 2.8 (beta version at the time of this note), I updated this article as well, although it is still missing some important details related to the new features that ship with the newer SECS versions.

Introduction

Lately I have been discussing Svelto.ECS extensively with several, more or less experienced, programmers. I gathered a lot of feedback and took a lot of notes that I will be using as starting point for my next articles where I will talk more about the theory and good practices. Just to give a little spoiler, I realized that the biggest obstacle that new coders face when starting using Svelto.ECS is the shift of programming paradigm. It’s astonishing how much I have to write to explain the novel concepts introduced by Svelto.ECS compared to the small amount of code written to develop the framework. In fact, while the framework itself is very simple (and lightweight), learning how to move from the class inheritance heavy object oriented design or even the naive Unity components based design, to the “new” modular and decoupled design that Svelto.ECS forces to use, is what usually discourages people from adopting the framework.

Being the framework extensively used at Freejam, I also noticed that it’s thanks to my continuous availability to explain the fundamental concepts that my colleagues have less of a hard time to get in to the flow. Although Svelto.ECS tries to be as rigid as possible, bad habits are hard to die, so users tend to abuse the little flexibility left to adapt the framework to the “old” paradigms they are comfortable with. This can result in a catastrophe due to misunderstandings or reinterpretation of the concepts that are behind the logic of the framework. That’s why I am committed to write as many articles as possible, especially because I know that the ECS paradigm is the best solution I found so far to write efficient and maintainable code for large projects that are refactored and reshaped multiple times over the span of years and Robocraft as well as Cardlife are the existing proof of what I try to demonstrate.

I am not going to talk much about the theories behind the framework in this article, but I want to remind what took me to the path of ditching the use of an IoC Container and starting using exclusively the ECS framework: an IoC container is a very dangerous tool if used without Inversion of Control in mind. As you should have read from my previous articles, I differentiate between Inversion of Creation Control and Inversion of Flow Control. Inversion of Flow Control is basically the Hollywood principle “Don’t call us, we will call you”. This means that dependencies injected should never been used directly through public methods, in doing so you would just use an IoC container as a substitute of any other form of global injection like singletons. However, once an IoC container is used following the IoC principle, it mainly ends up in using repeatedly the Strategy Pattern to inject managers used only to register the entities to manage. In a real Inversion of Flow Control context, managers are always in charge of handle entities. Does it sounds like what the ECS pattern is about? It indeed does. From this reasoning I took the ECS pattern and evolved it into a rigid framework to the point it can be considered using it like adopting a new coding paradigm.

The Survival Example

let’s start downloading the project from https://github.com/sebas77/Svelto.MiniExamples/tree/master/Example2-Survival

open the scene Level01 and open the project in your IDE. Everything starts from maincontext.cs file.

The Composition Root and the EnginesRoot

The class Main is the Application Composition Root. A Composition Root is where dependencies are created and injected (I talk a lot about this in my articles). A composition root belongs to the context, but a context can have more than one composition root. For example a factory is a composition root. Furthermore an application can have more than one context but this is an advanced scenario and not part of this example.

Before to start digging in the code, let’s introduce the first terms of the Svelto.ECS domain language. ECS is an acronym for Entity Component System. The ECS infrastructure has been analyzed abundantly with several articles written by many authors, but while the basic concepts are in common, the implementations differ a lot. Above all, there isn’t a standard way to solve the few problems rising from using ECS oriented code. That’s where I put most of my effort on, but this is something I will talk about later or in the next articles. At the heart of the theory there are the concepts of Entity, Components (of the entities) and Systems. While I understand why the word System has been used historically, I initially found it not intuitive for the purpose, so Engine is synonym of System and you may use it interchangeably according your preferences.

The EnginesRoot class is the core of Svelto.ECS. With it is possible to register the engines and build all the entities of the game. It doesn’t make much sense to create engines dynamically, so all the engines should be added in the EnginesRoot instance from the same composition root where it has been created. For similar reasons, an EnginesRoot instance must never been injected and engines can’t be removed once added.

We need at least one composition root to be able to create and inject dependencies wherever needed. Yes, it’s possible to have even more than one EnginesRoot per application, but this is too not part of this article, which I will try to keep as simple as possible. This is how a composition root with engines creation and dependencies injection looks like:

//The Engines Root is the core of Svelto.ECS. You shouldn't inject the EngineRoot, //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 submit the entities. Custom ones can be created for special cases. _unityEntitySubmissionScheduler = new UnityEntitySubmissionScheduler(); _enginesRoot = new EnginesRoot(_unityEntitySubmissionScheduler); //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(); //Sequencers are the official way to guarantee order between engines, but may not be the best way for //your product. var playerDeathSequence = new PlayerDeathSequencer(); var enemyDeathSequence = new EnemyDeathSequencer(); //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 dependencies must be solved at this point through constructor injection. var playerShootingEngine = new PlayerGunShootingEngine(rayCaster, time); var playerMovementEngine = new PlayerMovementEngine(rayCaster, time); var playerAnimationEngine = new PlayerAnimationEngine(); var playerDeathEngine = new PlayerDeathEngine(playerDeathSequence, entityFunctions); //Enemy related engines var enemyAnimationEngine = new EnemyAnimationEngine(time, enemyDeathSequence, entityFunctions); var enemyAttackEngine = new EnemyAttackEngine(time); var enemyMovementEngine = new EnemyMovementEngine(); //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(); //Factory is one of the few patterns that work very well with ECS. Its use is highly encouraged var enemyFactory = new EnemyFactory(gameObjectFactory, _entityFactory); var enemySpawnerEngine = new EnemySpawnerEngine(enemyFactory, entityFunctions); var enemyDeathEngine = new EnemyDeathEngine(entityFunctions, enemyDeathSequence); //hud and sound engines var hudEngine = new HUDEngine(time); var damageSoundEngine = new DamageSoundEngine(); var scoreEngine = new ScoreEngine(); //The ISequencer implementation is very simple, but allows to perform //complex concatenation including loops and conditional branching. //These two sequencers are a real stretch and are shown only for explanatory purposes. //Please do not see sequencers as a way to dispatch or broadcast events, they are meant only and exclusively //to guarantee the order of execution of the involved engines. //For this reason the use of sequencers is and must be actually rare, as perfectly encapsulated engines //do not need to be executed in specific order. //a Sequencer can: //- ensure the order of execution through one step only (one step executes in order several engines) //- ensure the order of execution through several steps. Each engine inside each step has the responsibility //to trigger the next step through the use of the Next() function //- create paths with branches and loop using the Condition parameter. playerDeathSequence.SetSequence(playerDeathEngine, playerMovementEngine, playerAnimationEngine, enemyAnimationEngine, damageSoundEngine, hudEngine); enemyDeathSequence.SetSequence(enemyDeathEngine, scoreEngine, damageSoundEngine, enemyAnimationEngine, enemySpawnerEngine); //All the logic of the game must lie inside engines //Player engines _enginesRoot.AddEngine(playerMovementEngine); _enginesRoot.AddEngine(playerAnimationEngine); _enginesRoot.AddEngine(playerShootingEngine); _enginesRoot.AddEngine(new PlayerInputEngine()); _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine()); _enginesRoot.AddEngine(playerDeathEngine); _enginesRoot.AddEngine(new PlayerSpawnerEngine(gameObjectFactory, _entityFactory)); //enemy engines _enginesRoot.AddEngine(enemySpawnerEngine); _enginesRoot.AddEngine(enemyAttackEngine); _enginesRoot.AddEngine(enemyMovementEngine); _enginesRoot.AddEngine(enemyAnimationEngine); _enginesRoot.AddEngine(enemyDeathEngine); //other engines _enginesRoot.AddEngine(new ApplyingDamageToTargetsEngine()); _enginesRoot.AddEngine(new CameraFollowTargetEngine(time)); _enginesRoot.AddEngine(new CharactersDeathEngine()); _enginesRoot.AddEngine(damageSoundEngine); _enginesRoot.AddEngine(hudEngine); _enginesRoot.AddEngine(scoreEngine); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 //The Engines Root is the core of Svelto.ECS. You shouldn't inject the EngineRoot, //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 submit the entities. Custom ones can be created for special cases. _unityEntitySubmissionScheduler = new UnityEntitySubmissionScheduler ( ) ; _enginesRoot = new EnginesRoot ( _unityEntitySubmissionScheduler ) ; //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 ( ) ; //Sequencers are the official way to guarantee order between engines, but may not be the best way for //your product. var playerDeathSequence = new PlayerDeathSequencer ( ) ; var enemyDeathSequence = new EnemyDeathSequencer ( ) ; //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 dependencies must be solved at this point through constructor injection. var playerShootingEngine = new PlayerGunShootingEngine ( rayCaster , time ) ; var playerMovementEngine = new PlayerMovementEngine ( rayCaster , time ) ; var playerAnimationEngine = new PlayerAnimationEngine ( ) ; var playerDeathEngine = new PlayerDeathEngine ( playerDeathSequence , entityFunctions ) ; //Enemy related engines var enemyAnimationEngine = new EnemyAnimationEngine ( time , enemyDeathSequence , entityFunctions ) ; var enemyAttackEngine = new EnemyAttackEngine ( time ) ; var enemyMovementEngine = new EnemyMovementEngine ( ) ; //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 ( ) ; //Factory is one of the few patterns that work very well with ECS. Its use is highly encouraged var enemyFactory = new EnemyFactory ( gameObjectFactory , _entityFactory ) ; var enemySpawnerEngine = new EnemySpawnerEngine ( enemyFactory , entityFunctions ) ; var enemyDeathEngine = new EnemyDeathEngine ( entityFunctions , enemyDeathSequence ) ; //hud and sound engines var hudEngine = new HUDEngine ( time ) ; var damageSoundEngine = new DamageSoundEngine ( ) ; var scoreEngine = new ScoreEngine ( ) ; //The ISequencer implementation is very simple, but allows to perform //complex concatenation including loops and conditional branching. //These two sequencers are a real stretch and are shown only for explanatory purposes. //Please do not see sequencers as a way to dispatch or broadcast events, they are meant only and exclusively //to guarantee the order of execution of the involved engines. //For this reason the use of sequencers is and must be actually rare, as perfectly encapsulated engines //do not need to be executed in specific order. //a Sequencer can: //- ensure the order of execution through one step only (one step executes in order several engines) //- ensure the order of execution through several steps. Each engine inside each step has the responsibility //to trigger the next step through the use of the Next() function //- create paths with branches and loop using the Condition parameter. playerDeathSequence . SetSequence ( playerDeathEngine , playerMovementEngine , playerAnimationEngine , enemyAnimationEngine , damageSoundEngine , hudEngine ) ; enemyDeathSequence . SetSequence ( enemyDeathEngine , scoreEngine , damageSoundEngine , enemyAnimationEngine , enemySpawnerEngine ) ; //All the logic of the game must lie inside engines //Player engines _enginesRoot . AddEngine ( playerMovementEngine ) ; _enginesRoot . AddEngine ( playerAnimationEngine ) ; _enginesRoot . AddEngine ( playerShootingEngine ) ; _enginesRoot . AddEngine ( new PlayerInputEngine ( ) ) ; _enginesRoot . AddEngine ( new PlayerGunShootingFXsEngine ( ) ) ; _enginesRoot . AddEngine ( playerDeathEngine ) ; _enginesRoot . AddEngine ( new PlayerSpawnerEngine ( gameObjectFactory , _entityFactory ) ) ; //enemy engines _enginesRoot . AddEngine ( enemySpawnerEngine ) ; _enginesRoot . AddEngine ( enemyAttackEngine ) ; _enginesRoot . AddEngine ( enemyMovementEngine ) ; _enginesRoot . AddEngine ( enemyAnimationEngine ) ; _enginesRoot . AddEngine ( enemyDeathEngine ) ; //other engines _enginesRoot . AddEngine ( new ApplyingDamageToTargetsEngine ( ) ) ; _enginesRoot . AddEngine ( new CameraFollowTargetEngine ( time ) ) ; _enginesRoot . AddEngine ( new CharactersDeathEngine ( ) ) ; _enginesRoot . AddEngine ( damageSoundEngine ) ; _enginesRoot . AddEngine ( hudEngine ) ; _enginesRoot . AddEngine ( scoreEngine ) ;

This code is part of the survival example which is now well commented and follows almost all the good practice rules that I suggest to use, including keeping the engines logic platform independent and testable. The comments will help you to understand most of it, but a project of this size may be already too much to swallow if you are new to Svelto. For this reason, let’s proceed as we would have done if started from scratch:

Entities

the first step, after creating the empty Composition Root and an instance of the EnginesRoot class, would be to identify the entities you want to work with first. Let’s logically start from the Player Entity. The Svelto.ECS entity must not be confused with the Unity GameObject. If you had the chance to read other ECS related articles, you will see that in many of those, entities are often described as indices. This is probably the worst way possible to introduce the concept. While this is true for Svelto.ECS too, it’s well hidden. As matter of fact, I want the Svelto.ECS user to visualize, describe and identify every single entity in terms of Game Design Domain language. An entity in code must be an entity described in the game design document. Any other form of entity definition will result in a contrived way to adapt your old paradigms to the Svelto.ECS needs. Follow this fundamental rule and you won’t be wrong in most of the cases. An entity class doesn’t exist per se in code, but you still must define it in a not abstract way.

Engines

Next step is to think about what behaviours to give to this Entity. Every behaviour is always modeled inside an Engine, there is no way to add logic in any other classes inside a Svelto.ECS application. For this purpose we can start from the player character movement and define the PlayerMovementEngine class. The name of the engine must be very specific, as the more specific it is, the higher is the chance the Engine will follow the Single Responsibility Rule. Naming classes properly in Svelto.ECS is of fundamental importance. It’s not just to comunicate clearly your intentions, but it’s actually more about letting you think about your intentions.

For the same reason I would suggest (as good practice) to put your engine inside a specialised namespace. Using specialized namespaces helps a lot to identify code design errors when entities are used inside not compatible namespaces. For example, you wouldn’t expect any enemy entity to be used inside a player namespace, unless you want to break the good rules related to modularity and decoupling of objects. The idea is that entities of a specific namespace can be used only inside that namespace or a less abstracted namespace. While with Svelto.ECS is much harder to turn your code in to a fully fledged spaghetti bowl, where dependencies are injected everywhere and randomly, this rule will help you to take your code to an even better level where dependencies are correctly abstracted between classes.

08/04/19 Note: this good practice has been taken now to another level, encapsulating the layers of abstraction inside separate composition roots belonging in different assemblies. I never discussed this approach formally, so another article or wiki paragraph will come later on.

In Svelto.ECS abstraction is pushed on several fronts, but ECS intrinsically promote separation of the data from the logic that must handle it. Entities are defined by their data, not their behaviours. Engines instead are the place where to put the shared behaviours of entities, so that engines can always operate on a set of entities.

Svelto.ECS, and the ECS paradigm in general, allows the coders to achieve one of the holy grails of clean programming, that is the perfect encapsulation of logic. Engines must not have public functions, consequentially Engines are never injected in any other engine. If you think to pass an engine as parameter, you are probably doing something wrong already.

Compared to Unity monobehaviours, engines already show the first great benefit, which is the possibility to access to all the entity states of a given type from the same code scope. This means that the code can easily use the state of all the entities directly from the same place where the shared entity logic is going to run. Furthermore separate engines can handle the same entities so that an engine can change an entity state while another engine can read it, effectively putting the two engines in communication through the same entity data. An example of this can be seen with the engines PlayerGunShootingEngine and PlayerGunShootingFxsEngine. In this case the two engines are in the same namespace, so they can share the same entity data. PlayerGunShootingEngine determines if a player target (an enemy) has been damaged and writes the lastTargetPosition of the IGunAttributesComponent (which is a component of the PlayerGunEntity). the PlayerGunShootFxsEngine handles the graphic effects of the gun and reads the position of the currently targeted player target. This is an example of communication between engines through data polling. It’s quite logical that engines should (and must) never hold states.

public class PlayerGunShootingEngine : MultiEntitiesReactiveEngine<GunEntityViewStruct, PlayerEntityViewStruct>, IQueryingEntitiesEngine { readonly IRayCaster _rayCaster; readonly ITaskRoutine<IEnumerator> _taskRoutine; readonly ITime _time; public PlayerGunShootingEngine(IRayCaster rayCaster, ITime time) { _rayCaster = rayCaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine(StandardSchedulers.physicScheduler); _taskRoutine.SetEnumerator(Tick()); } public IEntitiesDB entitiesDB { set; private get; } public void Ready() { _taskRoutine.Start(); } protected override void Add(ref GunEntityViewStruct entityView, ExclusiveGroup.ExclusiveGroupStruct? previousGroup) { } protected override void Remove(ref GunEntityViewStruct entityView, bool itsaSwap) { _taskRoutine.Stop(); } protected override void Add(ref PlayerEntityViewStruct entityView, ExclusiveGroup.ExclusiveGroupStruct? previousGroup) { } protected override void Remove(ref PlayerEntityViewStruct entityView, bool itsaSwap) { _taskRoutine.Stop(); } IEnumerator Tick() { while (entitiesDB.HasAny<PlayerEntityViewStruct>(ECSGroups.Player) == false || entitiesDB.HasAny<GunEntityViewStruct>(ECSGroups.Player) == false) yield return null; //skip a frame //never changes var playerGunEntities = entitiesDB.QueryEntities<GunEntityViewStruct>(ECSGroups.Player, out var count); //never changes var playerEntities = entitiesDB.QueryEntities<PlayerInputDataStruct>(ECSGroups.Player, out count); while (true) { var playerGunComponent = playerGunEntities[0].gunComponent; playerGunComponent.timer += _time.deltaTime; if (playerEntities[0].fire && playerGunComponent.timer >= playerGunEntities[0].gunComponent.timeBetweenBullets) Shoot(playerGunEntities[0]); yield return null; } } /// <summary> /// Design note: shooting and find a target are possibly two different responsibilities /// and probably would need two different engines. /// </summary> /// <param name="playerGunEntityView"></param> void Shoot(GunEntityViewStruct playerGunEntityView) { var playerGunComponent = playerGunEntityView.gunComponent; var playerGunHitComponent = playerGunEntityView.gunHitTargetComponent; playerGunComponent.timer = 0; var entityHit = _rayCaster.CheckHit(playerGunComponent.shootRay, playerGunComponent.range, GAME_LAYERS.ENEMY_LAYER, GAME_LAYERS.SHOOTABLE_MASK | GAME_LAYERS.ENEMY_MASK, out var point, out var instanceID); if (entityHit) { var damageInfo = new DamageInfo(playerGunComponent.damagePerShot, point); //note how the GameObject GetInstanceID is used to identify the entity as well if (instanceID != -1) entitiesDB.QueryEntity<DamageableEntityStruct>((uint) instanceID, ECSGroups.PlayerTargets) .damageInfo = damageInfo; playerGunComponent.lastTargetPosition = point; playerGunHitComponent.targetHit.value = true; } else { playerGunHitComponent.targetHit.value = false; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public class PlayerGunShootingEngine : MultiEntitiesReactiveEngine < GunEntityViewStruct , PlayerEntityViewStruct > , IQueryingEntitiesEngine { readonly IRayCaster _rayCaster ; readonly ITaskRoutine < IEnumerator > _taskRoutine ; readonly ITime _time ; public PlayerGunShootingEngine ( IRayCaster rayCaster , ITime time ) { _rayCaster = rayCaster ; _time = time ; _taskRoutine = TaskRunner . Instance . AllocateNewTaskRoutine ( StandardSchedulers . physicScheduler ) ; _taskRoutine . SetEnumerator ( Tick ( ) ) ; } public IEntitiesDB entitiesDB { set ; private get ; } public void Ready ( ) { _taskRoutine . Start ( ) ; } protected override void Add ( ref GunEntityViewStruct entityView , ExclusiveGroup . ExclusiveGroupStruct ? previousGroup ) { } protected override void Remove ( ref GunEntityViewStruct entityView , bool itsaSwap ) { _taskRoutine . Stop ( ) ; } protected override void Add ( ref PlayerEntityViewStruct entityView , ExclusiveGroup . ExclusiveGroupStruct ? previousGroup ) { } protected override void Remove ( ref PlayerEntityViewStruct entityView , bool itsaSwap ) { _taskRoutine . Stop ( ) ; } IEnumerator Tick ( ) { while ( entitiesDB . HasAny < PlayerEntityViewStruct > ( ECSGroups . Player ) == false | | entitiesDB . HasAny < GunEntityViewStruct > ( ECSGroups . Player ) == false ) yield return null ; //skip a frame //never changes var playerGunEntities = entitiesDB . QueryEntities < GunEntityViewStruct > ( ECSGroups . Player , out var count ) ; //never changes var playerEntities = entitiesDB . QueryEntities < PlayerInputDataStruct > ( ECSGroups . Player , out count ) ; while ( true ) { var playerGunComponent = playerGunEntities [ 0 ] . gunComponent ; playerGunComponent . timer += _time . deltaTime ; if ( playerEntities [ 0 ] . fire && playerGunComponent.timer >= playerGunEntities[0].gunComponent.timeBetweenBullets) Shoot(playerGunEntities[0]); yield return null ; } } /// <summary> /// Design note: shooting and find a target are possibly two different responsibilities /// and probably would need two different engines. /// </summary> /// <param name="playerGunEntityView"></param> void Shoot ( GunEntityViewStruct playerGunEntityView ) { var playerGunComponent = playerGunEntityView . gunComponent ; var playerGunHitComponent = playerGunEntityView . gunHitTargetComponent ; playerGunComponent . timer = 0 ; var entityHit = _rayCaster . CheckHit ( playerGunComponent . shootRay , playerGunComponent . range , GAME_LAYERS . ENEMY_LAYER , GAME_LAYERS . SHOOTABLE_MASK | GAME_LAYERS . ENEMY_MASK , out var point , out var instanceID ) ; if ( entityHit ) { var damageInfo = new DamageInfo ( playerGunComponent . damagePerShot , point ) ; //note how the GameObject GetInstanceID is used to identify the entity as well if ( instanceID ! = - 1 ) entitiesDB . QueryEntity < DamageableEntityStruct > ( ( uint ) instanceID , ECSGroups . PlayerTargets ) . damageInfo = damageInfo ; playerGunComponent . lastTargetPosition = point ; playerGunHitComponent . targetHit . value = true ; } else { playerGunHitComponent . targetHit . value = false ; } } }

Engines are not supposed to know how to interact with other engines. The best engines are the ones that do not even need to trigger any form of external communication. These engines reflect a well encapsulated behaviour and usually work through a loop. Loops can be modeled with a Svelto.Task task inside Svelto.ECS applications. Since the player movement must be updated every physic tick, it would be natural to create a task that is executed every physic update. Svelto.Tasks allows to run every kind of IEnumerator on several types of schedulers. In this case we decide to create a task on the PhysicScheduler that allows to update the player position:

Svelto.Tasks can run IEnumerator directly (using the extension methods Run and RunOnScheduler for Svelto Tasks 1.5 and RunOn for Svelto Tasks 2.0), but in this case I decided to use a TaskRoutine, as I want to stop it when the entity is removed.

Svelto.ECS exploit the Add and Remove callbacks to know when specific entities are added or removed. These callbacks are used also during a swap (note: these interfaces are currently undergoing some refactoring to make the difference between Add,Remove and Swap more obvious, so this paragraph may be obsolete soon).

Engines more commonly implement the IQueryingEntityViewEngine interface instead. This allows to access the entity database and retrieve data from it. Remember you can always query any entity from inside an engine, but in the moment you are querying an entity that is not compatible with the namespace where the engine lies, then you know you are already doing something wrong. Engines should never assume that the entities are available and they must work on a set of entities. Engines must always be functional, regardless the number of entities currently available (0, 1 or N). This rule can be broken if you know by design that specific entities will be unique (like the Player in this example). A very common approach to how to query entities is found in the EnemyMovementEngine:

public class EnemyMovementEngine : IQueryingEntitiesEngine { public IEntitiesDB entitiesDB { set; private get; } public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { //query all the enemies from the standard group (no disabled nor respawning) var enemyTargetEntityViews = entitiesDB.QueryEntities<EnemyTargetEntityViewStruct>( ECSGroups.EnemyTargets, out var enemyTargetsCount); if (enemyTargetsCount > 0) { var enemies = entitiesDB.QueryEntities<EnemyEntityViewStruct>(ECSGroups.ActiveEnemies, out var enemiesCount); //using always the first target because in this case I know there can be only one, but if //there were more, I could use different strategies, like choose the closest. This is //for a very simple AI scenario of course. for (var i = 0; i < enemiesCount; i++) enemies[i].movementComponent.navMeshDestination = enemyTargetEntityViews[0].targetPositionComponent.position; } yield return null; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class EnemyMovementEngine : IQueryingEntitiesEngine { public IEntitiesDB entitiesDB { set ; private get ; } public void Ready ( ) { Tick ( ) . Run ( ) ; } IEnumerator Tick ( ) { while ( true ) { //query all the enemies from the standard group (no disabled nor respawning) var enemyTargetEntityViews = entitiesDB . QueryEntities < EnemyTargetEntityViewStruct > ( ECSGroups . EnemyTargets , out var enemyTargetsCount ) ; if ( enemyTargetsCount > 0 ) { var enemies = entitiesDB . QueryEntities < EnemyEntityViewStruct > ( ECSGroups . ActiveEnemies , out var enemiesCount ) ; //using always the first target because in this case I know there can be only one, but if //there were more, I could use different strategies, like choose the closest. This is //for a very simple AI scenario of course. for ( var i = 0 ; i < enemiesCount ; i ++ ) enemies [ i ] . movementComponent . navMeshDestination = enemyTargetEntityViews [ 0 ] . targetPositionComponent . position ; } yield return null ; } } }

In this case the engine main loop is running directly immediately on the predefined scheduler. Tick().Run() shows the shortest way to run an IEnumerator with Svelto.Tasks. The IEnumerator will keep on yielding to the next frame until at least one Enemy Target is found. Since we know that there will be always just one target (another not nice assumption), I pickup the first available. While the Enemy Target can be just one (although could have been more!), the enemies are many and the engine takes care of the movement logic for all of them.

Note that the component never exposes the Unity navmesh dependency directly. Entity Component, as I will say later, must always expose value types. In this case this rule also allows to keep the code testable, as the field value type navMeshDestination could be later implemented without using a Unity Nav Mesh stub.

To conclude the paragraph related to engines, note that there isn’t such a thing as a too small engine. Hence, don’t be afraid to write an engine even for just few lines of code, after all you can’t put logic anywhere else and you want your engines to follow the Single Responsibility Rule.

EntityViews

So far we introduced the concept of Engine and an abstracted definition of Entity, let’s now define what an EntityView is. I have to admit, of the 5 concepts of which Svelto.ECS is built upon, the EntityViews is probably the most confusing. Previously called Node, name taken from the Ash ECS framework, I realized that node meant nothing. EntityView may be confusing as well, since programmers usually associate views with the concept coming from Model View Controller pattern, however in Svelto.ECS is called View because an EntityView is how the Engine views an Entity. This scheme of the Svelto.ECS concepts should help a bit:

I suggested to start working on the Engine first, thus we are on the right side of this scheme. Every Engine comes with its own set of EntityViews. An Engine can reuse namespace compatible EntityViews, but it’s more common for an Engine to define its entity views. The Engine doesn’t care if a Player Entity definition actually exists, it dictates the fact that it needs a PlayerEntityView to work. The writing of the code is driven by the Engine needs, you shouldn’t create the entity and its field before to know how to use those fields. In a more complex scenario, the name of the EntityView could have been even more specific.

(note: since the introduction of Svelto.ECS 2.5, EntityStructs are much more relevant then EntityViewStructs. I still call EntityStructs and EntityViewStructs entity views, because I still like the original definition. However EntityStructs are effectively entity components)

EntityViews are classes that hold only Entity Components. Entity Components in Svelto.ECS are always interfaces that must be implemented, but the Engine and EntityView doesn’t need to know the implementation. For this reason, without even implementing the component yet, I can just start to type the logic in the engine like if it was in place:

Even if PlayerEntityView is still an empty class, I start to use the fields I need. Since an EntityView can hold only components, the field must be a component interface.

Hoping you use an IDE that supports refactoring, the IDE will immediately warn you that the field you are trying to access actually doesn’t exist. This is where the refactoring tools can help you speeding up the writing of the code. For example, using Jetbrains rider (but it’s the same with Visual Studio) you can create the field automatically like this:

this would add the component field in the EntityView like:

since the IPlayerInputComponent interface doesn’t exist yet, I name it and use on the spot. Then I use again the refactoring tool:

this will create an empty interface, so that now the code would look like:

the inputComponent field now exists, but it’s empty so the input field is not defined yet, but I know I need it.

Yes that’s right, I would use again the refactoring tool:

so that the IPlayerInputComponent interface will be filled with the right properties. As long as I don’t run the code, I can build it without needing to implement the Entity Component IPlayerInputComponent interface yet. Honestly, once you get in this flow, you will notice how fast can be coding with Svelto.ECS using the IDE refactoring tools.

Components

We understood that engines model behaviors for a set of entities and we understood that engines do not use entities directly, but use the entity components through entity views. We understood that an entity view can hold ONLY public entity components.

When EntityViewStructs are used to abstract objects coming from OOP platforms/libraries, an entity component is an interface. The Entity Component interface is then implemented with a so called Implementor. We are now starting to define the Entity itself and we are on the left side of the scheme above.

Components should always hold value types and the fields are always getter and setter properties. This allows to create testable code that is not dependent by the implementation of the Not ECS/OOP platforms or libraries used in the project. Moreover it prevents people from cheating and use public functions (which would include logic!) of random objects.

EntityDescriptors

This is where the Entity Descriptors actually come to help to put everything together and hopefully let everything click in place. We know that Engines can access to Entity data through the Entity Components held by the Entity Views. We know that Engines model the behavior of the entities through their entity views, that entity views hold only Entity Components and that Entity Components are value types. While I have given an abstracted definition of entity, we haven’t seen any class that actually represent an entity. This is in line with the concept of entities being just IDs inside a modern ECS framework. However without a proper definition of Entity, this would lead coders to identify Entities with EntityViews, which would be catastrophically wrong. EntityViews is the way several Engines can view the same Entity but, conceptually, they are not the entities. The Entity itself should be always be seen as a set of data defined through the entity components, but even this representation is weak. An EntityDescriptor instance gives the chance to the coder to name properly their entities independently by the engines that are going to handle them. Therefore in the case of the Player Entity, we would need an PlayerEntityDescriptor. This class will then be using to build the entity, and while what it really does is something totally different, the fact that the user is able to write BuildEntity<PlayerEntityDescriptor>() helps immensely to visualize the entities to build and to communicate the intentions to other coders.

However what an EntityDescriptor really does is to build a list of Entity Views!!!

This is how the PlayerEntityDescriptor looks like:

public class PlayerEntityDescriptor : IEntityDescriptor { static readonly IEntityBuilder[] _entitiesToBuild = { new EntityBuilder<PlayerEntityViewStruct>(), new EntityBuilder<DamageableEntityStruct>(), new EntityBuilder<DamageSoundEntityView>(), new EntityBuilder<CameraTargetEntityView>(), new EntityBuilder<HealthEntityStruct>(), new EntityBuilder<EnemyTargetEntityViewStruct>(), new EntityBuilder<PlayerInputDataStruct>() }; public IEntityBuilder[] entitiesToBuild => _entitiesToBuild; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class PlayerEntityDescriptor : IEntityDescriptor { static readonly IEntityBuilder [ ] _entitiesToBuild = { new EntityBuilder < PlayerEntityViewStruct > ( ) , new EntityBuilder < DamageableEntityStruct > ( ) , new EntityBuilder < DamageSoundEntityView > ( ) , new EntityBuilder < CameraTargetEntityView > ( ) , new EntityBuilder < HealthEntityStruct > ( ) , new EntityBuilder < EnemyTargetEntityViewStruct > ( ) , new EntityBuilder < PlayerInputDataStruct > ( ) } ; public IEntityBuilder [ ] entitiesToBuild = > _entitiesToBuild ; }

the EntityDescriptors (and the Implementors) are the only classes that can use identifiers from multiple namespaces. In this case the PlayerEntityDescriptor defines the list of EntityViews to instantiate and inject in the engine when the PlayerEntity is built.

EntityDescriptorHolder

The EntityDescriptorHolder is an extension for Unity and should be used only in specific cases. The most common one is to create a sort of polymorphism storing the information of the entity to build on the Unity GameObject. It can be useful when prefabs are seen exclusively as a way to serialise entities and currently they are commonly used to serialise entities for GUIs. However, building entities explicitly is always preferred, so use EntityDescriptorHolders only when you have understood Svelto.ECS properly otherwise there is the risk to abuse it. This function from the example shows how to use the class:

void BuildEntitiesFromScene(UnityContext contextHolder) { //An EntityDescriptorHolder is a special Svelto.ECS class created to exploit //GameObjects to dynamically retrieve the Entity information attached to it. //Basically a GameObject can be used to hold all the information needed to create //an Entity and later queries to build the entitity itself. //This allow to trigger a sort of polyformic code that can be re-used to //create several type of entities. IEntityDescriptorHolder[] entities = contextHolder.GetComponentsInChildren<IEntityDescriptorHolder>(); //However this common pattern in Svelto.ECS application exists to automatically //create entities from gameobjects already presented in the scene. //I still suggest to avoid this method though and create entities always //manually. Basically EntityDescriptorHolder should be avoided //whenver not strictly necessary. for (int i = 0; i < entities.Length; i++) { var entityDescriptorHolder = entities[i]; var entityDescriptor = entityDescriptorHolder.RetrieveDescriptor(); _entityFactory.BuildEntity (((MonoBehaviour) entityDescriptorHolder).gameObject.GetInstanceID(), entityDescriptor, (entityDescriptorHolder as MonoBehaviour).GetComponentsInChildren<IImplementor>()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void BuildEntitiesFromScene ( UnityContext contextHolder ) { //An EntityDescriptorHolder is a special Svelto.ECS class created to exploit //GameObjects to dynamically retrieve the Entity information attached to it. //Basically a GameObject can be used to hold all the information needed to create //an Entity and later queries to build the entitity itself. //This allow to trigger a sort of polyformic code that can be re-used to //create several type of entities. IEntityDescriptorHolder [ ] entities = contextHolder . GetComponentsInChildren < IEntityDescriptorHolder > ( ) ; //However this common pattern in Svelto.ECS application exists to automatically //create entities from gameobjects already presented in the scene. //I still suggest to avoid this method though and create entities always //manually. Basically EntityDescriptorHolder should be avoided //whenver not strictly necessary. for ( int i = 0 ; i < entities . Length ; i ++ ) { var entityDescriptorHolder = entities [ i ] ; var entityDescriptor = entityDescriptorHolder . RetrieveDescriptor ( ) ; _entityFactory . BuildEntity ( ( ( MonoBehaviour ) entityDescriptorHolder ) . gameObject . GetInstanceID ( ) , entityDescriptor , ( entityDescriptorHolder as MonoBehaviour ) . GetComponentsInChildren < IImplementor > ( ) ) ; } }

Note that with this example I am already using the less preferred, not generic, function BuildEntity. I will talk about it in a bit. The Implementors in this case are always monobehaviours in the gameobject. Also this is not a good practice. I actually should remove this code from the example, but left to show you this other case. Implementors, as we will see next, should be Monobehaviours only when strictly needed!

Implementors

Before to build our entity, let’s define the last concept in Svelto.ECS that is the Implementor. As we know now, Entity Components are always interfaces and in c# interfaces must be implemented. The object that implements those interfaces are called Implementors. Implementors have several important characteristics:

Allow to uncouple the number of objects to build from the number of entity components needed to define the entity data.

Allow to share data between different components, as components expose data through properties, different component properties could return the same implementor field.

Allow to create stub of the entity component interface very easily. This is crucial for leaving the engine code testable.

Act as bridge between Svelto.ECS Engines and third party platforms . This characteristic is of fundamental importance. If you need unity to communicate with the engines you don’t need to use awkward workarounds, simply create an implementor as Monobehaviour. In this way you could use, inside the implementor, Unity callbacks, like OnTriggerEnter/OnTriggerExit and change data according the Unity callback. Logic should not be used inside these callback, except setting entity components data. Here an example:

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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 ; }

Remember the granularity of your EntityViews, entity components and implementors is completely discretional and up to you. More granular they are, more the chance are to be reusable.

Build Entities

Let’s say we have created our Engines, added them in the EnginesRoot, created its EntityViews that use Entity Components as interfaces to be implemented by one or more Implementors. It is now time to build our first entity. An Entity is always built through the Entity Factory instance generated by the EnginesRoot through the function GenerateEntityFactory. Differently than the EnginesRoot instance, an IEntityFactory instance can be injected and passed around. Entities can be built inside the Composition Root or dynamically inside game factories, so for the latter case passing the IEntityFactory by parameter is necessary.

so let’s see how the normal BuildEntity<T> is used inside the EnemySpawnerEngine code

IEnumerator IntervaledTick() { //this is of fundamental importance: Never create implementors as Monobehaviour just to hold //data (especially if read only data). Data should always been retrieved through a service layer //regardless the data source. //The benefits are numerous, including the fact that changing data source would require //only changing the service code. In this simple example I am not using a Service Layer //but you can see the point. //Also note that I am loading the data only once per application run, outside the //main loop. You can always exploit this pattern when you know that the data you need //to use will never change var enemiestoSpawn = ReadEnemySpawningDataServiceRequest(); var enemyAttackData = ReadEnemyAttackDataServiceRequest(); var spawningTimes = new float[enemiestoSpawn.Length]; for (var i = enemiestoSpawn.Length - 1; i >= 0 && _numberOfEnemyToSpawn > 0; --i) spawningTimes[i] = enemiestoSpawn[i].enemySpawnData.spawnTime; _enemyFactory.Preallocate(); while (true) { //Svelto.Tasks can yield Unity YieldInstructions but this comes with a performance hit //so the fastest solution is always to use custom enumerators. To be honest the hit is minimal //but it's better to not abuse it. yield return _waitForSecondsEnumerator; //cycle around the enemies to spawn and check if it can be spawned for (var i = enemiestoSpawn.Length - 1; i >= 0 && _numberOfEnemyToSpawn > 0; --i) { if (spawningTimes[i] <= 0.0f) { var spawnData = enemiestoSpawn[i]; //In this example every kind of enemy generates the same list of EntityViews //therefore I always use the same EntityDescriptor. However if the //different enemies had to create different EntityViews for different //engines, this would have been a good example where EntityDescriptorHolder //could have been used to exploit the the kind of polymorphism explained //in my articles. var enemyAttackStruct = new EnemyAttackStruct { attackDamage = enemyAttackData[i].enemyAttackData.attackDamage, timeBetweenAttack = enemyAttackData[i].enemyAttackData.timeBetweenAttacks }; //has got a compatible entity previously disabled and can be reused? //Note, pooling make sense only for Entities that use implementors. //A pure struct based entity doesn't need pooling because it never allocates. //to simplify the logic, we use a recycle group for each entity type var fromGroupId = ECSGroups.EnemiesToRecycleGroups + (uint) spawnData.enemySpawnData.targetType; if (entitiesDB.HasAny<EnemyEntityViewStruct>(fromGroupId)) ReuseEnemy(fromGroupId, spawnData); else yield return _enemyFactory.Build(spawnData.enemySpawnData, enemyAttackStruct); spawningTimes[i] = spawnData.enemySpawnData.spawnTime; _numberOfEnemyToSpawn--; } spawningTimes[i] -= SECONDS_BETWEEN_SPAWNS; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 IEnumerator IntervaledTick ( ) { //this is of fundamental importance: Never create implementors as Monobehaviour just to hold //data (especially if read only data). Data should always been retrieved through a service layer //regardless the data source. //The benefits are numerous, including the fact that changing data source would require //only changing the service code. In this simple example I am not using a Service Layer //but you can see the point. //Also note that I am loading the data only once per application run, outside the //main loop. You can always exploit this pattern when you know that the data you need //to use will never change var enemiestoSpawn = ReadEnemySpawningDataServiceRequest ( ) ; var enemyAttackData = ReadEnemyAttackDataServiceRequest ( ) ; var spawningTimes = new float [ enemiestoSpawn . Length ] ; for ( var i = enemiestoSpawn . Length - 1 ; i > = 0 && _numberOfEnemyToSpawn > 0; -- i ) spawningTimes [ i ] = enemiestoSpawn [ i ] . enemySpawnData . spawnTime ; _enemyFactory . Preallocate ( ) ; while ( true ) { //Svelto.Tasks can yield Unity YieldInstructions but this comes with a performance hit //so the fastest solution is always to use custom enumerators. To be honest the hit is minimal //but it's better to not abuse it. yield return _waitForSecondsEnumerator ; //cycle around the enemies to spawn and check if it can be spawned for ( var i = enemiestoSpawn . Length - 1 ; i > = 0 && _numberOfEnemyToSpawn > 0; -- i ) { if ( spawningTimes [ i ] < = 0.0f ) { var spawnData = enemiestoSpawn [ i ] ; //In this example every kind of enemy generates the same list of EntityViews //therefore I always use the same EntityDescriptor. However if the //different enemies had to create different EntityViews for different //engines, this would have been a good example where EntityDescriptorHolder //could have been used to exploit the the kind of polymorphism explained //in my articles. var enemyAttackStruct = new EnemyAttackStruct { attackDamage = enemyAttackData [ i ] . enemyAttackData . attackDamage , timeBetweenAttack = enemyAttackData [ i ] . enemyAttackData . timeBetweenAttacks } ; //has got a compatible entity previously disabled and can be reused? //Note, pooling make sense only for Entities that use implementors. //A pure struct based entity doesn't need pooling because it never allocates. //to simplify the logic, we use a recycle group for each entity type var fromGroupId = ECSGroups . EnemiesToRecycleGroups + ( uint ) spawnData . enemySpawnData . targetType ; if ( entitiesDB . HasAny < EnemyEntityViewStruct > ( fromGroupId ) ) ReuseEnemy ( fromGroupId , spawnData ) ; else yield return _enemyFactory . Build ( spawnData . enemySpawnData , enemyAttackStruct ) ; spawningTimes [ i ] = spawnData . enemySpawnData . spawnTime ; _numberOfEnemyToSpawn -- ; } spawningTimes [ i ] -= SECONDS_BETWEEN_SPAWNS ; } } }

Don’t forget to read all the comments in the example, they help to clarify even more the Svelto.ECS concepts. Due to the simplicity of the example, I am actually not using the BuildEntityInGroup<T> which is instead commonly used in more sophisticated products. In Robocraft every engine that handles the logic of the functional cubes handles the logic of ALL the functional cubes of that specific type in game. However often is needed to know to which vehicle the cubes belong to, so using a group per machine would help to split the cubes of the same type per machine, where the machine ID is the group ID. This allows us to implement fancy things like running one Svelto.Tasks task per machine inside the same engine, which could even run in parallel using multi-threading.

This piece of code highlight one crucial issue, which I may talk more about in the next articles…from the comment (in case you haven’t read it):

Never create implementors as Monobehaviour just to hold data. Data should always been retrieved through a service layer regardless the data source. The benefit are numerous, including the fact that changing data source would require only changing the service code. In this simple example I am not using a Service Layer but you can see the point. Also note that I am loading the data only once per application run, outside the main loop. You can always exploit this trick when you now that the data you need to use will never change.

Initially I was reading the data directly from the monobehaviour like a good lazy coder would have done. This forced me to create an implementor as monobehaviour just to read serialized data. It could be considered OK as long as we don’t want to abstract the data source, however serializing the information into a json file and reading it from a service request is much better than reading this kind of data from an entity component.

Every entity needs an unique ID. This unique ID must be unique regardless the descriptor type and the group it belongs to. I took this decision recently, so if I say otherwise in other articles, please let me know I will fix it.

Communication in Svelto.ECS

One problem of which solution has never been standardized by any ECS implementation is the communication between systems. ECS communication must happen through entity data polling, but in SECS there are some exceptions:

Reactive Engines

These are reactive callback that are called when an entity is added, remove or swapped. The entity is passed by ref and the values can be modified. These interfaces may change with SECS 2.8.

DispatchOnSet/DispatchOnChange

The use of DispatchOnSet<T> and DispatchOnChange<T> Radically changed over the time. Nowadays is used only to implement communication between implementors that abstract underlying platform events and the engine that has the responsibility to handle that specific event. In a pure ECS scenario, without OOP integration, these tools become useless.

The Sequencer

The Sequencer is exclusively used when is absolutely necessary there isn’t any other way to provide an execution order between engines.

Entity Streams

Svelto ECS 2.8 introduces the concept of Entity Stream, please read the related articles to know more about it.

Svelto.ECS and Unity

Svelto.ECS (as well as Svelto.Tasks) is designed to be platform agnostic. However I mainly use it with unity, so Unity extensions are provided. The EntityDescriptorHolder is an example. Using implementors as Monobehaviour let to exploit most of the Unity callbacks, but on other platforms the reasoning could be very similar. All that said, Svelto.ECS gives you the chance to abstract from Unity and you should use Unity classes as less as possible, especially Monobehaviours. It’s also important to keep in mind that the creation of GameObject(s) is uncoupled from the creation of Entities, the only thing they have in common is the fact that gameobject monobehaviours can be implementors. You can see from the example that enemy gameobjects and their ECS entities are built independently.

Logic inside utility classes

You can create static utility classes to share code, that is not a problem as long as the static classes do not hold any state (they must be just a set of static functions)

if you are new to the ECS design and you wonder why it could be useful, you should read my previous articles:

That’s all folks! FEEDBACK ME!

Share this: Facebook

LinkedIn

Twitter

Reddit

Email

