Search Unity

Feedback Per Component Enable/Disable, Optional, Remove/Add Tracking

Discussion in 'Entity Component System' started by Lieene-Guo, Sep 27, 2020.

  1. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    I make these three things working:
    1. Pre Component Remove/Add (2bit flags) Tracking with one Tracking System
    2. Pre Component Enable/Disable (1bit flag) capability.
    3. OptionalComponent support
    They work like this:

    Code (CSharp):
    1.  
    2. public struct DataA : IComponentData{}// Buffer/Component/SCD/Object are all supported
    3. protected override void OnCreate()
    4.  {
    5.     DisableInfo = World.GetOrCreateSystem<ComponentDisableInfoSystem>();
    6.     DisableInfo.RegisterTypeForDisable<DataA>();
    7.  
    8.     ExistInfo = World.GetOrCreateSystem<ComponentExistInfoSystem>();
    9.     ExistInfo.RegisterTypeForTracking<DataA>();
    10. }
    11. protected override void OnUpdate()
    12. {
    13.     var disableHandle = DisableInfo.GetDisableHand<DataA>();
    14.     var existHandle = ExistInfo.GetExistHandle<DataA>();
    15.  
    16.     Entities.WithoutBurst() // WithoutBurst Only for Debug.Log
    17.     .WithChangeFilter<ComponentDisable>()
    18.     .WithChangeFilter<ComponentExist>()
    19.     .ForEach((Entity e, in ComponentDisable disable, in ComponentExist exist) =>
    20.     {
    21.         Debug.Log($"Entity[{e}] Has DataA={HasComponent<DataA>(e)} Enabled={disable.GetEnabled(disableHandle)}, Exist={exist.GetTrackState(existHandle)}");
    22.     }).Schedule();
    23. }
    Please check
    https://github.com/lieene/UnityECS-CompoentStateTrack
    Bug report is expected.
    Note:
    Memory FootPrint
    1 Bit per Disable/Enable type pre entity
    2 Bits per ExistTrack type pre entity
     
    Last edited: Sep 28, 2020
    AlexanderKallin likes this.
  2. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    A couple of bugs fixed. And this a Unity Project now can be tested out of the box.
     
    Last edited: Sep 28, 2020
  3. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    The way ComponentExist works:

    0. Add ComponentExist on Entities you want to track

    Then in your system

    1. In OnCreate() call ComponentExistInfoSystem.RegisterTypeForTracking<T>() tell tracking system I want to track this type's change (That will apply to all Entity with ComponentExist as it Register a type not a entity), you can Register multiple Type, and Register the same Type multiple times is fine.

    2.in OnUpdate() call ComponentExistInfoSystem.GetExistHandle<T>() to get any number of TrackHandle you need. You will need them in job.

    3. add ComponentExist component to your Query or ForEach

    4. In the job, Call ComponentExist.GetTrackState(ComponentExistHandle) to get that component's State. Or WasAdded/WasRemoved/WasExist(ComponentExistHandle) for specific states.

    All Types' State is in that single ComponentExist

    WithChangeFilter<ComponentExist>() will only pass when there's some component added or removed the last frame.
    (Adding ComponentExist component will trigger change)

    ComponentDisable works the same way.
    use
    bool ComponentDisable.GetEnabled(ComponentDisableHandle)
    void ComponentDisable.SetEnabled(ComponentDisableHandle,bool)
    to Get/Set Enabled/Disable
     
    Last edited: Sep 28, 2020
  4. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Enable/Disable State does not depend on the target component's existence.
    You can disable a Type before it is added to an Entity.
    Or You can use it as a state bit. without adding the target component ever.
     
  5. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Added OptionalComponent support
     
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I did i quick read through the code. I think this is a good temporary solution until we have builtin support for Enable / Disable component built into the core of Unity.Entities.
     
  7. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Here is an example: on Physics Collision, filter damage, and send damage event to the target.

    There are three Optional Components in my damage system.
    DamageFilter, DamageParameter, and the third is ComponentDisable.
    When Damage Component is Disabled, No damage event will be sent.
    When ComponentDisable dose not exist, Damage is considered Enabled.
    When DamageFilter dose not exist. I want to use the default filter.
    When DamageParameterdose not exist. I want to use the default parameter.

    Here's the code with my Disable and Optional Approach.

    Code (CSharp):
    1. public class ApplyDamageSystem : SystemBase
    2. {
    3.     HPChangeEventSystem m_HPSystem;
    4.     ComponentDisableInfoSystem DisableInfo;
    5.     EntityQuery CoreQuery;
    6.     protected override void OnCreate()
    7.     {
    8.         m_HPSystem = World.GetOrCreateSystem<HPChangeEventSystem>();
    9.         DisableInfo = World.GetOrCreateSystem<ComponentDisableInfoSystem>();
    10.         DisableInfo.RegisterTypeForDisable<Damage>();
    11.     }
    12.  
    13.     protected override void OnUpdate()
    14.     {
    15.         var writer = m_HPSystem.GetStreamWriter();
    16.         var dmgDisableHandle = DisableInfo.GetDisableHandle<Damage>();
    17.         var dependency = Dependency;
    18.        //Collect Component from Query that target component is not required
    19.        //Use default if not found on entity
    20.        //This is done in one IJobChunk
    21.         (var disables, var damageFilters, var damageParams) =
    22.             CoreQuery.ToOptionalDataArrayAsync<ComponentDisable, DamageFilter, DamageParameter>(
    23.                 this, Allocator.TempJob, ref dependency,
    24.                 default,
    25.                 DamageFilter.Default,
    26.                 DamageParameter.Default);
    27.  
    28.         var dmgJob =
    29.         Entities.WithName("ApplyDamage")
    30.         .WithAll<CoreTag>()
    31.         .WithChangeFilter<PhysicsContactState2D>()
    32.         .WithStoreEntityQueryInField(ref CoreQuery)
    33.         .ForEach((Entity coreEntity,
    34.             int entityInQueryIndex,
    35.             in Damage damage,
    36.             in DynamicBuffer<PhysicsContactState2D> contacts) =>
    37.         {
    38.             var disable = disables[entityInQueryIndex];
    39.             var damageFilter = damageFilters[entityInQueryIndex];
    40.             var parameter = damageParams[entityInQueryIndex];
    41.  
    42.             var handle = writer.BeginBatch();
    43.             for (int i = 0, len = contacts.Length; i < len; i++)
    44.             {
    45.                 var contact = contacts[i];
    46.                 var enemyEntity = contact.Other;
    47.  
    48.                 if ( contact.Type == ContactType.Collider &
    49.                      contact.State == ContactState.Enter &
    50.                      HasComponent<FoeTag>(enemyEntity) &
    51.                      disable.GetEnabled(dmgDisableHandle))
    52.                 {
    53.                     handle.WriteDamage(enemyEntity, damageFilter.FilterDamage(damage, parameter), coreEntity);
    54.                 }
    55.             }
    56.             handle.EndBatch();
    57.         }).Schedule(JobHandle.CombineDependencies(Dependency, m_HPSystem.WaiteFroEventProcess));
    58.         m_HPSystem.WaiteFroStreamAccess = dmgJob;
    59.         disables.Dispose(dmgJob);
    60.         damageFilters.Dispose(dmgJob);
    61.         damageParams.Dispose(dmgJob);
    62.         Dependency = dmgJob;
    63.     }
    64. }
    Without Disable and Optional. I would need to have 8 ForEach to match every case(too much copy-paste).
    Or use 3 HasCompoent and GetComponent inside ForEach(not prefered).
    And one extra ComponentTag to mark disable

    Even with IJobChunk. I will end up with massive branches filtering component compositions.

    This is a common pattern that happens everywhere in game logics.
    Keeping it reasonably code/review friendly should be a hight priority Task.

    I am trying to keep everything in the Dots style.
    The Drawback is that ToOptionalDataArrayAsync+Entitise.ForEach will use a large chunk of memory and use one extra job. Trading back much cleaner source code and no branching in job. But it can be supported by Entites.ForEach CodeGen without this Drawback.

    A possible way is in here
    https://forum.unity.com/threads/can...reach-override-for-optional-component.973539/

    Hope Optional component can be supported by ECS as well.
     
    Last edited: Sep 28, 2020
    Lukas_Kastern likes this.
  8. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    And for ComponentExist It's designed to not change Version when there's no add and remove happening.

    WithChangeFilter<ComponentExist>() Will tell you there must be some tracked component added or removed otherwise the filter will deny that chunk.

    This can Dramatically reduce the filter match chance. and skip tons of redundant processes.

    Especially, for those removed components. Normally WithNone<T>() will be used. and it gives you all chunk without that T component no matter it is removed last frame or 1k frames ago. If there a logic that changes some value according to the existence of component T. Then that logic will run every frame even T is removed a long time ago. So we have to run a test logic or repeating calculation.
     
    Last edited: Sep 28, 2020