A simple example of using IEnableableComponent

There was a time in the history of Unity’s ECS that we used tag components to control the game logic. For example, when an entity is “active”, we add an Active component to it. When the entity becomes inactive, we remove this Active component. What this does then is it makes it easy to query for “active” or “inactive” entities. We just add to the query that it should include entities with Active component or exclude them if we want the inactive ones.

However, this was considered bad because adding or removing components causes a “structural change”. Remember that the data in ECS are stored in chunks where each chunk only allows a certain group of components. This group of components is referred to as the “archetype”. When a component is added or removed, that entity no longer fits the archetype of the chunk it is currently in so it must be moved to the appropriate chunk. This transfer of data to different chunks is referred to as “structural change”. They are slow and must be avoided. Joachim Ante (former CTO of Unity) confirmed this himself. Using booleans within components was deemed better. It introduces branching in jobs but still faster than causing structural changes. With the introduction of IEnableableComponent, we can use the query again to filter active or inactive entities, initialized or not initialized, processed or not processed, and whatever boolean state your game needs. This doesn’t cause a structural change as the enabled state is already embedded in the chunk as reserved bits (don’t ask me for the details).

IEnableableComponent is one of the new features of Unity’s Entities 1.0. When a component has this interface, it will have an enabled or disabled state. What this means is you can include this component in a query but disabled ones are ignored. Let’s get to the code!

The sample code

I’m just going to show you one simple system to showcase how an IEnableableComponent is used. What we do in this system is we create some entities that are active, and some that are inactive. On click, the system then toggles the active state of all entities. All active entities becomes inactive and all inactive becomes active. To show that it works, we assign each entity an integer ID using a component which we then display through Debug.Log(). Nothing fancy.

Let’s start with our components:

// This is our IEnableableComponent for this example
public struct Active : IComponentData, IEnableableComponent {
}

// This is the component that holds the integer ID that we can print through Debug.Log()
public readonly struct IntId : IComponentData {
    public readonly int Value;

    public IntId(int value) {
        this.Value = value;
    }
}

These are very straightforward. The struct Active is the component that will represent the “active” state of our entities. Note here that both the interfaces IComponentData and IEnableableComponent must be specified. A struct that us an IBufferElementData could also be an IEnableableComponent. Your IEnableableComponent struct can also contain other data. It doesn’t have to be just a tag component like in our example here. The struct IntId is the component that will hold the integer ID that we can assign and show through Debug.Log() so we could see that entities where the Active component is set to disabled are skipped.

Let’s get to the system. I’ll build this piece by piece and then show you the whole system at the end. I’ll be using a struct ISystem here and IJobChunk for the jobs. Let’s start with the member variables that we need:

public partial struct IEnableableComponentsTestSystem : ISystem {
    private Handles handles;
    private EntityQuery query;

    ...
}

Handles is just an internal utility struct that is made to collect the ECS handles (EntityTypeHandle, ComponentTypeHandle, BufferTypeHandle, etc) that we use for this system. Here’s how it is implemented:

private struct Handles {
    public ComponentTypeHandle<IntId> IdType;
    public ComponentTypeHandle<Active> ActiveType;

    public Handles(ref SystemState state) {
        this.IdType = state.GetComponentTypeHandle<IntId>();
        this.ActiveType = state.GetComponentTypeHandle<Active>();
    }

    public void Update(ref SystemState state) {
        this.IdType.Update(ref state);
        this.ActiveType.Update(ref state);
    }
}

It keeps two ComponentTypeHandles for IntId and Active components. It has an Update() method that updates the handles. This is a required call before the handles can be used in OnUpdate(). You will see this method used later. The following is the code for OnCreate().

[BurstCompile]
public void OnCreate(ref SystemState state) {
    this.handles = new Handles(ref state);
    
    this.query = new EntityQueryBuilder(Allocator.Temp)
        .WithAll<IntId>()
        .WithAll<Active>().Build(ref state);
    
    // Create some entities
    EntityManager entityManager = state.EntityManager;
    EntityArchetype archetype =
        entityManager.CreateArchetype(ComponentType.ReadWrite<IntId>(),
            ComponentType.ReadWrite<Active>());
    
    // Active entities
    for (int i = 0; i < 5; ++i) {
        Entity entity = entityManager.CreateEntity(archetype);
        entityManager.SetComponentData(entity, new IntId(i + 1));
    }
    
    // Inactive entities
    for (int i = 5; i < 8; ++i) {
        Entity entity = entityManager.CreateEntity(archetype);
        entityManager.SetComponentData(entity, new IntId(i + 1));
        entityManager.SetComponentEnabled<Active>(entity, false);
    }
}

In OnCreate(), we prepare the Handles instance and the query that we will use which is to select all entities with the components IntId and Active. We want to see if really only those entities with enabled Active component would be included. Then we create some entities. We create 5 entities where Active is enabled. Note here that components with IEnableableComponent are enabled by default. We use IDs 1-5 for these entities. We then create 3 more entities where the Active component is disabled. This is done by using the method EntityManager.SetComponentEnabled(). These entities are using IDs 6-8. I specifically created different amounts of entities here so that on toggle, we’ll get a different amount of entities. We initially have 5 “active” entities, then on toggle, we’ll have 3. On next toggle, we’ll have 5 again. I want to show that setting the components enabled or disabled works.

Before we get to OnUpdate(), I’ll show the jobs first. The first one is called ToggleActiveJob. This is the job that switches the enabled state of the Active component.

[BurstCompile]
private struct ToggleActiveJob : IJobChunk {
    public ComponentTypeHandle<Active> ActiveType;

    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) {
        for (int i = 0; i < chunk.Count; i++) {
            bool active = chunk.IsComponentEnabled(ref this.ActiveType, i);
            chunk.SetComponentEnabled(ref this.ActiveType, i, !active);
        }
    }
}

This is just a classic chunk iteration job. We use the method ArchetypeChunk.IsComponentEnabled() to see if a component is enabled. We use ArchetypeChunk.SetComponentEnabled() to set whether the component is enabled or not. I mentioned that only entities with enabled Active component would be returned by the query. That’s not entirely true. That’s because we’re using IJobChunk here and it would still process the chunks that matches the query regardless if the components are enabled or not. What determines if entities with disabled components are skipped is how we iterate the chunk. In our code here, we’re just using a for loop and we can access all data in the chunk even the ones with disabled components. If we want to skip disabled components, we have to use ChunkEntityEnumerator. However, in our use case here, we want to access disabled components because we want to enable them. Thus, we use a normal for loop.

This next job prints the IntId value of only the entities where the Active component is enabled.

[BurstCompile]
private struct PrintEnabledJob : IJobChunk {
    [ReadOnly]
    public ComponentTypeHandle<IntId> IdType;
    
    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) {
        NativeArray<IntId> ids = chunk.GetNativeArray(ref this.IdType);

        ChunkEntityEnumerator enumerator = new(useEnabledMask, chunkEnabledMask, chunk.Count);
        while (enumerator.NextEntityIndex(out int i)) {
            Debug.Log($"Enabled: {ids[i].Value}");
        }
    }
}

We’re still using IJobChunk but notice that we’re now using ChunkEntityEnumerator to iterate the contents of the chunk. Now this filters disabled components. Only entities with enabled Active component get its ID logged.

Now we schedule these jobs in OnUpdate():

public void OnUpdate(ref SystemState state) {
    if (!Input.GetMouseButtonDown(0)) {
        return;    
    }
    
    this.handles.Update(ref state);
    
    // Print enabled entities
    PrintEnabledJob printJob = new() {
        IdType = this.handles.IdType
    };
    state.Dependency = printJob.Schedule(this.query, state.Dependency);
        
    ToggleActiveJob toggleJob = new() {
        ActiveType = this.handles.ActiveType
    };
    state.Dependency = toggleJob.Schedule(this.query, state.Dependency);
}

OnUpdate() only proceeds when there’s a mouse down. I did it this way so we don’t spam the editor with debug logs per frame. We schedule PrintEnabledJob first so we can see the current “active” entities. Then we schedule ToggleActiveJob to switch the enabled state of the Active components. On next click, we should be able to see new “active” entities which was toggled in the previous click.

Putting them all together looks like this:

public partial struct IEnableableComponentsTestSystem : ISystem {
    private Handles handles;
    private EntityQuery query;
    
    [BurstCompile]
    public void OnCreate(ref SystemState state) {
        this.handles = new Handles(ref state);
        
        this.query = new EntityQueryBuilder(Allocator.Temp)
            .WithAll<IntId>()
            .WithAll<Active>().Build(ref state);
        
        // Create some entities
        EntityManager entityManager = state.EntityManager;
        EntityArchetype archetype =
            entityManager.CreateArchetype(ComponentType.ReadWrite<IntId>(),
                ComponentType.ReadWrite<Active>());
        
        // Active entities
        for (int i = 0; i < 5; ++i) {
            Entity entity = entityManager.CreateEntity(archetype);
            entityManager.SetComponentData(entity, new IntId(i + 1));
        }
        
        // Inactive entities
        for (int i = 5; i < 8; ++i) {
            Entity entity = entityManager.CreateEntity(archetype);
            entityManager.SetComponentData(entity, new IntId(i + 1));
            entityManager.SetComponentEnabled<Active>(entity, false);
        }
    }

    public void OnUpdate(ref SystemState state) {
        if (!Input.GetMouseButtonDown(0)) {
            return;    
        }
        
        this.handles.Update(ref state);
        
        // Print enabled entities
        PrintEnabledJob printJob = new() {
            IdType = this.handles.IdType
        };
        state.Dependency = printJob.Schedule(this.query, state.Dependency);
            
        ToggleActiveJob toggleJob = new() {
            ActiveType = this.handles.ActiveType
        };
        state.Dependency = toggleJob.Schedule(this.query, state.Dependency);
    }
    
    [BurstCompile]
    private struct ToggleActiveJob : IJobChunk {
        public ComponentTypeHandle<Active> ActiveType;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) {
            for (int i = 0; i < chunk.Count; i++) {
                bool active = chunk.IsComponentEnabled(ref this.ActiveType, i);
                chunk.SetComponentEnabled(ref this.ActiveType, i, !active);
            }
        }
    }
    
    [BurstCompile]
    private struct PrintEnabledJob : IJobChunk {
        [ReadOnly]
        public ComponentTypeHandle<IntId> IdType;
        
        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) {
            NativeArray<IntId> ids = chunk.GetNativeArray(ref this.IdType);

            ChunkEntityEnumerator enumerator = new(useEnabledMask, chunkEnabledMask, chunk.Count);
            while (enumerator.NextEntityIndex(out int i)) {
                Debug.Log($"Enabled: {ids[i].Value}");
            }
        }
    }

    private struct Handles {
        public ComponentTypeHandle<IntId> IdType;
        public ComponentTypeHandle<Active> ActiveType;

        public Handles(ref SystemState state) {
            this.IdType = state.GetComponentTypeHandle<IntId>();
            this.ActiveType = state.GetComponentTypeHandle<Active>();
        }

        public void Update(ref SystemState state) {
            this.IdType.Update(ref state);
            this.ActiveType.Update(ref state);
        }
    }
}

With this code file in your project, you can just hit play on an empty scene and the debug logging should work. On first click, you should see entities 1-5.

On the next click, you should see entities 6-8.

On another click, you should see entities 1-5 again. Next click, 6-8 again. This demonstrates that PrintEnabledJob only processes those entities where Active is enabled.

As an experiment, we could try rewriting PrintEnabledJob as an IJobEntity to see if it only processes “active” entities. Here’s what that job looks like:

[BurstCompile]
private partial struct PrintEnabledAsIJobEntity : IJobEntity {
    public void Execute(in IntId id) {
        Debug.Log($"Enabled: {id.Value}");
    }
}

Then edit OnUpdate() to schedule the job PrintEnabledAsIJobEntity instead:

// Print enabled entities
// PrintEnabledJob printJob = new() {
//     IdType = this.handles.IdType
// };
// state.Dependency = printJob.Schedule(this.query, state.Dependency);

state.Dependency = new PrintEnabledAsIJobEntity().Schedule(this.query, state.Dependency);

You will see that the behavior is the same and this proves that IJobEntity only processes entities that have enabled components that are IEnableableComponent. We could also opt to select only those entities with disabled components by using WithNone() in the EntityQueryBuilder. I’ll leave that to you as an exercise.

That’s all I have for now. If you like what you’ve read, please consider joining my mailing list. I’ll even give you a free game. 🙂

Til next time!

Leave a comment