diff --git a/component.go b/component.go index af0866b..e62ebec 100644 --- a/component.go +++ b/component.go @@ -46,7 +46,7 @@ func ConfigureComponent[T ComponentInterface](world *World, conf any) T { // - the entity has the component // - an internal error occurs func AddComponent[T ComponentInterface](world *World, entityId EntityId, component T) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -76,7 +76,7 @@ func AddComponent[T ComponentInterface](world *World, entityId EntityId, compone // // This solution is faster than an atomic solution. func AddComponents2[A, B ComponentInterface](world *World, entityId EntityId, a A, b B) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -112,7 +112,7 @@ func addComponents2[A, B ComponentInterface](world *World, entityRecord entityRe // // This solution is faster than an atomic solution. func AddComponents3[A, B, C ComponentInterface](world *World, entityId EntityId, a A, b B, c C) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -150,7 +150,7 @@ func addComponents3[A, B, C ComponentInterface](world *World, entityRecord entit // // This solution is faster than an atomic solution. func AddComponents4[A, B, C, D ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -189,7 +189,7 @@ func addComponents4[A, B, C, D ComponentInterface](world *World, entityRecord en // // This solution is faster than an atomic solution. func AddComponents5[A, B, C, D, E ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -229,7 +229,7 @@ func addComponents5[A, B, C, D, E ComponentInterface](world *World, entityRecord // // This solution is faster than an atomic solution. func AddComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -270,7 +270,7 @@ func addComponents6[A, B, C, D, E, F ComponentInterface](world *World, entityRec // // This solution is faster than an atomic solution. func AddComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F, g G) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -312,7 +312,7 @@ func addComponents7[A, B, C, D, E, F, G ComponentInterface](world *World, entity // // This solution is faster than an atomic solution. func AddComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, entityId EntityId, a A, b B, c C, d D, e E, f F, g G, h H) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -354,7 +354,7 @@ func addComponents8[A, B, C, D, E, F, G, H ComponentInterface](world *World, ent // - the componentId is not registered in the World // - an internal error occurs func (world *World) AddComponent(entityId EntityId, componentId ComponentId, conf any) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -385,7 +385,7 @@ func (world *World) AddComponent(entityId EntityId, componentId ComponentId, con // - the componentsIds are not registered in the World // - an internal error occurs func (world *World) AddComponents(entityId EntityId, componentsIdsConfs ...ComponentIdConf) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -423,7 +423,7 @@ func RemoveComponent[T ComponentInterface](world *World, entityId EntityId) erro var t T componentId := t.GetComponentId() - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("entity %v does not exist", entityId) } entityRecord := world.entities[entityId] @@ -446,6 +446,9 @@ func RemoveComponent[T ComponentInterface](world *World, entityId EntityId) erro // - the entity does not have the component // - the ComponentId is not registered in the World func (world *World) RemoveComponent(entityId EntityId, componentId ComponentId) error { + if !world.Exists(entityId) { + return fmt.Errorf("entity %v does not exist", entityId) + } entityRecord := world.entities[entityId] if !world.hasComponents(entityRecord, componentId) { @@ -486,7 +489,7 @@ func removeComponent(world *World, s storage, entityRecord entityRecord, compone // // It returns false if at least one ComponentId is not owned. func (world *World) HasComponents(entityId EntityId, componentsIds ...ComponentId) bool { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return false } entityRecord := world.entities[entityId] @@ -509,7 +512,14 @@ func (world *World) hasComponents(entityRecord entityRecord, componentsIds ...Co // // If the entity does not have the component, it returns nil func GetComponent[T ComponentInterface](world *World, entityId EntityId) *T { + if !world.Exists(entityId) { + return nil + } + s := getStorage[T](world) + if s == nil { + return nil + } entityRecord := world.entities[entityId] if !s.hasArchetype(entityRecord.archetypeId) { @@ -527,6 +537,9 @@ func GetComponent[T ComponentInterface](world *World, entityId EntityId) *T { // - the ComponentId is not registered in the World // - the entity does not have the component func (world *World) GetComponent(entityId EntityId, componentId ComponentId) (any, error) { + if !world.Exists(entityId) { + return nil, fmt.Errorf("entity %v does not exist", entityId) + } entityRecord := world.entities[entityId] s, err := world.getStorageForComponentId(componentId) if err != nil { diff --git a/tag.go b/tag.go index ca351a5..ab1618a 100644 --- a/tag.go +++ b/tag.go @@ -19,6 +19,10 @@ func (world *World) AddTag(tagId TagId, entityId EntityId) error { return fmt.Errorf("the tagId %d is not allowed, it collides with Components Ids range [%d-%d]", tagId, COMPONENTS_INDICES, TAGS_INDICES) } + if !world.Exists(entityId) { + return fmt.Errorf("the entity %d does not exist", entityId) + } + if world.HasTag(tagId, entityId) { return fmt.Errorf("the entity %d already owns the tag %d", entityId, tagId) } @@ -37,7 +41,7 @@ func (world *World) AddTag(tagId TagId, entityId EntityId) error { // HasTag returns a boolean, to check if an EntityId owns a Tag. func (world *World) HasTag(tagId TagId, entityId EntityId) bool { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return false } entityRecord := world.entities[entityId] @@ -50,7 +54,7 @@ func (world *World) HasTag(tagId TagId, entityId EntityId) bool { // - The entity does not exists. // - The entity already owns the Tag. func (world *World) RemoveTag(tagId TagId, entityId EntityId) error { - if int(entityId) >= len(world.entities) { + if !world.Exists(entityId) { return fmt.Errorf("the entity %d does not exist", entityId) } entityRecord := world.entities[entityId] diff --git a/world.go b/world.go index e818a79..f1a244d 100644 --- a/world.go +++ b/world.go @@ -55,7 +55,7 @@ type World struct { func CreateWorld(initialCapacity int) *World { world := &World{ pool: pool{}, - entities: make(entities, initialCapacity), + entities: make(entities, 0, initialCapacity), archetypes: make([]archetype, 0, 1024), storage: make([]storage, TAGS_INDICES), entityAddedFn: func(entityId EntityId) {}, @@ -237,6 +237,12 @@ func (world *World) PublishEntity(entityId EntityId) { // // It calls the callback setted in SetEntityRemovedFn beforehand, so that the callback still has access to the data. func (world *World) RemoveEntity(entityId EntityId) { + // Reject unknown or already-removed entities, which also prevents a + // double-remove from corrupting an archetype (negative key indexing). + if !world.Exists(entityId) { + return + } + world.entityRemovedFn(entityId) entityRecord := world.entities[entityId] @@ -268,9 +274,21 @@ func (world *World) RemoveEntity(entityId EntityId) { world.archetypes[archetype.Id] = archetype } + // Tombstone the slot: a negative key marks the id as free until it is + // recycled, so Has/Get/Exists no longer report stale data for it. + world.entities[entityId].key = -1 world.pool.Recycle(entityId) } +// Exists reports whether entityId refers to a live entity of the World. +// +// It returns false for ids that were never created, or that have been removed +// and not yet recycled into a new entity. A negative key is the tombstone left +// behind by RemoveEntity. +func (world *World) Exists(entityId EntityId) bool { + return int(entityId) < len(world.entities) && world.entities[entityId].key >= 0 +} + // Count returns the number of entities in World. func (world *World) Count() int { return len(world.entities) - world.pool.Count() diff --git a/world_test.go b/world_test.go index 2e726f1..84c4661 100644 --- a/world_test.go +++ b/world_test.go @@ -267,8 +267,8 @@ func TestWorld_RemoveEntity(t *testing.T) { func TestWorld_Count(t *testing.T) { world := CreateWorld(1024) - if world.Count() != 1024 { - t.Errorf("world.Count should return 0 if the world is empty") + if world.Count() != 0 { + t.Errorf("world.Count should return 0 if the world is empty, got %d", world.Count()) } entities := make([]EntityId, TEST_ENTITY_NUMBER) @@ -281,3 +281,50 @@ func TestWorld_Count(t *testing.T) { t.Errorf("world.Count returned %d after inserting %d entities", world.Count(), TEST_ENTITY_NUMBER) } } + +// TestEntityLiveness verifies that removed (and not-yet-recycled) entities are +// reported as non-existent, instead of returning stale component/tag data. +func TestEntityLiveness(t *testing.T) { + world := CreateWorld(16) + RegisterComponent[testComponent1](world, &ComponentConfig[testComponent1]{}) + + e := world.CreateEntity() + if err := AddComponent[testComponent1](world, e, testComponent1{}); err != nil { + t.Fatalf("%s", err.Error()) + } + if !world.Exists(e) { + t.Fatal("freshly created entity should exist") + } + + world.RemoveEntity(e) + + if world.Exists(e) { + t.Fatal("removed entity should not exist") + } + if world.HasComponents(e, testComponent1Id) { + t.Fatal("removed entity should not report owning a component") + } + if GetComponent[testComponent1](world, e) != nil { + t.Fatal("removed entity should not return a component pointer") + } + if err := AddComponent[testComponent1](world, e, testComponent1{}); err == nil { + t.Fatal("adding a component to a removed entity should error") + } + if err := RemoveComponent[testComponent1](world, e); err == nil { + t.Fatal("removing a component from a removed entity should error") + } + + // Never-created ids (within and beyond the preallocated capacity) don't exist. + if world.Exists(999) { + t.Fatal("never-created id should not exist") + } + + // Double-remove must be a safe no-op. + world.RemoveEntity(e) + + // The id can be recycled into a fresh, live entity. + e2 := world.CreateEntity() + if !world.Exists(e2) { + t.Fatal("recycled entity should exist") + } +}