Repository: mlange-42/arche
Pull request #140 (2023-03-22 11:45:57)

World reset
Implements `World.Reset()` for more efficient systematic simulations.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5c3bcb..9db259d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [[v0.5.2]](https://github.com/mlange-42/arche/compare/v0.5.1...v0.5.2)
+
+### Features
+
+* Adds method `World.Reset()`, to allow for more efficient systematic simulations (!138)
+
+### Performance
+
+* Speedup of archetype mask checks by 10% by checking mask before empty archetype (!139)
+
 ## [[v0.5.1]](https://github.com/mlange-42/arche/compare/v0.5.0...v0.5.1)
 
 ### Performance
diff --git a/ecs/archetype.go b/ecs/archetype.go
index c7ddc99..027c521 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -259,6 +259,13 @@ func (a *archetype) SetPointer(index uintptr, id ID, comp unsafe.Pointer) unsafe
 	return dst
 }
 
+// Reset removes all entities and components.
+//
+// Does NOT free the reserved memory.
+func (a *archetype) Reset() {
+	a.len = 0
+}
+
 // Components returns the component IDs for this archetype
 func (a *archetype) Components() []ID {
 	return a.Ids
diff --git a/ecs/bit_pool.go b/ecs/bit_pool.go
index d0721c5..f22cf85 100644
--- a/ecs/bit_pool.go
+++ b/ecs/bit_pool.go
@@ -1,7 +1,6 @@
 package ecs
 
 // bitPool is an entityPool implementation using implicit linked lists.
-// Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
 type bitPool struct {
 	bits      [MaskTotalBits]uint8
 	next      uint8
@@ -9,7 +8,7 @@ type bitPool struct {
 	available uint8
 }
 
-// newBitPool creates a new, initialized bit pool
+// newBitPool creates a new, initialized bit pool.
 func newBitPool() bitPool {
 	return bitPool{
 		bits:      [MaskTotalBits]uint8{},
@@ -19,7 +18,7 @@ func newBitPool() bitPool {
 	}
 }
 
-// Get returns a fresh or recycled bit
+// Get returns a fresh or recycled bit.
 func (p *bitPool) Get() uint8 {
 	if p.available == 0 {
 		if p.length >= MaskTotalBits {
@@ -36,8 +35,15 @@ func (p *bitPool) Get() uint8 {
 	return p.bits[curr]
 }
 
-// Recycle hands a bit back for recycling
+// Recycle hands a bit back for recycling.
 func (p *bitPool) Recycle(b uint8) {
 	p.next, p.bits[b] = b, p.next
 	p.available++
 }
+
+// Reset recycles all bits.
+func (p *bitPool) Reset() {
+	p.next = 0
+	p.length = 0
+	p.available = 0
+}
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index e8a55c6..cbb009f 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -4,7 +4,16 @@ import (
 	"math"
 )
 
-// newEntityPool creates a new, initialized Entity pool
+// entityPool is an implementation using implicit linked lists.
+// Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
+type entityPool struct {
+	entities          []Entity
+	next              eid
+	available         uint32
+	capacityIncrement int
+}
+
+// newEntityPool creates a new, initialized Entity pool.
 func newEntityPool(capacityIncrement int) entityPool {
 	entities := make([]Entity, 1, capacityIncrement)
 	entities[0] = Entity{0, math.MaxUint16}
@@ -16,16 +25,7 @@ func newEntityPool(capacityIncrement int) entityPool {
 	}
 }
 
-// entityPool is an implementation using implicit linked lists.
-// Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
-type entityPool struct {
-	entities          []Entity
-	next              eid
-	available         uint32
-	capacityIncrement int
-}
-
-// Get returns a fresh or recycled entity
+// Get returns a fresh or recycled entity.
 func (p *entityPool) Get() Entity {
 	if p.available == 0 {
 		e := newEntity(eid(len(p.entities)))
@@ -43,7 +43,7 @@ func (p *entityPool) Get() Entity {
 	return p.entities[curr]
 }
 
-// Recycle hands an entity back for recycling
+// Recycle hands an entity back for recycling.
 func (p *entityPool) Recycle(e Entity) {
 	if e.id == 0 {
 		panic("can't recycle reserved zero entity")
@@ -53,7 +53,14 @@ func (p *entityPool) Recycle(e Entity) {
 	p.available++
 }
 
-// Alive return whether an entity is still alive, based on the entity's generations
+// Reset recycles all entities. Does NOT free the reserved memory.
+func (p *entityPool) Reset() {
+	p.entities = p.entities[:1]
+	p.next = 0
+	p.available = 0
+}
+
+// Alive return whether an entity is still alive, based on the entity's generations.
 func (p *entityPool) Alive(e Entity) bool {
 	return e.id != 0 && e.gen == p.entities[e.id].gen
 }
diff --git a/ecs/resources.go b/ecs/resources.go
index 1c63434..c247a36 100644
--- a/ecs/resources.go
+++ b/ecs/resources.go
@@ -49,3 +49,10 @@ func (r *resources) Get(id ResID) interface{} {
 func (r *resources) Has(id ResID) bool {
 	return r.resources[id] != nil
 }
+
+// Reset removes all resources.
+func (r *resources) Reset() {
+	for i := 0; i < len(r.resources); i++ {
+		r.resources[i] = nil
+	}
+}
diff --git a/ecs/world.go b/ecs/world.go
index 76bf529..d92d8f4 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -372,6 +372,28 @@ func (w *World) HasResource(id ResID) bool {
 	return w.resources.Has(id)
 }
 
+// Reset removes all entities and resources as well as the listener from the world.
+//
+// Does NOT free reserved memory, remove archetypes, clear the registry etc.
+//
+// Can be used to run systematic simulations without the need to re-allocate memory for each run.
+// Accelerates re-populating the world by a factor of 2-3.
+func (w *World) Reset() {
+	w.checkLocked()
+
+	w.entities = w.entities[:1]
+	w.entityPool.Reset()
+	w.bitPool.Reset()
+	w.resources.Reset()
+
+	w.listener = nil
+
+	len := w.archetypes.Len()
+	for i := 0; i < len; i++ {
+		w.archetypes.Get(i).Reset()
+	}
+}
+
 // Query creates a [Query] iterator.
 //
 // The [ecs] core package provides only the filter [All] for querying the given components.


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 47045e3..eb13f43 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -117,6 +117,51 @@ func TestArchetypeAddGetSet(t *testing.T) {
 	assert.Equal(t, 0, int(a.Len()))
 }
 
+func TestArchetypeReset(t *testing.T) {
+	comps := []componentType{
+		{ID: 0, Type: reflect.TypeOf(position{})},
+		{ID: 1, Type: reflect.TypeOf(rotation{})},
+	}
+
+	arch := archetype{}
+	arch.Init(nil, 32, false, comps...)
+
+	arch.Add(
+		newEntity(0),
+		Component{ID: 0, Comp: &position{1, 2}},
+		Component{ID: 1, Comp: &rotation{3}},
+	)
+
+	arch.Add(
+		newEntity(1),
+		Component{ID: 0, Comp: &position{4, 5}},
+		Component{ID: 1, Comp: &rotation{6}},
+	)
+
+	assert.Equal(t, position{1, 2}, *(*position)(arch.Get(0, 0)))
+	assert.Equal(t, position{4, 5}, *(*position)(arch.Get(1, 0)))
+	assert.Equal(t, 2, int(arch.Len()))
+
+	arch.Reset()
+	assert.Equal(t, 0, int(arch.Len()))
+
+	arch.Add(
+		newEntity(0),
+		Component{ID: 0, Comp: &position{10, 20}},
+		Component{ID: 1, Comp: &rotation{3}},
+	)
+
+	arch.Add(
+		newEntity(1),
+		Component{ID: 0, Comp: &position{40, 50}},
+		Component{ID: 1, Comp: &rotation{6}},
+	)
+
+	assert.Equal(t, position{10, 20}, *(*position)(arch.Get(0, 0)))
+	assert.Equal(t, position{40, 50}, *(*position)(arch.Get(1, 0)))
+	assert.Equal(t, 2, int(arch.Len()))
+}
+
 func BenchmarkIterArchetype_1000(b *testing.B) {
 	b.StopTimer()
 	comps := []componentType{
diff --git a/ecs/bit_pool_test.go b/ecs/bit_pool_test.go
index abaa204..fe540cc 100644
--- a/ecs/bit_pool_test.go
+++ b/ecs/bit_pool_test.go
@@ -23,4 +23,19 @@ func TestBitPool(t *testing.T) {
 	}
 
 	assert.Panics(t, func() { p.Get() })
+
+	p.Reset()
+
+	for i := 0; i < MaskTotalBits; i++ {
+		assert.Equal(t, i, int(p.Get()))
+	}
+
+	assert.Panics(t, func() { p.Get() })
+
+	for i := 0; i < 10; i++ {
+		p.Recycle(uint8(i))
+	}
+	for i := 9; i >= 0; i-- {
+		assert.Equal(t, i, int(p.Get()))
+	}
 }
diff --git a/ecs/entity_pool_test.go b/ecs/entity_pool_test.go
index 84f2875..58e5a17 100644
--- a/ecs/entity_pool_test.go
+++ b/ecs/entity_pool_test.go
@@ -53,14 +53,19 @@ func TestEntityPool(t *testing.T) {
 }
 
 func TestEntityPoolStochastic(t *testing.T) {
+	p := newEntityPool(128)
+
 	for i := 0; i < 10; i++ {
-		p := newEntityPool(128)
+		p.Reset()
+		assert.Equal(t, 0, p.Len())
+		assert.Equal(t, 0, p.Available())
 
 		alive := map[Entity]bool{}
 		for i := 0; i < 10; i++ {
 			e := p.Get()
 			alive[e] = true
 		}
+
 		for e, isAlive := range alive {
 			assert.Equal(t, isAlive, p.Alive(e), "Wrong alive state of entity %v after initialization", e)
 			if rand.Float32() > 0.75 {
diff --git a/ecs/resources_test.go b/ecs/resources_test.go
index 2a3c3bd..5ef1bf9 100644
--- a/ecs/resources_test.go
+++ b/ecs/resources_test.go
@@ -35,3 +35,37 @@ func TestResources(t *testing.T) {
 	assert.False(t, res.Has(rotID))
 	assert.Panics(t, func() { res.Remove(rotID) })
 }
+
+func TestResourcesReset(t *testing.T) {
+	res := newResources()
+
+	posID := res.registry.ComponentID(reflect.TypeOf(position{}))
+	rotID := res.registry.ComponentID(reflect.TypeOf(rotation{}))
+
+	res.Add(posID, &position{1, 2})
+	res.Add(rotID, &rotation{5})
+
+	pos, ok := res.Get(posID).(*position)
+	assert.True(t, ok)
+	assert.Equal(t, position{1, 2}, *pos)
+
+	rot, ok := res.Get(rotID).(*rotation)
+	assert.True(t, ok)
+	assert.Equal(t, rotation{5}, *rot)
+
+	res.Reset()
+
+	assert.False(t, res.Has(posID))
+	assert.False(t, res.Has(rotID))
+
+	res.Add(posID, &position{10, 20})
+	res.Add(rotID, &rotation{50})
+
+	pos, ok = res.Get(posID).(*position)
+	assert.True(t, ok)
+	assert.Equal(t, position{10, 20}, *pos)
+
+	rot, ok = res.Get(rotID).(*rotation)
+	assert.True(t, ok)
+	assert.Equal(t, rotation{50}, *rot)
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index c801a67..5454c2f 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -441,6 +441,41 @@ func TestRegisterComponents(t *testing.T) {
 	assert.Equal(t, ID(1), ComponentID[rotation](&world))
 }
 
+func TestWorldReset(t *testing.T) {
+	world := NewWorld()
+
+	world.SetListener(func(e EntityEvent) {})
+
+	posID := ComponentID[position](&world)
+	velID := ComponentID[velocity](&world)
+
+	world.NewEntity(posID)
+	world.NewEntity(velID)
+	world.NewEntity(posID, velID)
+	world.NewEntity(posID, velID)
+
+	AddResource(&world, &rotation{100})
+
+	world.Reset()
+
+	assert.Equal(t, 0, int(world.archetypes.Get(0).Len()))
+	assert.Equal(t, 0, world.entityPool.Len())
+	assert.Equal(t, 1, len(world.entities))
+	assert.Nil(t, world.listener)
+
+	query := world.Query(All())
+	assert.Equal(t, 0, query.Count())
+	query.Close()
+
+	e1 := world.NewEntity(posID)
+	e2 := world.NewEntity(velID)
+	world.NewEntity(posID, velID)
+	world.NewEntity(posID, velID)
+
+	assert.Equal(t, Entity{1, 0}, e1)
+	assert.Equal(t, Entity{2, 0}, e2)
+}
+
 func TestArchetypeGraph(t *testing.T) {
 	world := NewWorld()
 
@@ -659,3 +694,36 @@ func BenchmarkGetResourceShortcut(b *testing.B) {
 
 	_ = res
 }
+
+func BenchmarkAdd_10_000_New(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		world := NewWorld(NewConfig().WithCapacityIncrement(1024))
+
+		posID := ComponentID[position](&world)
+		velID := ComponentID[velocity](&world)
+
+		for i := 0; i < 10000; i++ {
+			_ = world.NewEntity(posID, velID)
+		}
+	}
+}
+
+func BenchmarkAdd_10_000_Reset(b *testing.B) {
+	b.StopTimer()
+	world := NewWorld(NewConfig().WithCapacityIncrement(1024))
+
+	posID := ComponentID[position](&world)
+	velID := ComponentID[velocity](&world)
+
+	for i := 0; i < 10000; i++ {
+		_ = world.NewEntity(posID, velID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		world.Reset()
+		for i := 0; i < 10000; i++ {
+			_ = world.NewEntity(posID, velID)
+		}
+	}
+}