Repository: mlange-42/arche (go)

Issue:
Zero Entity singleton
`Entity` should have a zero value singleton, and possibly a method `Zero() bool`.

This is useforfor storing entity IDs with the possibility for zero/nil/unset.

Pull request:
Reserved Zero Entity
Fixes #20

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bacfc11..112d337 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 * Avoid allocation in `World.Has(entity, compID)` (#16)
 * `World.RemEntity(entity)` panics on dead entity, like all other `World` methods (#18)
+* Reserve zero value `Entity` to serve as nil/undefined value (#23)
 
 ## [[v0.1.0]](https://github.com/mlange-42/arche/tree/v0.1.0)
 
diff --git a/ecs/entity.go b/ecs/entity.go
index f5452d3..4a35091 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -11,6 +11,11 @@ func newEntity(id int) Entity {
 	return Entity{ID(id), 0}
 }
 
+// IsZero returns whether this entity is the reserved zero entity
+func (e Entity) IsZero() bool {
+	return e.id == 0
+}
+
 type entityIndex struct {
 	arch  int
 	index uint32
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index 091b792..1bd3f51 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -1,9 +1,11 @@
 package ecs
 
+import "math"
+
 // newEntityPool creates a new, initialized EntityPool
 func newEntityPool() entityPool {
 	return entityPool{
-		entities:  []Entity{},
+		entities:  []Entity{{0, math.MaxUint16}},
 		next:      0,
 		available: 0,
 	}
@@ -32,6 +34,9 @@ func (p *entityPool) Get() Entity {
 
 // Recycle hands an entity back for recycling
 func (p *entityPool) Recycle(e Entity) {
+	if e.id == 0 {
+		panic("can't recycle reserved zero entity")
+	}
 	p.entities[e.id].gen++
 	p.next, p.entities[e.id].id = e.id, p.next
 	p.available++
@@ -39,5 +44,5 @@ func (p *entityPool) Recycle(e Entity) {
 
 // Alive return whether an entity is still alive, based on the entity's generations
 func (p *entityPool) Alive(e Entity) bool {
-	return e.gen == p.entities[e.id].gen
+	return e.id != 0 && e.gen == p.entities[e.id].gen
 }
diff --git a/ecs/world.go b/ecs/world.go
index c863bc8..bfcd214 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -9,7 +9,7 @@ import (
 // NewWorld creates a new World
 func NewWorld() World {
 	return World{
-		entities:   []entityIndex{},
+		entities:   []entityIndex{{-1, 0}},
 		entityPool: newEntityPool(),
 		registry:   newComponentRegistry(),
 		archetypes: []archetype{newArchetype()},


Test patch:
diff --git a/ecs/entity_pool_test.go b/ecs/entity_pool_test.go
index 97fe4b2..a90ce7f 100644
--- a/ecs/entity_pool_test.go
+++ b/ecs/entity_pool_test.go
@@ -1,6 +1,7 @@
 package ecs
 
 import (
+	"math"
 	"math/rand"
 	"testing"
 
@@ -14,29 +15,32 @@ func TestEntityPoolConstructor(t *testing.T) {
 func TestEntityPool(t *testing.T) {
 	p := newEntityPool()
 
-	expectedAll := []Entity{newEntity(0), newEntity(1), newEntity(2), newEntity(3), newEntity(4)}
+	expectedAll := []Entity{newEntity(0), newEntity(1), newEntity(2), newEntity(3), newEntity(4), newEntity(5)}
+	expectedAll[0].gen = math.MaxUint16
 
 	for i := 0; i < 5; i++ {
 		_ = p.Get()
 	}
 	assert.Equal(t, expectedAll, p.entities, "Wrong initial entities")
 
-	e0 := p.entities[0]
+	assert.Panics(t, func() { p.Recycle(p.entities[0]) })
+
+	e0 := p.entities[1]
 	p.Recycle(e0)
 	assert.False(t, p.Alive(e0), "Dead entity should not be alive")
 
 	e0Old := e0
 	e0 = p.Get()
-	expectedAll[0].gen++
+	expectedAll[1].gen++
 	assert.True(t, p.Alive(e0), "Recycled entity of new generation should be alive")
 	assert.False(t, p.Alive(e0Old), "Recycled entity of old generation should not be alive")
 
 	assert.Equal(t, expectedAll, p.entities, "Wrong entities after get/recycle")
 
-	e0Old = p.entities[0]
+	e0Old = p.entities[1]
 	for i := 0; i < 5; i++ {
-		p.Recycle(p.entities[i])
-		expectedAll[i].gen++
+		p.Recycle(p.entities[i+1])
+		expectedAll[i+1].gen++
 	}
 
 	assert.False(t, p.Alive(e0Old), "Recycled entity of old generation should not be alive")
diff --git a/ecs/entity_test.go b/ecs/entity_test.go
index 0f89843..5b34311 100644
--- a/ecs/entity_test.go
+++ b/ecs/entity_test.go
@@ -2,6 +2,8 @@ package ecs
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestEntityAsIndex(t *testing.T) {
@@ -11,3 +13,8 @@ func TestEntityAsIndex(t *testing.T) {
 	val := arr[entity.id]
 	_ = val
 }
+
+func TestZeroEntity(t *testing.T) {
+	assert.True(t, Entity{}.IsZero())
+	assert.False(t, Entity{1, 0}.IsZero())
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index d3c60dc..b7c9048 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -9,33 +9,33 @@ import (
 func TestWorldEntites(t *testing.T) {
 	w := NewWorld()
 
-	assert.Equal(t, Entity{0, 0}, w.NewEntity())
 	assert.Equal(t, Entity{1, 0}, w.NewEntity())
 	assert.Equal(t, Entity{2, 0}, w.NewEntity())
+	assert.Equal(t, Entity{3, 0}, w.NewEntity())
 
 	assert.Equal(t, 0, int(w.entities[0].index))
-	assert.Equal(t, 1, int(w.entities[1].index))
-	assert.Equal(t, 2, int(w.entities[2].index))
-
-	w.RemEntity(Entity{1, 0})
-	assert.False(t, w.Alive(Entity{1, 0}))
-
-	assert.Equal(t, 0, int(w.entities[0].index))
+	assert.Equal(t, 0, int(w.entities[1].index))
 	assert.Equal(t, 1, int(w.entities[2].index))
+	assert.Equal(t, 2, int(w.entities[3].index))
+	w.RemEntity(Entity{2, 0})
+	assert.False(t, w.Alive(Entity{2, 0}))
 
-	assert.Equal(t, Entity{1, 1}, w.NewEntity())
-	assert.False(t, w.Alive(Entity{1, 0}))
-	assert.True(t, w.Alive(Entity{1, 1}))
+	assert.Equal(t, 0, int(w.entities[1].index))
+	assert.Equal(t, 1, int(w.entities[3].index))
 
-	assert.Equal(t, 2, int(w.entities[1].index))
+	assert.Equal(t, Entity{2, 1}, w.NewEntity())
+	assert.False(t, w.Alive(Entity{2, 0}))
+	assert.True(t, w.Alive(Entity{2, 1}))
 
-	w.RemEntity(Entity{2, 0})
-	w.RemEntity(Entity{1, 1})
-	w.RemEntity(Entity{0, 0})
+	assert.Equal(t, 2, int(w.entities[2].index))
+
+	w.RemEntity(Entity{3, 0})
+	w.RemEntity(Entity{2, 1})
+	w.RemEntity(Entity{1, 0})
 
-	assert.Panics(t, func() { w.RemEntity(Entity{2, 0}) })
-	assert.Panics(t, func() { w.RemEntity(Entity{1, 1}) })
-	assert.Panics(t, func() { w.RemEntity(Entity{0, 0}) })
+	assert.Panics(t, func() { w.RemEntity(Entity{3, 0}) })
+	assert.Panics(t, func() { w.RemEntity(Entity{2, 1}) })
+	assert.Panics(t, func() { w.RemEntity(Entity{1, 0}) })
 }
 
 func TestWorldComponents(t *testing.T) {