Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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) {
Expand Down Expand Up @@ -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]
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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]
Expand All @@ -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]
Expand Down
20 changes: 19 additions & 1 deletion world.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {},
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand Down
51 changes: 49 additions & 2 deletions world_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
}
}
Loading