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

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:
public struct DataA : IComponentData{}// Buffer/Component/SCD/Object are all supported
protected override void OnCreate()
 {
    DisableInfo = World.GetOrCreateSystem<ComponentDisableInfoSystem>();
    DisableInfo.RegisterTypeForDisable<DataA>();

    ExistInfo = World.GetOrCreateSystem<ComponentExistInfoSystem>();
    ExistInfo.RegisterTypeForTracking<DataA>();
}
protected override void OnUpdate()
{
    var disableHandle = DisableInfo.GetDisableHand<DataA>();
    var existHandle = ExistInfo.GetExistHandle<DataA>();

    Entities.WithoutBurst() // WithoutBurst Only for Debug.Log
    .WithChangeFilter<ComponentDisable>()
    .WithChangeFilter<ComponentExist>()
    .ForEach((Entity e, in ComponentDisable disable, in ComponentExist exist) =>
    {
        Debug.Log($"Entity[{e}] Has DataA={HasComponent<DataA>(e)} Enabled={disable.GetEnabled(disableHandle)}, Exist={exist.GetTrackState(existHandle)}");
    }).Schedule();
}

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

1 Like

A couple of bugs fixed. And this a Unity Project now can be tested out of the box.

The way ComponentExist works:

  1. Add ComponentExist on Entities you want to track

Then in your system

  1. In OnCreate() call ComponentExistInfoSystem.RegisterTypeForTracking() 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() to get any number of TrackHandle you need. You will need them in job.

  1. add ComponentExist component to your Query or ForEach

  2. 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() 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

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.

Added OptionalComponent support

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.

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.

public class ApplyDamageSystem : SystemBase
{
    HPChangeEventSystem m_HPSystem;
    ComponentDisableInfoSystem DisableInfo;
    EntityQuery CoreQuery;
    protected override void OnCreate()
    {
        m_HPSystem = World.GetOrCreateSystem<HPChangeEventSystem>();
        DisableInfo = World.GetOrCreateSystem<ComponentDisableInfoSystem>();
        DisableInfo.RegisterTypeForDisable<Damage>();
    }

    protected override void OnUpdate()
    {
        var writer = m_HPSystem.GetStreamWriter();
        var dmgDisableHandle = DisableInfo.GetDisableHandle<Damage>();
        var dependency = Dependency;
       //Collect Component from Query that target component is not required
       //Use default if not found on entity
       //This is done in one IJobChunk
        (var disables, var damageFilters, var damageParams) =
            CoreQuery.ToOptionalDataArrayAsync<ComponentDisable, DamageFilter, DamageParameter>(
                this, Allocator.TempJob, ref dependency,
                default,
                DamageFilter.Default,
                DamageParameter.Default);
 
        var dmgJob =
        Entities.WithName("ApplyDamage")
        .WithAll<CoreTag>()
        .WithChangeFilter<PhysicsContactState2D>()
        .WithStoreEntityQueryInField(ref CoreQuery)
        .ForEach((Entity coreEntity,
            int entityInQueryIndex,
            in Damage damage,
            in DynamicBuffer<PhysicsContactState2D> contacts) =>
        {
            var disable = disables[entityInQueryIndex];
            var damageFilter = damageFilters[entityInQueryIndex];
            var parameter = damageParams[entityInQueryIndex];

            var handle = writer.BeginBatch();
            for (int i = 0, len = contacts.Length; i < len; i++)
            {
                var contact = contacts[i];
                var enemyEntity = contact.Other;

                if ( contact.Type == ContactType.Collider &
                     contact.State == ContactState.Enter &
                     HasComponent<FoeTag>(enemyEntity) &
                     disable.GetEnabled(dmgDisableHandle))
                {
                    handle.WriteDamage(enemyEntity, damageFilter.FilterDamage(damage, parameter), coreEntity);
                }
            }
            handle.EndBatch();
        }).Schedule(JobHandle.CombineDependencies(Dependency, m_HPSystem.WaiteFroEventProcess));
        m_HPSystem.WaiteFroStreamAccess = dmgJob;
        disables.Dispose(dmgJob);
        damageFilters.Dispose(dmgJob);
        damageParams.Dispose(dmgJob);
        Dependency = dmgJob;
    }
}

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://discussions.unity.com/t/809424

Hope Optional component can be supported by ECS as well.

1 Like

And for ComponentExist It’s designed to not change Version when there’s no add and remove happening.

WithChangeFilter() 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() 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.