Search Unity

Is it good practice in ECS to frequently add and remove components?

Discussion in 'Data Oriented Technology Stack' started by joshdegrazia, Nov 1, 2018.

  1. joshdegrazia

    joshdegrazia

    Joined:
    Jan 27, 2018
    Posts:
    4
    To me this seems like the best approach for managing anything that is regularly represented by a state machine, and I've also heard about people using this approach for message passing between objects. But I read recently that this might be very memory intensive since changing archetypes of an entity will copy and paste its data somewhere. What's the general consensus on this? What are the alternatives?
     
  2. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,267
    "Copy and paste data" sounds costly but it should actually help you somewhere else. Since the point of copying is to move them together in a new chunk with correct archetype. Later if you want to get all entities with that component it is fast since ECS is based on getting chunks. (This is the tag component pattern)

    If you want to avoid copying you can make a (blittable) boolean field in your existing component instead, but then instead of paying chunk movement cost of adding-removing tag component, you are paying data access cost to check your field + iteration count cost. Since instead of iterating only entities you are interested in immediately you have to iterate through many unrelated ones.

    So it boils down to what you want to achieve with adding and removing that component. Data movement is inevitable to allow fast access later, through ComponentGroup or chunk query.
     
  3. Ryetoast

    Ryetoast

    Joined:
    Mar 8, 2015
    Posts:
    40
    It depends.

    Things that are going to be changed to a value for multiple frames, it's probably good add/remove a component to let jobs filter it out.

    If it's something that's going to be thrashing a lot or just doing something for a frame, it might be better to create an "event entity" or something like that to indicate that the change happened, but not alter the composition.

    If a component does something simple like an add, and could have multiple sources, maybe it's best to just leave an accumulator component on the target that gets cleared whenever it is applied.
     
  4. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    37
    In my experience (albeit I don't have a lot of it) with Unity ECS, adding and removing components is extremely expensive, as well as creating entities, you definitely shouldn't add/remove components in cases when it is likely to happen every frame to lots of entities. In some cases I resorted to having a flag inside a component to determine if it is active or not, instead of adding/removing the component itself
     
    illinar likes this.
  5. joshdegrazia

    joshdegrazia

    Joined:
    Jan 27, 2018
    Posts:
    4
    In my case I'm looking at using it for input handling, I would translate a button press into an action (eg. the A button would do the jump action), and each action would result in adding or removing components based on how I want the action to change the behaviour of my player entity (or any other entities listening for that action). So it wouldn't be multiple archetype changes per frame, it would be every few frames at most since each action would likely involve a cooldown before the button could be pressed again.
     
  6. Ryetoast

    Ryetoast

    Joined:
    Mar 8, 2015
    Posts:
    40
    I simplified some component names and such, but:

    For jump input, I just create an entity with basically an "InputJump" component, is checked as an early out in the InputJump system. If an InputJump exists this frame, all entities with the InputTarget and OnGround tags to have their y velocity increased by their JumpSpeed value.

    So the short version is that I don't change the composition upon the player pressing input, since this is going to be true for only a single frame and would just thrash back and forth on the composition, but I change the composition when the player leaves the ground, since I'm going to want to filter the player differently for many frames while they are airborne to handle their movement differently.
     
  7. nantoaqui

    nantoaqui

    Joined:
    Oct 1, 2014
    Posts:
    20
    How would you create a system to handle inputs like this one?
    https://github.com/sschmid/Entitas-Shmup/blob/master/Assets/Sources/Features/Input/InputSystem.cs

    I'm not able to translate this:
    Code (CSharp):
    1.        _pools.input.CreateEntity()
    2.                  .AddMoveInput(new Vector3(moveX, moveY))
    3.                  .AddInputOwner(PLAYER1_ID);
    Into this:
    Code (CSharp):
    1.                 if (movePressed) {
    2.                     var playerArchetype = new EntityArchetype ();
    3.                     var inputEntity = Commands.CreateEntity (index, playerArchetype);
    4.                     Commands.AddComponent (index, inputEntity, new MoveInput { Axis = axis });
    5.                     Commands.AddComponent (index, inputEntity, entity); // The type 'Unity.Entities.Entity' cannot be used as type parameter 'T' in the generic type
    6.                 }
    Also, how would clean up those short live entities? The System State Component just don't get in my head.

    Thanks
     
  8. jdtec

    jdtec

    Joined:
    Oct 25, 2017
    Posts:
    89
    Code (CSharp):
    1. Commands.AddComponent (index, inputEntity, entity);
    You're trying to add an entity where it's asking for a component. I add a playerId component myself but if you want a reference to a player entity then you could make a component with an entity in it.

    Something like this...

    Code (CSharp):
    1. public struct PlayerRef : IComponentData
    2.   {
    3.     public Entity m_playerEntity;
    4.   }
    I create short lived entities for events such as UI inputs and other things. In this case the flow is:

    UIView creates event entity with view input components
    UIInputProcessor queries for these view input components
    UIInputProcessor Update:
    processes query data (create a game input command from controller input / create a new unit / move a player)
    destroys the entities
     
  9. kleicht

    kleicht

    Joined:
    Aug 1, 2018
    Posts:
    12
    If inputEntity has to be processed by multiple systems I tag them with Destroy component.
    Then I have a DestroyEntitiesSystem that will query all entities with Destroy component and deletes them.
     
  10. nantoaqui

    nantoaqui

    Joined:
    Oct 1, 2014
    Posts:
    20
    Thanks a lot! I will try to add a "Player ID" and iterate over it. Also I tried to create the "DestroyEntitiesSystem" and it is working but doesn't feel right tough.

    Here's the Input Job:

    Code (CSharp):
    1.         struct PlayerInputJob : IJobForEachWithEntity<Player> {
    2.             public EntityCommandBuffer.Concurrent Commands;
    3.  
    4.             public float2 axis;
    5.             public bool movePressed;
    6.             public bool actionPressed;
    7.  
    8.             public void Execute (Entity entity, int index, [ReadOnly] ref Player player) {
    9.                 if (movePressed) {
    10.                     Commands.AddComponent (index, entity, new MoveInput { Axis = axis });
    11.                 }
    12.  
    13.                 if (actionPressed) {
    14.                     Commands.AddComponent (index, entity, new ActionInput ());
    15.                 }
    16.             }
    17.         }
    Input Cleanup system:

    Code (CSharp):
    1.    [UpdateBefore (typeof (SimulationSystemGroup))]
    2.     public class CleanupActionInputSystem : JobComponentSystem {
    3.         struct PlayerInputJob : IJobForEachWithEntity<ActionInput> {
    4.             public EntityCommandBuffer.Concurrent Commands;
    5.  
    6.             public void Execute (Entity entity, int index, [ReadOnly] ref ActionInput player) {
    7.                 Commands.RemoveComponent (index, entity, typeof (ActionInput));
    8.             }
    9.         }
    10.  
    11.         private EndSimulationEntityCommandBufferSystem endSimEcbSystem;
    12.  
    13.         protected override void OnCreateManager () {
    14.             this.endSimEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem> ();
    15.         }
    16.  
    17.         protected override JobHandle OnUpdate (JobHandle inputDependencies) {
    18.             var job = new PlayerInputJob () {
    19.                 Commands = this.endSimEcbSystem.CreateCommandBuffer ().ToConcurrent ()
    20.             };
    21.  
    22.             var jobHandle = job.Schedule (this, inputDependencies);
    23.  
    24.             this.endSimEcbSystem.AddJobHandleForProducer (jobHandle);
    25.  
    26.             return jobHandle;
    27.         }
    28.     }
    Code (CSharp):
    1.     [UpdateBefore (typeof (SimulationSystemGroup))]
    2.     public class CleanupMoveInputSystem : JobComponentSystem {
    3.         struct PlayerInputJob : IJobForEachWithEntity<MoveInput> {
    4.             public EntityCommandBuffer.Concurrent Commands;
    5.  
    6.             public void Execute (Entity entity, int index, [ReadOnly] ref MoveInput player) {
    7.                 Commands.RemoveComponent (index, entity, typeof (MoveInput));
    8.             }
    9.         }
    10.  
    11.         private EndSimulationEntityCommandBufferSystem endSimEcbSystem;
    12.  
    13.         protected override void OnCreateManager () {
    14.             this.endSimEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem> ();
    15.         }
    16.  
    17.         protected override JobHandle OnUpdate (JobHandle inputDependencies) {
    18.             var job = new PlayerInputJob () {
    19.                 Commands = this.endSimEcbSystem.CreateCommandBuffer ().ToConcurrent ()
    20.             };
    21.  
    22.             var jobHandle = job.Schedule (this, inputDependencies);
    23.  
    24.             this.endSimEcbSystem.AddJobHandleForProducer (jobHandle);
    25.  
    26.             return jobHandle;
    27.         }
    28.     }
    It has a lot of duplicated code and boilerplate.

    How can I improve this?

    Thanks!
     
  11. kleicht

    kleicht

    Joined:
    Aug 1, 2018
    Posts:
    12
    Currently you are tagging the player entity itself with the input component. If you want to do this approach then you have to destroy them manually as you did, when you're finished processing them.
    However a different approach would be ( as @jdtec mentioned it ) the following:
    Create a new entity and add MovementInput component to it. The trick is that the MovementInput component will get a reference to an entity on which it should apply the input.
    So it should look like this:

    Component Setup part:

    Code (CSharp):
    1. public struct MovementInput : IComponentData
    2. {
    3.     public Entity TargetEntity;
    4.     public float2 Axis;
    5. }
    6.  
    7. public struct Destroy : IComponentData {} // This is for the DestroySystemPart
    8.    
    Then in the PlayerInputJobSystem:

    Code (CSharp):
    1. public class PlayerInputJobSystem
    2. {
    3.  
    4. ...
    5.  
    6.     private EntityArchetype movementInputArchetype;
    7.  
    8.     public void OnCreate()
    9.     {
    10.         movementInputArchetype = EntityManager.CreateArchetype(ComponentType.ReadWrite<MoveInput>(), ComponentType.ReadWrite<Destroy>()); // It will auto tag the entity with destroy flag
    11.     }
    12.  
    13.     struct PlayerInputJob : IJobForEachWithEntity<Player> {
    14.         public EntityCommandBuffer.Concurrent Commands;
    15.         public EntityArchetype movementInputArchetype;
    16.  
    17.         public float2 axis;
    18.         public bool movePressed;
    19.         public bool actionPressed;
    20.  
    21.         public void Execute (Entity entity, int index, [ReadOnly] ref Player player) {
    22.             if (movePressed) {
    23.                 var movementInputEntity = Commands.CreateEntity (index, movementInputArchetype);
    24.                 Commands.SetComponent (index, movementInputEntity, new MoveInput { TargetEntity = entity, Axis = axis });
    25.             }
    26.             ...
    27.         }
    28.     }
    29.  
    30.     ...
    31.  
    32. }
    Notice that this way when we create the entity with the archetype it will be automatically tagged with Destroy component.

    You should also have a DestroyEntitySystem which will clean up these events:

    Code (CSharp):
    1. public class DestroyEntitySystem : ComponentSystem
    2. {
    3.     private EntityQuery query;
    4.     private EntityManager em;
    5.  
    6.     protected override void OnCreate()
    7.     {
    8.         base.OnCreate();
    9.  
    10.         query = GetEntityQuery(ComponentType.ReadOnly<Destroy>());
    11.         em = EntityManager;
    12.     }
    13.  
    14.     protected override void OnUpdate()
    15.     {
    16.         em.DestroyEntity(query);
    17.     }
    18.  
    19. }
    20.  
    This way every entity that you create with Destroy flag on it will be automatically destroyed.
    You just need to be carefull with the system ordering.

    For my specific game I went with the following setup:

    SystemUpdateOrder:
    • DestroyEntitySystem
    • ....
    • InputCreationSystems (like PlayerInputJobSystem)
    • ....
    • InputProcessingSystems (like MoveEntitiesSystem that will process the MoveInput components)

    This way I know that every input that I create will be processed before the Destroy System deletes them. Your project might need a different system order setting, but you get the idea.
     
    nantoaqui and leni8ec like this.
  12. pakfront

    pakfront

    Joined:
    Oct 6, 2010
    Posts:
    551
    nantoaqui likes this.
  13. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    609
    I did a lot of benchmark for adding/removing components vs polling and flagging them. Creation is so much slower that I avoid it wherever it is not very problematic. So my input system is as simple as this:

    Code (CSharp):
    1. [UpdateInGroup(typeof(InputSystemGroup))]
    2. public class InputSystem : ComponentSystem
    3. {
    4.     protected override void OnUpdate()
    5.     {
    6.         Entities.ForEach((ref PrimaryDirectionalInput i) =>
    7.         {
    8.             i.Forward.State = Input.GetKey(KeyCode.W);
    9.             i.Backward.State = Input.GetKey(KeyCode.S);
    10.             i.Left.State = Input.GetKey(KeyCode.A);
    11.             i.Right.State = Input.GetKey(KeyCode.D);
    12.         });
    13.         Entities.ForEach((ref ZoomInput i) =>
    14.         {
    15.             i.Delta = (int)Input.mouseScrollDelta.y;
    16.         });
    17.         Entities.ForEach((ref SelectInput i) =>
    18.         {
    19.             i.Value.State = Input.GetMouseButton(0);
    20.         });
    21.         Entities.ForEach((ref PointerScreenPosition p) =>
    22.         {
    23.             p.Value = new float2(Input.mousePosition.x, Input.mousePosition.y);
    24.         });
    25.     }
    26. }
    My input events are singletons that any system can easily get. There can be
    bool Consumed
    flag inside or a separate singleton that says who is supposed to currently receive the input or the recipients are enabled and disabled when needed.
     
    nantoaqui likes this.
  14. nantoaqui

    nantoaqui

    Joined:
    Oct 1, 2014
    Posts:
    20
    illinar how would you extend this for a multiplayer setup?

    The reason that i'm asking this is to learn how to handle this case mentioned by kleicht:

    Code (CSharp):
    1. public struct MovementInput : IComponentData
    2. {
    3.     public Entity TargetEntity;
    4.     public float2 Axis;
    5. }
    6.  
    How do you ensure that "TargetEntity" is a valid one? Also is not possible to query by the TargetEntity.
     
  15. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    609
    Sorry about going off topic.

    @nantoaqui With TargetEntity movement system would iterate through each PlayerMovement, and through each MovementInput instance and if the entity matches it would respond. BUT In this case might be better to have MovementInput and PlayerMovement on the same entity as you did initially, except don't remove it. Approach depends on many details.

    But it is usually easy and usually more performant to have any events as constantly existing component with bool flags as was mentioned by others. Not as clean, but not too bad and easier in some ways.
     
    nantoaqui likes this.
  16. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    7,135
    What about entities that are created in stages or evolver are enhanced/accumulate data over time?

    I can imagine entities that are created in stages or exist in LODs or become enhanced or gain data over time.
     
  17. illinar

    illinar

    Joined:
    Apr 6, 2011
    Posts:
    609
    @Arowx it kinda implies that those changes are very gradual an so it might not impact performance. And LODs imply versions of an object which could be done with tagging as Disabled (special case tag) one version and untagging another. Actually even hundreds or thousands of tags or new entities per frame are potantially not a big deal, it all depends. But setting a component via ref or ComponentDataFromEntity is like thousands times faster than adding new one as far as I recall.
     
    Last edited: May 18, 2019
    leni8ec and GliderGuy like this.