Skip to content

Inheritance

Inheritance lets an entity use another entity as a base for reusable component defaults. Components are owned by inheriting entities by default; components that should remain shared can opt into shared inheritance.

An entity can inherit from one base. That base must be marked with the built-in Abstract component.

ecs_entity_t base = ecs_new();
ecs_set(base, Health, { 100 });
ecs_set(base, Speed, { 4.0f });
ecs_add(base, Abstract);

Create the base with ecs_new(), add the default component values, then add Abstract.

Once an entity has Abstract, normal component add, set, and remove operations on that entity are rejected. Put component data on the base before marking it abstract.

Use a normal entity instead of an abstract one when you need to keep mutating its components directly.

Create another entity and link it to the abstract base with ecs_is_a():

ecs_entity_t instance = ecs_new();
ecs_is_a(instance, base);

ecs_is_a(instance, base) moves instance to an archetype table whose type records base as its inheritance target. Local component data already on the instance is kept.

An entity has one base at a time. Calling ecs_is_a() again with the same base is a no-op. Calling it with another abstract base replaces the inheritance target in the entity type and moves the entity to the matching archetype table.

The target must be alive, abstract, and different from the instance. Cycles are rejected, so an entity cannot inherit from itself or from one of its descendants.

Components use owned inheritance by default. If the base owns Health, linking an instance copies the effective value into the instance:

ecs_entity_t base = ecs_new();
ecs_set(base, Health, { 100 });
ecs_add(base, Abstract);
ecs_entity_t instance = ecs_new();
ecs_is_a(instance, base);

In this example, instance owns its copied Health. Later writes to the instance do not affect the base, and the base value is not synchronized back to the instance.

Use EcsInheritShared when a component should remain stored on the base:

ECS_COMPONENT_DEFINE(Material,
.inheritance = EcsInheritShared
);

For a native C++ component, use the component registration options:

ecs::component<Material>({
.inheritance = EcsInheritShared,
});

Add or set a component on the instance to override the inherited value:

ecs_set(instance, Health, { 75 });

After this, instance owns its own Health. Queries read the instance value instead of the base value, and writes affect only the instance:

const Health *base_health = ecs_get(base, Health);
const Health *instance_health = ecs_get(instance, Health);

Local components are independent from inherited components. Adding ecs_set(instance, Mana, { 20 }) does not change the base, and changing the base does not rewrite components already owned by instances.

Use ecs_is() to test inheritance:

if (ecs_is(instance, base)) {
/* instance inherits from base */
}

ecs_is() checks whether the entity inherits from the target, including transitive inheritance through intermediate bases.

Queries can match inherited components for read-only terms. A query with ecs_in(Health) matches an instance that inherits Health from its base:

ecs_query_id_t q = ecs_query({
.components = { ecs_in(Health) },
});
ecs_iter_t it = ecs_query_iter(q);
while (ecs_iter_next(&it)) {
const Health *health = ecs_field(&it, 0);
for (uint32_t i = 0; i < it.count; i++) {
if (ecs_field_is_shared(&it, 0)) {
/* health points to the base value, shared by the whole batch */
printf("%d\n", health->value);
} else {
/* health points to the entity-owned component array */
printf("%d\n", health[i].value);
}
}
}

When a component is configured as shared and inherited, ecs_field_kind(&it, index) returns EcsFieldShared. The pointer returned by ecs_field() points to the component stored on the base, not to an array with one element per entity.

When a field is owned by the matched entities, ecs_field_kind(&it, index) is EcsFieldOwned. The pointer is a normal component array indexed with i.

The inheritance rules for query fields are:

Term Inherited component behavior
ecs_in(T) Matches owned or inherited T; shared inherited fields are EcsFieldShared.
ecs_out(T) and ecs_inout(T) Match owned T, including components materialized through owned inheritance.
ecs_in_optional(T) May return an inherited shared field when T exists on a base.
ecs_inout_optional(T) Ignores shared inherited fields and returns NULL unless T is owned.

Writable terms do not match shared inherited data. A query with ecs_inout(Health) matches entities that own Health, including instances where Health was materialized through the default owned inheritance:

ecs_query_id_t q = ecs_query({
.components = { ecs_inout(Health) },
});

An instance configured with a shared Health inheritance is skipped by this query. An explicit ecs_set() also creates an instance-owned override and the query writes to that value.

Optional writable terms also ignore inherited shared values:

ecs_query_id_t q = ecs_query({
.components = { ecs_inout_optional(Health) },
});

For an entity that only inherits shared Health, this field is EcsFieldNone and ecs_field() returns NULL.

Queries can be restricted to entities that inherit from a base with ecs_query_desc_t.is_a:

ecs_query_id_t q = ecs_query({
.is_a = base,
.components = { ecs_in(Position) },
});

This matches entities that inherit from base and also satisfy the component terms. The base relationship is transitive: if Knight inherits from Player and Player inherits from Character, a query with .is_a = Character matches Knight.

Use inheritance for shared defaults, then override only the values that differ:

ecs_entity_t enemy = ecs_new();
ecs_set(enemy, Health, { 100 });
ecs_set(enemy, Speed, { 2.5f });
ecs_add(enemy, Abstract);
ecs_entity_t boss = ecs_new();
ecs_is_a(boss, enemy);
ecs_set(boss, Health, { 500 });

boss inherits Speed from enemy, but owns its own Health.