t1k:unity:dots-core:enableable-components
| Field | Value |
|---|---|
| Module | dots-core |
| Version | 2.3.2 |
| Effort | high |
| Tools | — |
Keywords: components, DOTS, ECS, enableable
How to invoke
Section titled “How to invoke”/t1k:unity:dots-core:enableable-componentsDOTS Enableable Components
Section titled “DOTS Enableable Components”Handles IEnableableComponent patterns — toggle, query, performance tradeoffs.
Does NOT handle general ECS queries (→ dots-ecs) or ECB patterns (→ dots-entity-command-buffer).
IEnableableComponent Interface
Section titled “IEnableableComponent Interface”// Declare: add IEnableableComponent alongside IComponentDatapublic struct IsAlive : IComponentData, IEnableableComponent { }public struct IsMoving : IComponentData, IEnableableComponent { }public struct IsAttacking: IComponentData, IEnableableComponent { }public struct IsStunned : IComponentData, IEnableableComponent { }Enableable components hold data AND an enabled bit — same chunk, no archetype change on toggle.
SetComponentEnabled vs Structural Change
Section titled “SetComponentEnabled vs Structural Change”| Operation | Chunk move? | Use when |
|---|---|---|
SetComponentEnabled(entity, false) | No | Frequent on/off toggle (stun, move, attack) |
EntityManager.RemoveComponent | Yes | Permanent state — entity never needs it again |
| Add tag component | Yes | Rare, permanent category distinction |
Rule: if the state toggles more than once per second on average, use IEnableableComponent.
Enable / Disable API
Section titled “Enable / Disable API”// In a system (SystemAPI):SystemAPI.SetComponentEnabled<IsStunned>(entity, true);bool stunned = SystemAPI.IsComponentEnabled<IsStunned>(entity);
// In an IJobEntity (via EnabledRefRW):void Execute(EnabledRefRW<IsMoving> moving, ref LocalTransform tf){ if (!moving.ValueRO) return; // process movement... moving.ValueRW = false; // disable inside job — safe}Query Filter Behavior
Section titled “Query Filter Behavior”// WithAll<T> — matches entities where T is PRESENT and ENABLED// (disabled T entities are excluded by default)SystemAPI.Query<RefRW<Health>>().WithAll<IsAlive>();
// WithDisabled<T> — matches entities where T is present but DISABLEDSystemAPI.Query<RefRW<Health>>().WithDisabled<IsAlive>();
// WithAny<T> — matches if T present (enabled OR disabled)// WithNone<T> — excludes entities that have T at all (regardless of enabled state)Key rule: WithAll does NOT match disabled components — a common source of missed entities.
EnabledRefRO / EnabledRefRW in IJobEntity
Section titled “EnabledRefRO / EnabledRefRW in IJobEntity”// Read-only checkvoid Execute(EnabledRefRO<IsAlive> alive) { if (!alive.ValueRO) return; }
// Read-write toggle (no structural change, safe in parallel job)void Execute(EnabledRefRW<IsAttacking> attacking) { attacking.ValueRW = false; }Batch Enable / Disable
Section titled “Batch Enable / Disable”// Batch via query — more efficient than per-entity loopvar query = SystemAPI.QueryBuilder().WithAll<IsStunned>().Build();state.EntityManager.SetComponentEnabled<IsStunned>(query, false);ECB and Enableable Components
Section titled “ECB and Enableable Components”// ECB supports SetComponentEnabled — deferred, plays back in orderecb.SetComponentEnabled<IsMoving>(entity, false);Gotcha: ECB SetComponentEnabled only works if the entity already has the component in its archetype. If the component was never added, this silently does nothing — add the component first.
Performance Notes
Section titled “Performance Notes”- Enable/disable = bit flip in chunk metadata — O(1), no memory allocation
- Structural change (add/remove) = archetype migration = chunk allocation potential
- Querying disabled components (
WithDisabled) still iterates chunks — use sparingly in hot paths
Gotcha — GetSingletonEntity() rejects enableable types
Section titled “Gotcha — GetSingletonEntity() rejects enableable types”SystemAPI.GetSingletonEntity<T>(), SystemAPI.GetSingleton<T>(), and SystemAPI.HasSingleton<T>() ALL throw under Burst when T : IEnableableComponent:
InvalidOperationException: Can't call GetSingletonEntity() on queries containing enableable component types.The default singleton query rejects enableable types because the singleton’s enabled state is ambiguous — “is the singleton enabled or disabled?” is a per-entity question, not a query-cardinality question. The fix is an explicit query built with EntityQueryOptions.IgnoreComponentEnabledState:
public partial struct MySystem : ISystem{ private EntityQuery signalQuery;
public void OnCreate(ref SystemState state) { this.signalQuery = new EntityQueryBuilder(Allocator.Temp) .WithAll<MyEnableableSingleton>() .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState) .Build(ref state); state.RequireForUpdate<MyEnableableSingleton>(); }
public void OnUpdate(ref SystemState state) { var entity = this.signalQuery.GetSingletonEntity(); // works — query ignores enabled state if (!state.EntityManager.IsComponentEnabled<MyEnableableSingleton>(entity)) return; // explicit per-entity enabled check // ... do work ... }}Key distinction:
EntityQueryOptions.IgnoreComponentEnabledState— query treats enableable as “match regardless of enabled bit” (what you want here)EntityQueryOptions.IncludeDisabledEntities— query also matches entities with the whole-entityDisabledtag (different concept; do NOT use for enableable singleton lookup)
Same pattern applies to SystemBase:
this.signalQuery = new EntityQueryBuilder(Allocator.Temp) .WithAll<MyEnableableSingleton>() .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState) .Build(this);Symptom: under Burst the __codegen__OnUpdate stack trace pinpoints the singleton call site. Caught multiple times in production systems that converted a plain tag/data component into IEnableableComponent without updating their singleton lookups.
Gotchas
Section titled “Gotchas”Query filter trap — WithAll<T> and RefRW<T> exclude entities where T is DISABLED
Section titled “Query filter trap — WithAll<T> and RefRW<T> exclude entities where T is DISABLED”When you query an IEnableableComponent T using SystemAPI.Query<RefRW<T>>() OR .WithAll<T>(), the query matches entities where T is present AND enabled only. Disabled-T entities are excluded by default.
Failure shape: a system whose JOB is to TOGGLE T from disabled → enabled (state-transition systems like SkillTapTriggerSystem, BossAbilityActivationSystem) cannot iterate the entities it needs to act on — because they’re disabled, the query filters them out.
Fix — .WithPresent<T>() matches enabled OR disabled:
// WRONG — filters out idle (disabled) casters; system can never start a castforeach (var (request, state) in SystemAPI.Query<EnabledRefRW<SkillTapRequest>, RefRW<ActiveCastState>>()){ /* ... */ }
// RIGHT — .WithPresent<ActiveCastState>() includes disabled entities so the// system can flip ActiveCastState from disabled → enabled.foreach (var (request, state) in SystemAPI.Query<EnabledRefRW<SkillTapRequest>, RefRW<ActiveCastState>>() .WithPresent<ActiveCastState>()){ /* ... */ }Query-filter semantics:
.WithAll<T>()/RefRW<T>/RefRO<T>→ present AND enabled (default filter).WithPresent<T>()→ enabled or disabled (the union).WithDisabled<T>()→ present AND disabled only
Codebase precedent: WagerCardSystem, BossPhaseShakeSystem use .WithPresent<T>() for the same reason.
Also applies to SystemAPI.Query foreach (not just IJobEntity): the default enabled-only filter hits the managed-system query path identically. A SystemAPI.Query<RefRO<X>, EnabledRefRW<TEvent>>() foreach that must SET TEvent from its initial disabled state iterates zero entities — and silently no-ops with no exception — unless you add .WithPresent<TEvent>().
// WRONG — TickTimerCompletedEvent starts disabled; query skips every timer,// so the completion event is never set to true.foreach (var (timer, completed) in SystemAPI.Query<RefRO<TickTimer>, EnabledRefRW<TickTimerCompletedEvent>>()){ /* never runs */ }
// RIGHTforeach (var (timer, completed) in SystemAPI.Query<RefRO<TickTimer>, EnabledRefRW<TickTimerCompletedEvent>>() .WithPresent<TickTimerCompletedEvent>()){ /* iterates every timer regardless of event-enabled state */ }Recurred 3+ times (HIGH-VALUE pattern): TickTimerEvaluationSystem (2026-05-29, fix in unity-dots-library 4ed1a31), RunResetSystem (9d50ce2), SkillTapTriggerSystem + SkillAutoFireFallbackSystem (2026-05-28 W7, a02cd35) all shipped the same disabled→enabled transition bug. Any system whose JOB is to ENABLE an IEnableableComponent from its disabled default needs .WithPresent<T>() — the absence is a silent zero-iteration no-op, never a compile or runtime error.
Reference Files
Section titled “Reference Files”| Cross-Reference | Content |
|---|---|
→ See dots-ecs | General ECS queries, IComponentData, SystemAPI |
→ See dots-entity-command-buffer | ECB patterns including SetComponentEnabled |
→ See dots-rpg | Core module — IsAlive, IsMoving, IsAttacking usage |