Search Unity

Why isn't SetFilterChange working in my system?

Discussion in 'Entity Component System' started by kork, Jan 28, 2019.

  1. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    I'm currently trying to create a very simple reactive system. I have a turn based game and at the end of the turn one unit who is next gets marked with an
    ActiveUnit 
    marker component. Now to update the GUI I would like to react on this and somehow highlight the currently active unit, so I wrote this little guy:

    Code (CSharp):
    1.   [DependencyComponent]
    2.     [DisableAutoCreation]
    3.     [UpdateAfter(typeof(NewTurnBarrier))]
    4.     public class ActiveUnitTrackingSystem : ComponentSystem
    5.     {
    6.         private ComponentGroup _activeUnitGroup;
    7.  
    8.         protected override void OnCreateManager()
    9.         {
    10.             _activeUnitGroup = GetComponentGroup(ComponentType.ReadOnly<Unit>(), ComponentType.ReadOnly<ActiveUnit>());
    11.             _activeUnitGroup.SetFilterChanged(ComponentType.ReadOnly<ActiveUnit>());
    12.         }
    13.  
    14.         protected override void OnUpdate()
    15.         {
    16.             var entities = _activeUnitGroup.GetEntityArray();
    17.             if (entities.Length > 0)
    18.             {
    19.                 Debug.Log("New Active Unit!");
    20.             }
    21.         }
    22.     }
    However, the log output never comes. I also tried
     CompoentType.Create
    instead of
    ComponentType.ReadOnly
    , no difference. In an earlier thread I read that
    SetFilterChange
    only works with
    ISharedComponentData
    . Is this still the case? If so, how would you track changes to this component which is not shared?
     
  2. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546

    I don't use this active right now.

    But what I know about it: It only detects that a chunk has changed.
    So you won't get the single component, instead you get all components in a specific chunk.

    Maybe you can work with event entities?
     
  3. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Well since only one Unit can be active at any given time, having this tracked at the chunk level would be OK since that group would only ever contain one unit.

    I could of course also use event entities, but I'm not super happy with this approach as you always have to think about what events you need to throw if you change a component, because you don't know who might be interested in a change of that component. Worst case, over time you get a change event for each component you use. While this would be doable - it isn't exactly making the code cleaner or easier to maintain. So I was really hoping that
    SetFilterChanged
    would help me out here a bit.
     
  4. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    Have you tried it with ISharedComponentData?

    A other way would be to take use of chunk api.
    There you can ask for the chunk version (it's the same what SetFilterChange does internally I think)
    If the chunk version is changed you know that something was changed.

    EDIT:
    Api can be found at the bottom
    https://github.com/Unity-Technologi...blob/master/Documentation~/chunk_iteration.md
     
  5. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    So I upgraded to preview 23 this morning and now I have the reverse behaviour, that it is always shown as modified even if I didn't move the component around.

    Not yet.

    Yes, so i tried this, just to get some insight into what is actually going on. And lo and behold, it always returns version 0 for the chunk where that component is living. So my running theory is:

    1. I remove the
    ActiveUnit 
    marker from the one Unit that currently has it. Now the archetype that has
    ActiveUnit 
    in it, has no more members because i removed the last unit from it. So probably the unit gets moved elswhere, and the archetypes last chunk is destroyed (or something the like, I don't really know about the internals).
    2. I add the
    ActiveUnit 
    marker to the new Unit that should get it. Now the archetype needs a new chunk because now we have a new entity for this archetype. So this will create a new chunk somewhere and initialize this with Version 0. And since I do not modify the component afterwards, it stays 0.

    And 0 is a magic number that is always treated as "changed" - see this excerpt from
    ChangeVersionUtility
    :

    Code (CSharp):
    1.  public static bool DidChange(uint changeVersion, uint requiredVersion)
    2.         {
    3.             // initial state data always triggers a change
    4.             if (changeVersion == 0)
    5.                 return true;

    So basically it would seem that the whole chunk component type versioning isn't helping me when I try to track added components, it only seems to work for detecting entities that have modified components. Unless I'm missing something totally obvious here.

    I'll try the
    ISharedComponentData 
    next and see how this would work.
     
  6. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Negative: using
    ISharedComponentData 
    basically would kill this component for any use inside of jobs, so no more
    IJobChunk
    or
    IJobComponentData<ActiveUnit>.
     
  7. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    ISharedComponentData it's better for filtering, also you always can get SCD index and use it in chunk iteration (In IJobChunk for example) chunk has SCD index value. For IJobProcessComponentData you can use require component tag.
     
  8. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Thank you this is very helpful. So I tried switching to a shared component, but now there doesn't seem to be any
    DidChange 
    function in
    ArchetypeChunkArray 
    which takes an
    ArchetypeChunkSharedComponentType 
    as parameter, so how would you know if the component was added to a different entity?


    Code (CSharp):
    1.  [DependencyComponent]
    2.     [DisableAutoCreation]
    3.     [UpdateAfter(typeof(NewTurnBarrier))]
    4.     public class ActiveUnitTrackingSystem : JobComponentSystem
    5.     {
    6.         private ComponentGroup _activeUnitGroup;
    7.  
    8.         protected override void OnCreateManager()
    9.         {
    10.             _activeUnitGroup = GetComponentGroup(ComponentType.ReadOnly<Unit>(), ComponentType.ReadOnly<ActiveUnit>());
    11.         }
    12.  
    13.         private struct DetectChangeJob : IJobChunk
    14.         {
    15.             [ReadOnly] public ArchetypeChunkSharedComponentType<ActiveUnit> ChunkComponentType;
    16.             public uint LastSystemVersion;
    17.             [WriteOnly] public NativeQueue<BBool>.Concurrent Results;
    18.            
    19.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    20.             {
    21.                 if (chunk.DidChange(ChunkComponentType, LastSystemVersion)) // <---- This does not compile
    22.                 {
    23.                     Debug.Log( "VersioN: " + chunk.GetComponentVersion(ChunkComponentType));
    24.                     Results.Enqueue(true);                  
    25.                 }
    26.                 else
    27.                 {
    28.                     Results.Enqueue(false);
    29.                 }
    30.             }
    31.         }
    32.  
    33.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    34.         {
    35.             var chunkComponentType = GetArchetypeChunkSharedComponentType<ActiveUnit>();
    36.             var results = new NativeQueue<BBool>(Allocator.TempJob);
    37.  
    38.             var detectHandle = new DetectChangeJob
    39.             {
    40.                 LastSystemVersion = LastSystemVersion,
    41.                 ChunkComponentType = chunkComponentType,
    42.                 Results = results.ToConcurrent()
    43.             }.Schedule(_activeUnitGroup, inputDeps);
    44.            
    45.             detectHandle.Complete();
    46.  
    47.             while (results.TryDequeue(out var result))
    48.             {
    49.                 if (result)
    50.                 {
    51.                     Debug.Log("NEW ACTIVE UNIT!");
    52.                     break;
    53.                 }
    54.  
    55.             }
    56.                
    57.             results.Dispose();
    58.  
    59.             return default;
    60.         }
    61.     }
     
  9. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    First of all - SCD it’s per chunk basis, not per entity.
     
  10. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    So you mean that all entities in a single chunk share the same
    ISharedComponentData
    ?

    Edit: yes, apparently they do. So If you have a marker component as ISharedComponentData I would think that even if you can get the component version of this shared component data somehow (can you actually?) it would be always zero as the data itself never changes. The component just moves to a different entity.

    So i guess this whole component versioning scheme isn't really useful in tracking added components, it only seems to be for tracking modified components. And since by definition you cannot modify a marker component (because there is nothing in there to modify), it seems like you have to use something different to find out if a component has been added / removed from an entity.
     
    Last edited: Jan 30, 2019
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Not sure why you're even trying to make this reactive. Why not just have a GUI system that filters ActiveUnit and set the UI off it. You can even use the new GetSingleton<>(), RequireSingletonForUpdate<> API to ensure only a single unit is active.

    If it's not updated every frame and setting the UI is expensive for some reason, just fire off a 1 frame event instead of tagging the entity and filter that.
     
  12. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Well, I'm still trying to find out how to build things. So let me add a few words about my thought process on why I would like this to be reactive: Updating the UI every frame is indeed expensive, so I'd like to avoid updating the UI every frame it if I can.

    Using 1-frame events is an option, but doing it reactively has the benefit that the system that changes a thing doesn't need to know which other systems might be interested in that change. When using events, every system that changes something needs to know which events to fire. When using a reactive approach every new system can just define what it is interested in and will just work without having to change any other system.

    For example a system handling AOE damage will reduce the health of all affected units. It shouldn't need to know about the UI, but now it needs to send an additional event "unit modified" or something the like for each affected unit. Another system that does damage over time would also need to send these events every time a unit loses some health. Now I have two systems that are concerned with UI updates that should just have game logic in them. And since UI is basically a cross cutting concern (almost every change will have some impact on the UI) this quickly spreads UI knowledge over all kinds of systems. Lets assume I add some shields to my units and later I decide to add a UI indicator showing the shield level. Now I need to re-visit all systems that somehow modify the shield level and add UI events there. It's doable, but it's error-prone (e.g. you could forget a system, introducing subtle UI bugs) and these systems shouldn't be concerned with this in the first place.

    So that is why I tried to go the reactive way here. Another way would be creating a system that runs per frame and checks for all units if the unit's state differs from the UI state and updates the UI if necessary, though I'm not sure how well this would perform.

    Interesting, I didn't know about these. I tried to find a change log for the ECS where such changes are listed but I couldn't find one. Did you find these by accident?
     
  13. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
  14. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Thank you very much, I actually came over this but I must have skimmed it a bit too quickly, I overlooked the lines with the singleton stuff :)

    @eizenhorn, IIRC you actually wrote a game with ECS, how did you integrate your UI with the rest of the world? Did you send events like @tertle suggested? Or did you use a different approach?
     
  15. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    I'm use MB for UI and my systems send's eventh when data for UI changes.
     
  16. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    Thank you!
     
  17. timmehhhhhhh

    timmehhhhhhh

    Joined:
    Sep 10, 2013
    Posts:
    157
    @kork maybe i am misunderstanding something here, but several times in the thread it sounds like all you want is to know when an entity has been added to the group - if this is the case, we have ISystemStateComponentData just for this task:

    Code (CSharp):
    1.  
    2. [DependencyComponent]
    3. [DisableAutoCreation]
    4. [UpdateAfter(typeof(NewTurnBarrier))]
    5. public class ActiveUnitTrackingSystem : ComponentSystem
    6. {
    7.     ComponentGroup activatedUnits;
    8.     ComponentGroup deactivatedUnits;
    9.  
    10.     struct Initialized : ISystemStateComponentData { }
    11.  
    12.     protected override void OnCreateManager()
    13.     {
    14.         var activatedQuery = new EntityArchetypeQuery
    15.         {
    16.             All = new ComponentType[] { typeof(Unit), typeof(ActiveUnit), },
    17.             None = new ComponentType[] { typeof(Initialized), }
    18.         };
    19.         activatedUnits = GetComponentGroup(activatedQuery);
    20.  
    21.         var deactivatedQuery = new EntityArchetypeQuery
    22.         {
    23.             All = new ComponentType[] { typeof(Unit), typeof(Initialized), },
    24.             None = new ComponentType[] { typeof(ActiveUnit), },
    25.         };
    26.         deactivatedUnits = GetComponentGroup(deactivatedQuery);
    27.     }
    28.     protected override void OnUpdate()
    29.     {
    30.         ForEach((Entity entity) =>
    31.         {
    32.             Debug.Log("Unit activated!");
    33.             PostUpdateCommands.AddComponent(entity, new Initialized());
    34.         }, activatedUnits);
    35.  
    36.         ForEach((Entity entity) =>
    37.         {
    38.             Debug.Log("Unit deactivated!");
    39.             PostUpdateCommands.RemoveComponent<Initialized>(entity);
    40.         }, deactivatedUnits);
    41.     }
    42. }
    43.  
    joachim goes over them a bit here if you like videos.

    personally, i am still very much hoping for reactivity at the component data level (ie, getting some flag or notification when a component / buffer / shared / whatever data has changed). but this along with the ForEach is already quite a nice start for more reactive code.
     
    Last edited: Feb 24, 2019
  18. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    @tbriley thank you for the example, something like this is what I am using right now.

    I did some further digging into the reactive side of things and for now it seems there are two approaches on how to handle change:

    * If you want to know if a component was added/removed you need to do the "marker component dance", e.g. create a new marker component derived from
    ISystemStateComponentData
    and then build a system that adds/removes this marker component so you can keep track of whether or not the component you actually want to track was added/removed. I'm not really a huge fan of this system as it basically requires you to have a component pair plus a system for each component for which you want to track adds/removes, which is quite a lot of boilerplate for something that is frequently needed.

    * If you want to know if a component itself was changed (e.g. it's values were changed), you can use the
    SetFilterChange
    functionality of or manually compare the component versions and do chunk iteration over the chunks that have a newer component version. Even there you only know that at least one of the components in that chunk has changed, not which one, so your change handling logic needs to be idempotent or you need a way of tracking the previous value so you can compare values and decide if they have changed or not.

    So
    SetFilterChange
    isn't going to help with added/removed components and this was basically my mistake leading to the creation of this thread. I thought it would work as there is a special component version 0 which would indicate an added component. However this is only useful if your component can actually change (e.g. it has some members you can modify). Since
    ActiveUnit
    is just a marker component and has no members, after being added the component version will always stay at 0 as I don't modify the component anymore. And because
    SetFilterChange
    will ignore components with a version of 0 my debug output never came.

    So my cheat sheet for now is:

    * Add/Remove - do the "marker component dance"
    * Modify - use
    SetFilterChange
    or do chunk iteration and compare the chunk's component version with
    LastSystemVersion
    .
     
    Sibz9000 likes this.
  19. Sibz9000

    Sibz9000

    Joined:
    Feb 24, 2018
    Posts:
    149
    @kork Thanks for the update, I ran into issues using the changed filter. This explains why. My solution was a to add a changed component, though I decided to scrap the particular system in the end. In future I'll probably use an event system.