Translation wiki project Svelto.ECS. ECS framework for Unity3D


Hi, Habr! I present to you the translation of the Svelto.ECS wiki project, written by Sebastiano Mandalà.

Svelto.ECS is the result of many years of research and application of the SOLID principles in the development of Unity games. This is one of the many implementations of the ECS pattern, available for C # with various unique functions introduced to eliminate the flaws of the pattern itself.

First look


The easiest way to see the main features of Svelto.ECS is to download the Vanilla Example . If you want to make sure that it is easy to use, I'll show you an example:

//  void ApplicationCompositionRoot() { var simpleSubmissionEntityViewScheduler = new SimpleSubmissionEntityViewScheduler(); _enginesRoot = new EnginesRoot(simpleSubmissionEntityViewScheduler); var entityFactory = _enginesRoot.GenerateEntityFactory(); var entityFunctions = _enginesRoot.GenerateEntityFunctions(); _enginesRoot.AddEngine(new BehaviourForSimpleEntityEngine(entityFunctions)); entityFactory.BuildEntity<SimpleEntityDescriptor>(new EGID(1), new[] { new SimpleImplementor() });` } //  class SimpleEntityDescriptor : GenericEntityDescriptor<BehaviourEntityViewForSimpleEntity> { } public class BehaviourEntityViewForSimpleEntity : EntityView { public ISimpleComponent simpleComponent; } public interface ISimpleComponent { public int counter {get; set;} } class SimpleImplementor : ISimpleComponent { public int counter { get; set; } } //  ()    public class BehaviourForSimpleEntityAsStructEngine : IQueryingEntityViewEngine { public IEntityViewsDB entityViewsDB { private get; set; } public void Ready() { Update().Run(); } //   . //    N ,  N    0  1. IEnumerator Update() { Console.Log("Task Waiting"); while (true) { var entityViews = entityViewsDB .QueryGroupedEntityViews<BehaviourEntityViewForSimpleEntity>(0); if (entityViews.Length> 0) { for (var i = 0; i < entityViews.Length; i++) AddOne(entityViews[i].counter); Console.Log("Task Done"); yield break; } yield return null; } } static void AddOne(int counter) { counter += 1; } } 

Unfortunately, it is not possible to quickly understand the theory behind this code, which may look simple, but at the same time confusing. To understand, you will need to spend time reading the “wall of text” and try out the examples given.

Introduction


Recently I have been discussing Svelto.ECS a lot with a few more or less experienced programmers. I collected a lot of reviews and made a lot of notes that I will use as a starting point for my next articles, where I will talk more about theory and good practices. A small spoiler: I realized that when you start using Svelto.ECS, the biggest obstacle is the change of programming paradigm . It's amazing how much I have to write in order to explain the new concepts presented by Svelto.ECS, compared with the small amount of code written for developing the framework. In fact, while the framework itself is very simple and lightweight, the transition from OOP with the active use of inheritance or the usual Unity components to a “new” modular and loosely coupled design that Svelto.ECS proposes to use prevents people from adapting to the framework.

Svelto.ECS is actively used in Freejam (a comment of the translator - the Author is the technical director in this company). Since I can always explain to colleagues the basic concepts of the framework, it takes less time for them to understand how to work with it. Although Svelto.ECS is as tough as possible, bad habits are difficult to overcome, so users tend to abuse some flexibility to adapt the framework to the “old” paradigms with which they are comfortable. This can lead to a catastrophe due to misunderstanding or distortion of the concepts underlying the logic of the framework. That is why I intend to write as many articles as possible, especially since I am sure that the ECS paradigm is the best solution at the moment for writing efficient and supported code for large projects that change and are altered many times over several years. Robocraft and Cardlife are proof of that.

I am not going to talk much about the theories underlying this article. I only remind you why I refused to use the IoC container and started using the ECS framework exclusively: IoC container is a very dangerous tool if it is used without understanding the very essence of control inversion. As you could see from my previous articles, I distinguish between Inversion of Creation Control and Inversion of Flow Control. Inversion flow control is like the principle of Hollywood: "Do not call us, we will call you." This means that embedded dependencies should never be used directly through public methods, since you simply use an IoC container as a replacement for any other form of global injection, for example, singletons. However, if the IoC container is used in accordance with the Inversion of Control (IoC) principle, then basically everything comes down to the repeated use of the “Template Method” pattern for implementing managers used only for registering objects that they manage. In the real context of flow control inversion, managers are always responsible for managing entities. Does this look like an ECS pattern? Of course. Based on this reasoning, I took the ECS pattern and developed a rigid framework based on it, and its use is tantamount to applying a new programming paradigm.

Composition Root and EnginesRoot


The Main class is the Composition Root of the application. The root of the composition is the place where dependencies are created and implemented (I told a lot about this in my articles). The composition root belongs to the context, but the context can have more than one composition root. For example, a Factory is the root of a composition. An application may have more than one context, but this is an advanced scenario, and in this example we will not consider it.

Before diving into the code, let's get acquainted with the first rules of the Svelto.ECS language. ECS is an abbreviation for Entity Component System (Entity Component System). The ECS infrastructure has been well analyzed in articles by many authors, but while the basic concepts are common, the implementations vary greatly. First of all, there is no standard way to solve some problems that arise when using ECS-oriented code. It is with regard to this issue that I make most of my efforts, but I will tell about this later or in the following articles. The theory is based on the concepts of Entity, Components (entities) and Systems. Although I understand why the word System was historically used, I didn’t think it was intuitive enough for this purpose from the very beginning, so I used the Engine as a synonym for System, and you, depending on your preferences, can use one of these terms.

The EnginesRoot class is the core of Svelto.ECS. With it, you can register engines and design all the essences of the game. Creating engines dynamically does not make much sense, so they should all be added to an instance of EnginesRoot from the same composition root where it was created. For similar reasons, an instance of EnginesRoot should never be embedded, and engines should not be deleted after they have been added.

To create and implement dependencies, we need at least one root composition. Yes, in one application there may well be more than one EnginesRoot, but we will not touch on this in the current article, which I try to simplify as much as possible. Here is the composition root with engine creation and dependency injection:

 void SetupEnginesAndEntities() { //Engines Root   Svelto.ECS.      EngineRoot // ,  Composition Root     ,   //   . //UnitySumbmissionEntityViewScheduler -  ,   //EnginesRoot,     EntityViews. //    ,   , //         Unity. _enginesRoot = new EnginesRoot(new UnitySumbmissionEntityViewScheduler()); //Engines root      ,   , //   . //   EntityFactory  EntityFunctions. //EntityFactory      //(    ), //   . _entityFactory = _enginesRoot.GenerateEntityFactory(); // EntityFunctions     //   , //  .        var entityFunctions = _enginesRoot.GenerateEntityFunctions(); //GameObjectFactory   Unity GameObject //   // GameObject.Instantiate.      // ,    ,   //       //(  ,   //         //      -  ) GameObjectFactory factory = new GameObjectFactory(); //    3     Svelto.ECS. //        //  . //         //     . var enemyKilledObservable = new EnemyKilledObservable(); var scoreOnEnemyKilledObserver = new ScoreOnEnemyKilledObserver(enemyKilledObservable); //ISequencer   3     Svelto.ECS // .       : //1)       //(   //,   ,     //  ). //2)   ,     // . ISequencer      //  Sequencer playerDamageSequence = new Sequencer(); Sequencer enemyDamageSequence = new Sequencer(); //    Unity. //     . IRayCaster rayCaster = new RayCaster(); ITime time = new Others.Time(); // .         //  . var playerHealthEngine = new HealthEngine(entityFunctions, playerDamageSequence); var playerShootingEngine = new PlayerGunShootingEngine(enemyKilledObservable, enemyDamageSequence, rayCaster, time); var playerMovementEngine = new PlayerMovementEngine(rayCaster, time); var playerAnimationEngine = new PlayerAnimationEngine(); //  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); //    var hudEngine = new HUDEngine(time); var damageSoundEngine = new DamageSoundEngine(); // Sequencer  ,    // ,     . playerDamageSequence.SetSequence( new Steps // ,  ! { { //  //      Next   enemyAttackEngine, new To //        { //      //   Next playerHealthEngine, } }, { //  playerHealthEngine, //      Next   new To //       { //      Next     //DamageCondition.damage { DamageCondition.damage, new IStep[] { hudEngine, damageSoundEngine } }, //      Next     //DamageCondition.dead { DamageCondition.dead, new IStep[] { hudEngine, damageSoundEngine, playerMovementEngine, playerAnimationEngine, enemyAnimationEngine } }, } } }); 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 } }, } } }); // ,     //  _enginesRoot.AddEngine(playerMovementEngine); _enginesRoot.AddEngine(playerAnimationEngine); _enginesRoot.AddEngine(playerShootingEngine); _enginesRoot.AddEngine(playerHealthEngine); _enginesRoot.AddEngine(new PlayerInputEngine()); _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine()); //  _enginesRoot.AddEngine(enemySpawnerEngine); _enginesRoot.AddEngine(enemyAttackEngine); _enginesRoot.AddEngine(enemyMovementEngine); _enginesRoot.AddEngine(enemyAnimationEngine); _enginesRoot.AddEngine(enemyHealthEngine); //  _enginesRoot.AddEngine(new CameraFollowTargetEngine(time)); _enginesRoot.AddEngine(damageSoundEngine); _enginesRoot.AddEngine(hudEngine); _enginesRoot.AddEngine(new ScoreEngine(scoreOnEnemyKilledObserver)); 

This code is from the example of Survival, which is now commented and corresponds to almost all the rules of good practice that I propose to apply, including the use of platform-independent and testable logic of engines. Comments will help you understand most of them, but a project of this size can be difficult to understand if you are new to Svelto.

Entities


The first step after creating an empty composition root and an instance of the EnginesRoot class is to identify the objects you want to work with first. It is logical to start with Entity Player. The essence of Svelto.ECS should not be confused with the Unity Game Object (GameObject). If you read other articles related to ECS, you could see that in many of them entities are often described as indices. This is probably the worst way to introduce the concept of ECS. Although this is true for Svelto.ECS, it is hidden in it. I want the user Svelto.ECS to represent, describe and identify each entity from the point of view of the Game Design Domain language. The entity in the code must be the object described in the design document of the game. Any other form of definition of an entity will lead to an artificial way of adapting your old ideas to the principles of Svelto.ECS. Follow this basic rule and you will not go wrong. An entity class itself does not exist in the code, but you still need to define it not abstractly.

Engines


The next step is to think about what behavior to set the Entities. Each behavior is always modeled inside the engine, you can not add logic to any other classes within the Svelto.ECS application. We can start with the movement of the player’s character and define the PlayerMovementEngine class. The name of the engine should be very narrowly focused, because the more specific it is, the more likely it is that the engine will follow the Single Responsibility Rule. Proper class naming in Svelto.ECS is of fundamental importance. And the goal is not only to clearly show your intentions, but also to help you “see” them yourself.

For the same reason, it is important that your engine be in a very specialized namespace. If you define namespaces according to the folder structure, adapt to the concepts of Svelto.ECS. Using specific namespaces helps detect design errors when entities are used inside incompatible namespaces. For example, it is not supposed that any object-enemy will be used inside the player's namespace, if the goal is not to violate the rules related to modularity and weakly related objects. The idea is that objects of a specific namespace can be used only within itself or the parent namespace. Using Svelto.ECS is much more difficult to turn your code into spaghetti, where dependencies are injected right and left, and this rule will help you raise the bar of code quality even higher when dependencies are properly abstracted between classes.

In Svelto.ECS, abstraction is extended to several frontiers, but ECS essentially facilitates the abstraction of data from the logic that must process the data. Entities are determined by their data, not their behavior. Such engines are a place where you can place the joint behavior of identical entities so that the engines can always work with a set of entities.

Svelto.ECS and the ECS paradigm allow the encoder to achieve one of the holy grails of pure programming, which is the perfect encapsulation of logic. Engines must not have public functions. The only public functions that must exist are those that are necessary to implement the framework interfaces. This leads to the forgetting of dependency injection and helps to avoid bad code that occurs when using dependency injection without inversion of control. The engines should NEVER be introduced into any other engine or any other type of class. If you think you want to implement the engine, you simply make a fundamental error in the design of the code.

Compared to Unity MonoBehaviours, the engines already show the first huge advantage, which is the ability to access all states of entities of a given type from the same code area. This means that the code can easily use the state of all objects directly from the same place where the common object logic will be executed. In addition, individual engines can process the same objects so that the engine can change the state of the object, while the other engine can read it, effectively using two engines for communication through the same entity data. An example can be seen by looking at the PlayerGunShootingEngine and PlayerGunShootingFxsEngine engines . In this case, the two engines are in the same namespace, so they can share the same entity data. PlayerGunShootingEngine determines whether the player (enemy) has been damaged, and writes the lastTargetPosition value of the IGunAttributesComponent component (which is the PlayerGunEntity component). PlayerGunShootFxsEngine processes the graphic effects of the weapon and reads the position of the target selected by the player. This is an example of interaction between the engines through data polling (data polling). Later in this article, I will show how to allow the mechanism to communicate between them by pushing data (Data pushing) or data binding (Data binding) . Logically, the engines should never keep state.

Engines do not need to know how to interact with other engines. External communication occurs through abstraction, and Svelto.ECS solves the connection between the engines in three different official ways, but I will tell about this later. The best engines are those that do not require any external communications. These engines reflect well-encapsulated behavior and usually work through a logical cycle. Loops are always modeled using Svelto.Task tasks within Svelto.ECS applications. Since the player’s movement must be updated every physical tick, it would be natural to create a task to be performed in every physical update. Svelto.Tasks allows you to run each type of IEnumerator on several types of schedulers. In this case, we decided to create a task on PhysicScheduler , which allows you to update the player's position:

 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() { // ,      //  EnginesRoot    . // ,         . var _playerEntityViews = entityViewsDB.QueryEntityViews<PlayerEntityView>(); var playerEntityView = _playerEntityViews[0]; while (true) { Movement(playerEntityView); Turning(playerEntityView); //   yield,     ! yield return null; } } 

Svelto.Tasks tasks can be performed directly or via ITaskRoutine objects. I will not talk much about Svelto.Tasks here, since I wrote other articles for him. The reason why I decided to use the task routine instead of running the IEnumerator implementation directly is quite discretionary. I wanted to show that you can start a cycle when a player object is added to the engine and stop it when it is deleted. However, you need to know when the object is added and deleted.

Svelto.ECS , , . Svelto.ECS, . , , , . , .

, SingleEntityViewEngine , MultiEntitiesViewEngine <EntityView1, ..., EntityViewN> . - , , .

IQueryingEntityViewEngine . . , - , , , , , , - . , , . , , . EnemyMovementEngine There is a very general approach to how to request objects:

 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; } } 

In this case, the main engine loop runs directly on the predefined scheduler. Tick ​​(). Run ()Shows the shortest way to launch IEnumerator with Svelto.Tasks. IEnumerator will continue to yield to the next frame until at least one Enemy target is found. Since we know that there will always be only one goal (another bad assumption), I choose the first one available. While the goal of Enemy Target can be only one (although it could be more!), There are many Enemies, and the engine still takes care of the logic of movement for everyone. In this case, I cheated because I actually use the Unity Nav Mesh System, so all I need to do is simply set a destination in NavMesh. Honestly, I have never used the Unity NavMesh code, so I'm not even sure how it works, this code is simply inherited from the original Survival demonstration.

, Navmesh Unity. , , . , navMeshDestination Unity Nav Mesh.

, , , , . , , - , , .


Prior to this, we introduced the concept of the Engine and the abstract definition of the Entity, let's now define what the Representation of the Entity is. I have to admit that of the 5 concepts on which Svelto.ECS is built, Entity Representations are probably the most confusing. Previously called Node (the name taken from the ECS framework Ash ), I realized that the name “Node” meant nothing. EntityView can also be misleading, as programmers usually associate views with a concept coming from the Model Model View Controller (Model View Controller), Svelto.ECS View, EntityView — , . , , EntityMap, EntityView , . Svelto.ECS :



, . EntityViews. EntityViews, EntityViews. , Player, , PlayerEntityView . , , . EntityView . , ( . .), PlayerPhysicEngine PlayerPhysicEntityView , PlayerGraphicEngine PlayerGraphicEntityView PlayerAnimationEngine PlayerAnimationEntityView . , PlayerPhysicMovementEngine PlayerPhysicJumpEngine ( . .).

Components


, , , , . , EntityView — , (public) . , , :

, — . , Svelto.ECS. . . Interface Segregation Principle”, writing small interfaces of components, even with one property each, you will notice that you have begun to reuse the interfaces of components inside different entities. In our example, ITransformComponent is reused in many representations of the entity. Using components as interfaces also allows them to implement the same objects, which in many cases makes it possible to simplify the connection between entities that see the same entity through different representations of entities (or the same, if possible).

Therefore, in Svelto.ECS the entity component is always an interface, and this interface is used only through the EntityView field inside the engine. The entity component interface is then implemented by the so-called«». , .

, . , ref, . , (data oriented), , . , ( !) . , , — , Unity. , Survival, , , Unity.


, . , , . , , EntityView — , . , , . , ECS. , . — , , . , , — . EntityDescriptor , . Player PlayerEntityDescriptor . , , , - , , BuildEntity<PlayerEntityDescriptor>() , .

, EntityDescriptor, — EntityViews!!! EntityViews , , , .

PlayerEntityDescriptor :

 using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } } 

( ) , . PlayerEntityDescriptor EntityViews PlayerEntity.

EntityDescriptorHolder


EntityDescriptorHolder Unity . , Unity GameObject. , . , Robocraft , . . , GameObject MonoBehaviour's. , EntityDescriptorHolders , Svelto.ECS, . , :

 void BuildEntitiesFromScene(UnityContext contextHolder) { //EntityDescriptorHolder -    Svelto.ECS , //       . //         . //      , //    //     IEntityDescriptorHolder[] entities = contextHolder.GetComponentsInChildren<IEntityDescriptorHolder>(); //     Svelto.ECS, ,   //      . //        . //    EntityDescriptorHolder, //    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>()); } } 

, , BuildEntity . . MonoBehaviour GameObject. . , , . , , MonoBehaviours , !


, Svelto.ECS, . , , C# . , , «». :


 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; } 

, , . , .


, , EnginesRoot , , , . . (Entity Factory), EnginesRoot GenerateEntityFactory . EnginesRoot IEntityFactory . , IEntityFactory .

IEntityFactory . PreallocateEntitySlots BuildMetaEntity , BuildEntity BuildEntityInGroup .

BuildEntityInGroup , Survival , , BuildEntity :

 IEnumerator IntervaledTick() { //  :       //MonoBehaviour    . //       //   . // ,     , //         . //        ,     . //  ,        //   , //  .      , // ,   ,   . var enemiestoSpawn = ReadEnemySpawningDataServiceRequest(); while (true) { //Svelto.Tasks    yield  Unity, //    . //       . // ,  , //    . yield return _waitForSecondsEnumerator; if (enemiestoSpawn != null) { for (int i = enemiestoSpawn.Length - 1; i >= 0 && _numberOfEnemyToSpawn > 0; --i) { var spawnData = enemiestoSpawn[i]; if (spawnData.timeLeft <= 0.0f) { //          int spawnPointIndex = Random.Range(0, spawnData.spawnPoints.Length); //       . var go = _gameobjectFactory.Build(spawnData.enemyPrefab); //        MonoBehaviour. //      . var data = go.GetComponent<EnemyAttackDataHolder>(); //     MonoBehaviour   // : List<IImplementor> implementors = new List<IImplementor>(); go.GetComponentsInChildren(implementors); implementors.Add(new EnemyAttackImplementor(data.timeBetweenAttacks, data.attackDamage)); //         EntityViews, //     EntityDescriptor. //,       EntityView //  ,     ,  EntityDescriptorHolder //       , //    . _entityFactory.BuildEntity<EnemyEntityDescriptor>( go.GetInstanceID(), implementors.ToArray()); var transform = go.transform; var spawnInfo = spawnData.spawnPoints[spawnPointIndex]; transform.position = spawnInfo.position; transform.rotation = spawnInfo.rotation; spawnData.timeLeft = spawnData.spawnTime; numberOfEnemyToSpawn--; } spawnData.timeLeft -= 1.0f; } } } } 

Do not forget to read all the comments in this example, they will help to better understand the concepts of Svelto.ECS. Because of the simplicity of the example, I do not use the BuildEntityInGroup , which is used in more complex projects. In Robocraft, each engine that processes the logic of functional cubes processes the logic of ALL functional cubes of this particular type in the game. However, it is often necessary to know which vehicle the cubes belong to, so using a group for each car will help to break the cubes of the same type into cars, where the car ID is the group ID. This allows us to implement cool things, such as running one Svelto.Tasks task on a machine inside the same engine, which can work in parallel using multi-threading.

, , , … ( ):

MonoBehaviour . . , , . , . , , . , , , .

MonoBehaviour, . MonoBehaviour . , , json- , .

Svelto.ECS


, ECS, — . , , Svelto.ECS . — / , .

DispatchOnSet / DispatchOnChange


, (Data polling). DispatchOnSet DispatchOnChange ( ), , T . , (Push) , , . , , , , . DispatchOnSet DispatchOnChangecannot be launched without changing the data, it allows to consider them as a data binding mechanism instead of a normal event. There is also no trigger function to call, instead the data value held by these classes must be set or changed. There are no large examples in the Survival code, but you can see how the targetHit boolean field from IGunHitTargetComponent works . The difference between DispatchOnSet and DispatchOnChange is that the latter triggers the event only when the data is actually changing, and the first is always.

Sequencer


Ideal Engines are fully encapsulated, and you can write the logic of this engine as a sequence of instructions using Svelto.Tasks and pernetworks (IEnumerators). However, this is not always possible, since in some cases engines should send events to other engines. This is usually done through the Entity data, especially using DispatchOnSet and DispatchOnChange , , , “” , . , , , , . , ! . IEnumerator Svelto Tasks «» «» .

/


, Svelto.ECS Svelto.ECS. , , , Svelto.ECS, , , , .

Source: https://habr.com/ru/post/413107/


All Articles