Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Logic clash between PostUpdateCommands and EntityManager.HasComponent

Discussion in 'Entity Component System' started by NotaNaN, Feb 19, 2019.

  1. NotaNaN

    NotaNaN

    Joined:
    Dec 14, 2018
    Posts:
    324
    I'm having a dilemma where I'm using the EntityManager to check if an entity has a component -- but due to PostUpdateCommands not adding the component until after update -- on the following iteration the EntityManager.HasComponent cannot detect the component as it technically hasn't been added yet. Thus, giving my code the all-clear to add another one... Which isn't good... To make matters worse the addition of the component happens on another entity. So that spices things up a bit...

    So this is my question: How do you elegantly get around this problem? Is there some sneaky PostUpdateCommands.HasComponent that I'm missing or another way to check the PostUpdateCommands "Action query"? I would really like to stay away from "clunky" solutions... But if clunky solutions are the only solutions, what's the least clunky one? :oops:

    Also, here's a pretty massive code block (apologies in advance); and for anyone wondering about my use-case, the gist of it is that I'm making a little trigger system to deal damage, knockback, recoil, and the like, to entities within my game:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Unity.Entities;
    5. using Unity.Collections;
    6. using Unity.Transforms;
    7.  
    8. /// <summary>
    9. /// DamageBoxBuffer's are damaging "colliders" that can be attached to an entity.
    10. /// </summary>
    11. public struct DamageBoxBuffer : IBufferElementData
    12. {
    13.     /// <summary>
    14.     /// When true, this collider does not get destroyed when it successfully makes a collision.
    15.     /// </summary>
    16.     public byte IsPersistent;
    17.  
    18.     /// <summary>
    19.     /// When true, the Position variable uses relative coordinates instead of world coordinates
    20.     /// </summary>
    21.     public byte IsRelative;
    22.  
    23.     public Vector3 Position;
    24.     public Vector3 Size;
    25.     public Vector3 Orientation;
    26.     /// <summary>
    27.     /// The distance the box will travel, essentially elongation to the size.
    28.     /// </summary>
    29.     public float Distance;
    30.  
    31.     public float Damage;
    32.     /// <summary>
    33.     /// If True, the collidee is knocked back in the direction of the KnockbackDirection variable.
    34.     /// If false, the collidee is knocked back in relation to the location of the center of the collider.
    35.     /// </summary>
    36.     public byte SetKnockbackDirection;
    37.     public float KnockbackStrength;
    38.     public Vector2 KnockbackDirection;
    39.  
    40.     public float RecoilDamage;
    41.     /// <summary>
    42.     /// If True, the collider is knocked back in the direction of the RecoilDirection variable.
    43.     /// If false, the collider is knocked back in relation to the location of the center of the collider.
    44.     /// </summary>
    45.     public byte SetRecoilDirection;
    46.     public float RecoilStrength;
    47.     public Vector2 RecoilDirection;
    48.  
    49.     /// <summary>
    50.     /// The amount of Invulnerability frames the collidee has when a collision occurs.
    51.     /// </summary>
    52.     public float InvulnerabilityFrames;
    53. }
    54.  
    55. public class DamageBoxSystem : ComponentSystem
    56. {
    57.     private ComponentGroup _Group;
    58.     protected override void OnCreateManager()
    59.     {
    60.         var query = new EntityArchetypeQuery
    61.         {
    62.             All = new ComponentType[]
    63.             {
    64.                 typeof(DamageBoxBuffer)
    65.             }
    66.         };
    67.         _Group = GetComponentGroup(query);
    68.     }
    69.  
    70.     public ArchetypeChunkEntityType entityType;
    71.     //public ArchetypeChunkComponentType<HealthComponent> healthType;
    72.     public ArchetypeChunkBufferType<DamageBoxBuffer> damageBoxType;
    73.  
    74.     protected override void OnUpdate()
    75.     {
    76.         entityType = GetArchetypeChunkEntityType();
    77.         //healthType = GetArchetypeChunkComponentType<HealthComponent>();
    78.         damageBoxType = GetArchetypeChunkBufferType<DamageBoxBuffer>(true);
    79.  
    80.         // Get the chunks and run the code on them
    81.         NativeArray<ArchetypeChunk> chunkArray = _Group.CreateArchetypeChunkArray(Allocator.TempJob);
    82.         for (int e = 0; e < chunkArray.Length; e++)
    83.         {
    84.             Process(chunkArray[e]);
    85.         }
    86.  
    87.         chunkArray.Dispose();
    88.     }
    89.  
    90.     public HealthComponent healthComponent;
    91.     public PhysicsComponent physicsComponent;
    92.     public DamageBoxBuffer damageBox;
    93.  
    94.     protected void Process(ArchetypeChunk chunk)
    95.     {
    96.         var entityArray = chunk.GetNativeArray(entityType);
    97.         var damageBoxArray = chunk.GetBufferAccessor(damageBoxType);
    98.  
    99.         for (int e = 0; e < chunk.Count; e++)
    100.         {
    101.             var position = EntityManager.GetComponentObject<Transform>(entityArray[e]).position;
    102.             //Position position = EntityManager.GetComponentData<Position>(entityArray[e]);
    103.  
    104.             bool attachedCDCtoEntity = false;
    105.  
    106.             for (int b = 0; b < damageBoxArray[e].Length; b++)
    107.             {
    108.                 damageBox = damageBoxArray[e][b];
    109.  
    110.                 //If IsRelative is true, cast the box relative to the entity's position.
    111.                 if (damageBoxArray[e][b].IsRelative == 1)
    112.                 {
    113.                     Vector2 spawnPosition = new Vector2(position.x + damageBox.Position.x, position.y + damageBox.Position.y);
    114.  
    115.                     var colBox = Physics2D.BoxCastAll(
    116.                         spawnPosition,
    117.                         damageBox.Size,
    118.                         0,
    119.                         damageBox.KnockbackDirection,
    120.                         damageBox.Distance);
    121.  
    122.                     for (int c = 0; c < colBox.Length; c++)
    123.                     {
    124.                         GameObject gameObject = colBox[c].collider.gameObject;
    125.  
    126.                         //Exlcude any colliders that are attached to the Entity doing the boxcast.
    127.                         if (EntityManager.GetComponentObject<Transform>(entityArray[e]).gameObject == gameObject)
    128.                         {
    129.                             continue;
    130.                         }
    131.                  
    132.                         var gameObjectEntity = colBox[c].collider.gameObject.GetComponent<GameObjectEntity>();
    133.  
    134.                         //Exclude any colliders that are not attached to hybrid entities Unless...
    135.                         if (gameObjectEntity == null)
    136.                         {
    137.                             //There is a parent which the object corresponds with that has GameObjectEntity.
    138.                             gameObjectEntity = colBox[c].collider.gameObject.GetComponentInParent<GameObjectEntity>();
    139.  
    140.                             if (gameObjectEntity == null)
    141.                             {
    142.                                 continue;
    143.                             }
    144.                         }
    145.  
    146.                         DynamicBuffer<DamageBuffer> damageBuffer;
    147.  
    148.                         //If the Entity already has a DamageBuffer, get a reference to it. If the entity lacks a DamageBuffer, add one to the entity and hold a reference to it.
    149.                         if (EntityManager.HasComponent<DamageBuffer>(gameObjectEntity.Entity))
    150.                         {
    151.                             damageBuffer = EntityManager.GetBuffer<DamageBuffer>(gameObjectEntity.Entity);
    152.                         }
    153.                         else
    154.                         {
    155.                             damageBuffer = PostUpdateCommands.AddBuffer<DamageBuffer>(gameObjectEntity.Entity);
    156.                         }
    157.                  
    158.                         damageBuffer.Add(new DamageBuffer()
    159.                         {
    160.                             Damage = 10,
    161.                         });
    162.  
    163.                         if (!EntityManager.HasComponent<CannotDamageComponent>(gameObjectEntity.Entity) && attachedCDCtoEntity == false)
    164.                         {
    165.                             //Add a new CannotDamageComponent and set the invulnerability-length of it
    166.                             PostUpdateCommands.AddComponent(gameObjectEntity.Entity, new CannotDamageComponent { TimeLeft = damageBoxArray[e][b].InvulnerabilityFrames });
    167.                             attachedCDCtoEntity = true;
    168.                         }
    169.                     }
    170.                 }
    171.             }
    172.         }
    173.     }
    174. }
    175.  
     
    Last edited: Feb 19, 2019
  2. Attatekjir

    Attatekjir

    Joined:
    Sep 17, 2018
    Posts:
    23
    This does not directly solve your problem, but a work around could be to make the components hold an enumerable that signals its state instead of the (lack of) component signal a state of an entity. This would solve the many to one relation as the many all put the one component value to the same value.

    Furthermore, this approach is likely faster than anything involving adding or removing components with entitymanager or postupdatebarrier, as they move data between chunks on a single thread.

    There was a thread on this forum comparing different techniques of triggers for systems some time ago. I find the experiments done there hard to follow but its worth checking out:

    https://forum.unity.com/threads/fla...onent-or-poll-over-component-data-int.599200/



    Bonus: My suggestion for a clunky solution would be to add entities that are to recieve a component are added to a NativeHashMap as a key. Then you could NativeHashMap.GetKeyArray to retrieve all unique entities that need a component added.
     
    recursive and NotaNaN like this.
  3. NotaNaN

    NotaNaN

    Joined:
    Dec 14, 2018
    Posts:
    324
    Sorry for the late response, but thank you for the great reply! The potential solutions you proposed are some really good ideas, but i found my own clunky solution that worked... Relatively well. It uses the same principles as your clunky solution except i don't use a NativeHashMap and i instead compare entity references on the fly via a large entity-NativeArray. Yeah... I know, it's Really inefficient. But it works, so I'm fine with it... For now. If it does become a bottleneck or a massive problem you can bet that code is going to be ejected straight into the "bad ideas" bin and I'll come up with something better (or use your other idea!).

    Also, thank you so much for the link to that amazing thread! I had no idea getting and setting things by reference was so efficient... And i now know that i should really stay away from .SetComponent's of any kind if i can. Yikes! Never would have thought they were so evil.

    Thank you for your time, Attatejir. It is greatly appreciated!

    Bonus: After reading that mega-thread I'm a little paranoid... Are adding and removing lots of DynamicBuffers as inefficientt as removing components? My whole damage system uses them to, well, deal damage... If DynamicBuffers are potentially slow when used in this way, what are some good alternatives to them? Should i just have a constantly-existing "damage query component" that has a float that tracks how much damage the entity should take? (It'd be doing things by reference... So that's good). Doing this just feels so... Anti-ECS i guess... Anybody have some thoughts on this? Is it worth the performance boost and is it they way i should be doing things?
     
    Last edited: Feb 20, 2019
    Attatekjir likes this.
  4. Attatekjir

    Attatekjir

    Joined:
    Sep 17, 2018
    Posts:
    23
    Cheers, happy to help.

    Adding or removing the actual buffer to an entity changes its "archetype", which requires the entities data to be moved between chunks, similar to adding or removing an IComponentData.

    When a buffer is initially added to an entity it is stored in the actual chunk with a size specified by [InternalBufferCapacity(x)]. I believe this means that it is as efficient to operate on this buffer in the chunk as operating on a regular component. When u add more "bufferelements" to the buffer than its current capacity allows for then it will allocate additional capacity outside of the chunk. Naturally, this means operations will not be as efficient on it as it was in the chunk.

    I would say that having a permanent DynamicBuffer could be viable in your case, but then what if 100 enemies target a single entity then your buffer would be extended to be huge to accommodate a few busy updates and then thereafter remain relatively empty wasting accesing time and storage.


    Instead of a DynamicBuffer you could also implement a NativeQueue that stores all the damage requests. This is what that big Unity ecs demo made by Nordeus did :

    https://github.com/Unity-Technologies/UniteAustinTechnicalPresentation

    Take a look at AttackTargetJob.cs and CommandSystem.cs

    Note that this demo was made many ecs version ago and thus alot of the code uses outdated api like [Inject], but the flow of the data and systems remain nice examples.
     
    NotaNaN likes this.
  5. NotaNaN

    NotaNaN

    Joined:
    Dec 14, 2018
    Posts:
    324
    Curses, i didn't even think about my entities moving between chunks... And with me adding and removing DynamicBuffers every frame (with potentially large capacities of IBufferElement's as well) i believe it'd be a good idea to follow your suggestion and use something else. Also, i never would've thought that when a DynamicBuffer reaches its capacity that it allocates more data outside its own chunk -- decreasing performance when accessing data beyond its capacity... I always thought it just did something magical and allocated more memory for me. So thanks for mentioning that as I'll keep that in mind next time I'm making a decision on whether to use DynamicBuffers. (Ignorance is bliss... But not when programing).

    Whoa... NativeQueue? (Never heard of that).
    Okay, I've read your links and did a some research on NativeQueue's... (For anyone who dosn't know what a NativeQueue is, it's a FIFO collection: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) ). But i have some questions about them... The first being; are they like lists in that they are resizable? I'm guessing that this is true as a queue that cannot change size dynamically doesn't sound all that useful.
    (I looked in the Unity Docs ( https://docs.unity3d.com/Manual/JobSystemNativeContainer.html ) for an answer but as far as i can tell they only ever stated that NativeLists are resizable, never Queues).
    EDIT: Ha ha... Decided it would be a good idea to tinker with some NativeQueues in my code and i immediately saw that there is no "size" allocation when creating a new one. So yes, they are in fact, resizable. And automatic too. Fancy.
    EDIT 2: Hmm... NativeQueues have no .Add function like NativeLists; and there doesn't seem to be any obvious way to add something to a NativeQueue... No .Queue method or anything. (They do have a Dequeue method though...)
    EDIT 3: I'm an idiot. Found the Enqueue method (no idea how i missed it the first time...).


    My second question is a bit more important; Exactly how / where are you supposed to fill this queue? In the examples you gave me (which were absolutely excellent by the way) they never seemed to populate the NativeQueue within those same scripts (except once in the AttackTargetJob at line: 43). Are other systems supposed to add items into the queue? If so, isn't that like... Illegal? I thought direct system to system communication was considered bad! (Alternatively i could have just missed the part where they were doing the populating, which when considering myself... Is highly probable).

    Either way, if you would answer that last question then i think I'll be all set to implement your amazing suggestion. Thank you so much for helping me through this; it really has been a pleasure! :D
     
    Last edited: Feb 21, 2019
  6. Attatekjir

    Attatekjir

    Joined:
    Sep 17, 2018
    Posts:
    23
    I do agree that system to system communication is not the optimal solution. However, I can not think of a better solution. An additional downside to the NativeQueue approach is that the jobsystem does not seem to recognise Native containers for job dependencies. If jobs A and B are both to work on only the same Native container, then they do not wait for eachother to finish, forcing you to .Complete() the first job and only then schedule the second job. This can be seen in CommandSystem.cs on line 36. My hope is that we are able to make NativeContainers matter in job dependencies in the future.

    Also, consider what @tertle recommends on system communication in the following thread:

    https://forum.unity.com/threads/sys...b-completion-on-shutdown.629182/#post-4214482


    Good luck on whatever solution you end up using! ;)
     
    NotaNaN likes this.
  7. NotaNaN

    NotaNaN

    Joined:
    Dec 14, 2018
    Posts:
    324
    Yikes, that's definitely something i will keep in mind when i start Jobbifying my code in the future! I'll also be sure to consider what @tertle said about system dependencies and only use them when i absolutely have to or when massive efficiency gains are at stake!

    Thanks! I ended up using NativeQueues just like you suggested; but I did have to use two NativeQueues — one to hold the soon-to-be-applied damage and the other to hold the entity that will be receiving it. All in all though, it works like a charm and I wish I new about these things a long time ago! :D

    Thank you so much for pointing me in the right direction, giving me great advice, and providing me with amazingly effective (and elegant) solutions from your own knowledge and experiences! I never would have thought I could learn so much from a single thread and from a single person! Thank you for all the time you’ve put into your replies to help me out. I hope to see you around the forums again soon! (But hopefully not with issues I need to fix). :p
     
    Attatekjir likes this.