Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Feedback Unintuitive behavior of EntityQuery in System and OnUpdate

Discussion in 'Entity Component System' started by sschoener, Oct 6, 2019.

  1. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    Hi there, I have stumbled upon some behavior that I find very unintuitive. I fully understand why it is there but it took me some time to figure it out. Take it as a piece of UX feedback :) If you want to get the full experience, create an empty Unity project, install the Entities package (version 0.1.1 preview), and then paste each of the code snippets below into a script file (without reading the discussion in between). If you can correctly predict what each of them will do, congratulations! :)

    Consider this setup:
    Code (CSharp):
    1. using Unity.Entities;
    2.  
    3. public struct Tag : IComponentData {}
    4.  
    5. [UpdateInGroup(typeof(SimulationSystemGroup))]
    6. public class SpawnEntitySystem : ComponentSystem
    7. {
    8.     protected override void OnCreate()
    9.     {
    10.         EntityManager.CreateEntity();
    11.     }
    12.  
    13.     protected override void OnUpdate() {}
    14. }
    15.  
    16. [UpdateInGroup(typeof(SimulationSystemGroup))]
    17. [UpdateAfter(typeof(SpawnEntitySystem))]
    18. public class Example : ComponentSystem
    19. {
    20.     protected override void OnUpdate()
    21.     {
    22.         if (Entities.WithAll<Tag>().ToEntityQuery().CalculateEntityCount() == 0) {
    23.             PostUpdateCommands.AddComponent(EntityManager.UniversalQuery, typeof(Tag));
    24.         }
    25.     }
    26. }
    27.  
    The intention here is that the Example system checks whether there is any entity with a given tag and if there is not, it adds the tag to all entities. This last part is not actually the core of the problem; I'm mostly interested in systems that only run if a certain kind of entity does not exist (think of a puzzle game that only lets you interact if there is no animation playing).

    This system seems to do what you expect, but only in the first frame. The query that is constructed in the update loop is cached internally by the system which then decides to only run when the query matches any entities, which is exactly not what we want. In the first frame the update is running because it doesn't have any query stored yet.

    There are a couple of unintuitive consequences. For example, the documentation suggests caching EntityQuery objects as members (that saves some work, though the systems themselves cache all queries anyway). If you do this, you end up with this setup:

    Code (CSharp):
    1. using Unity.Entities;
    2.  
    3. public struct Tag : IComponentData {}
    4.  
    5. [UpdateInGroup(typeof(SimulationSystemGroup))]
    6. public class SpawnEntitySystem : ComponentSystem
    7. {
    8.     protected override void OnCreate()
    9.     {
    10.         EntityManager.CreateEntity();
    11.     }
    12.  
    13.     protected override void OnUpdate() {}
    14. }
    15.  
    16. [UpdateInGroup(typeof(SimulationSystemGroup))]
    17. [UpdateAfter(typeof(SpawnEntitySystem))]
    18. public class Example : ComponentSystem
    19. {
    20.     private EntityQuery _query;
    21.     protected override void OnCreate()
    22.     {
    23.         _query = Entities.WithAll<Tag>().ToEntityQuery();
    24.     }
    25.  
    26.     protected override void OnUpdate()
    27.     {
    28.         if (_query.CalculateEntityCount() == 0) {
    29.             PostUpdateCommands.AddComponent(EntityManager.UniversalQuery, typeof(Tag));
    30.         }
    31.     }
    32. }
    On the surface, this seems to have a similar semantic, but the system will actually never run. The query is created in the OnCreate function and will then be used to determine whether to update the system.

    Now we could go ahead and notice that we actually depend on another query, the universal query (OK, this is not a real dependency due to the nature of the query, but let's assume it's something else). To make this dependency a bit more obvious, you might want to use RequireForUpdate:

    Code (CSharp):
    1.  
    2. using Unity.Entities;
    3.  
    4. public struct Tag : IComponentData {}
    5.  
    6. [UpdateInGroup(typeof(SimulationSystemGroup))]
    7. public class SpawnEntitySystem : ComponentSystem
    8. {
    9.     protected override void OnCreate()
    10.     {
    11.         EntityManager.CreateEntity();
    12.     }
    13.  
    14.     protected override void OnUpdate() {}
    15. }
    16.  
    17. [UpdateInGroup(typeof(SimulationSystemGroup))]
    18. [UpdateAfter(typeof(SpawnEntitySystem))]
    19. public class Example : ComponentSystem
    20. {
    21.     private EntityQuery _query;
    22.     protected override void OnCreate()
    23.     {
    24.         _query = Entities.WithAll<Tag>().ToEntityQuery();
    25.         RequireForUpdate(EntityManager.UniversalQuery);
    26.     }
    27.  
    28.     protected override void OnUpdate()
    29.     {
    30.         if (_query.CalculateEntityCount() == 0) {
    31.             PostUpdateCommands.AddComponent(EntityManager.UniversalQuery, typeof(Tag));
    32.         }
    33.     }
    34. }
    You can actually use RequireForUpdate multiple times in a system to specify multiple dependencies, so it seems like it is always adding new restrictions - but in this case it will first and foremost remove the dependency on the query that you created just before, so now the system runs in every frame again.

    I'm not sure what the right answer is; but I feel like it is worth pointing out that this behavior is potentially confusing. :)
     
  2. ndesy

    ndesy

    Joined:
    Jul 22, 2019
    Posts:
    20
    Your query should reflect what you are looking for. Instead of checking if a query returns no result, your query should return the entities that don't have the tag component. Something like :

    Code (CSharp):
    1. [UpdateInGroup(typeof(SimulationSystemGroup))]
    2. [UpdateAfter(typeof(SpawnEntitySystem))]
    3. public class Example : ComponentSystem
    4. {
    5.     private EntityQuery _query;
    6.  
    7.     protected override void OnCreate()
    8.     {
    9.         _query = EntityManager.CreateEntityQuery(ComponentType.Exclude<Tag>());
    10.     }
    11.  
    12.     protected override void OnUpdate()
    13.     {
    14.         EntityManager.AddComponent<Tag>(_query);
    15.     }
    16. }
     
    florianhanke likes this.
  3. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    Thanks! :) This works in this example but I'm more interested in the general case of systems that should only run when a certain component, let's call it Tag, does not exist at all. As stated above, this might be a system that should only run when there is no animation playing (block input when animation is playing), when there is no player present etc. In that case you could count the total number of entities and make sure that it equals the number of entities that do not have Tag (that seems very roundabout and fails if there are no entities at all).

    I can think of a few ways to fix this (e.g. use [AlwaysUpdate]), that is not the problem. I'm mostly interested in the UX ramifications of this: It struck me as very surprising behavior because I would not have expected that creating a query has such side-effects (even though I now understand why they happen).
     
  4. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    And use GetEntityQuery.
     
  5. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    I'm not sure what you are referring to. If you suggest to use GetEntityQuery to fix the problem itself, then that won't work since that is using the same mechanisms behind the scenes as the fluent interface you get via Entities.With<> etc. In fact, GetEntityQuery is part of the code path that ends up registering the query to the component system. If you suggest using GetEntityQuery itself instead of the fluent interface on principle, then I'd really appreciate some pointers as to why I should be using an older interface instead of a set of convenience functions that are built right on top of it (as said, they also call into GetEntityQuery eventually).

    I'd count EntityManager.CreateEntityQuery as one of many valid answers to the technical problem, but the confusing interface remains.
     
  6. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,754
    Unless there is something new, GetEntityQuery is formal and advised way by Unity DOTS team, of getting entities in a systems.
    Going against, may be potentially problematic in later development, specially when DOTS still evolves, unless there is good reason behind, to do so.
     
  7. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    My answer was about that.
    If you declare system query at creation you should use GetEntityQuery for “safe” future changes. Cause GetEntityQuery not only creates query, but builds all required dependencies (EntityManager.CreateEntityQuery not doing that) and doing query caching postprocessing and much earlier 'early out' if cached exists.
    Fluent is for implicit query declaration in update loop like IJFE does etc. For your case you're looking from wrong direction. You shouldn't do WithAll and check length, you should (if you still want fluent) use WithNone in this case system runs only if entity without specific component exists.
     
    Last edited: Oct 7, 2019
  8. Lucas-Meijer

    Lucas-Meijer

    Unity Technologies

    Joined:
    Nov 26, 2012
    Posts:
    175
    the new compiler based Entities.ForEach() that we're working on will not have this problem. It will invoke GetEntiityQuery from OnCreate() method, so you'd no longer have this situation of "query will be created only after the first frame is run". agree that this behaviour is confusing.
     
    echeg likes this.
  9. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    @eizenhorn: Thanks for clarifying. My original question does not mention EntityManager.CreateEntityQuery at all at any point. Your three word answer confused me more than anything else :( Also, my question is not about this specific example but about the general case of executing a system when a component does not exist (see original post or my answer to the first answer).

    @Lucas-Meijer: Thanks, this seems to fix one of the problems. This would still lead to surprising behavior when you want to use a query to check whether no entity has a component, i.e. a component does not exist at all. The Update for the system is only scheduled when no query is empty (unless you have AlwaysUpdate on the system or manually specify RequireForUpdate), so it is hard to make a system check for non-existence. Also, my expectation was that RequireForUpdate and RequireSingletonForUpdate add additional update dependencies (since you can use them multiple times), but the first call to any of those will override whatever update dependencies were already added by any calls to GetEntityQuery. That was counter-intuitive.
     
  10. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    sschoener likes this.
  11. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    Ah! That makes much more sense, thank you :)
     
  12. sschoener

    sschoener

    Joined:
    Aug 18, 2014
    Posts:
    73
    Here is another piece of unintuitive behavior w.r.t. how the entity queries in a system influence its update: The documentation currently states that
    which as we have seen above is not quite true once you use RequireForUpdate.

    There is another case where the behavior differs from what is described in the documentation, namely when filtering for changes:
    Code (CSharp):
    1. public struct DataComponent : IComponentData
    2. {
    3.     public int x;
    4. }
    5.  
    6. [UpdateInGroup(typeof(SimulationSystemGroup))]
    7. public class Simulation : ComponentSystem
    8. {
    9.     private EntityQuery _query;
    10.     protected override void OnCreate()
    11.     {
    12.         var e = EntityManager.CreateEntity();
    13.         EntityManager.AddComponent<DataComponent>(e);
    14.  
    15.         _query = Entities.WithAllReadOnly<DataComponent>().ToEntityQuery();
    16.         _query.SetFilterChanged(typeof(DataComponent));
    17.     }
    18.  
    19.     protected override void OnUpdate()
    20.     {
    21.         // this assert fails
    22.         Debug.Assert(_query.CalculateEntityCount() > 0);
    23.     }
    24. }
    The
    OnUpdate on the system will execute anyway as long as there are any DataComponents present, no matter whether they changed or not.
     
    YurySedyakin likes this.