Repository: mlange-42/arche
Pull request #173 (2023-03-26 18:37:03)

Batch-remove methods return the number of removed entities
* Batch-remove methods return the number of removed entities
* Add batch operations example


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12d018a..3b396a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 * Generic mappers (`Map1`, ...) have methods `NewEntities`, `NewEntitiesWith` and `RemoveEntities` for batch operations (#151)
 * Batch-creation methods (ID-based and generic) have variants like `NewEntitiesQuery` that return a query over the created entities (#152)
 * Notification during batch-creation is delayed until the resp. query is closed (#157)
+* Match-remove methods (`RemoveEntities()`) return the number of removed entities (#173)
 
 ### Performance
 
@@ -34,6 +35,10 @@
 
 * Archetype storage buffers are "zeroed" when removing entities, to allow GC on pointers and slices in components (#147)
 
+### Documentation
+
+* Adds an example for batch-creation and batch-removal of entities (#173)
+
 ### Other
 
 * Restructure and extend benchmarks (#146, #153, #155, #156)
diff --git a/ecs/batch.go b/ecs/batch.go
index c9cd335..2c977e9 100644
--- a/ecs/batch.go
+++ b/ecs/batch.go
@@ -67,8 +67,10 @@ func (b *Batch) NewEntitiesWithQuery(count int, comps ...Component) Query {
 
 // RemoveEntities removes and recycles all entities matching a filter.
 //
+// Returns the number of removed entities.
+//
 // Panics when called on a locked world.
 // Do not use during [Query] iteration!
-func (b *Batch) RemoveEntities(filter Filter) {
-	b.world.removeEntities(filter)
+func (b *Batch) RemoveEntities(filter Filter) int {
+	return b.world.removeEntities(filter)
 }
diff --git a/ecs/world.go b/ecs/world.go
index 8e978eb..bcae634 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -255,13 +255,16 @@ func (w *World) RemoveEntity(entity Entity) {
 
 // removeEntities removes and recycles all entities matching a filter.
 //
+// Returns the number of removed entities.
+//
 // Panics when called on a locked world.
 // Do not use during [Query] iteration!
-func (w *World) removeEntities(filter Filter) {
+func (w *World) removeEntities(filter Filter) int {
 	w.checkLocked()
 
 	lock := w.lock()
 	numArches := w.archetypes.Len()
+	var count uintptr
 	for i := 0; i < numArches; i++ {
 		arch := w.archetypes.Get(i)
 
@@ -270,6 +273,8 @@ func (w *World) removeEntities(filter Filter) {
 		}
 
 		len := uintptr(arch.Len())
+		count += len
+
 		var j uintptr
 		for j = 0; j < len; j++ {
 			entity := arch.GetEntity(j)
@@ -283,6 +288,8 @@ func (w *World) removeEntities(filter Filter) {
 		arch.Reset()
 	}
 	w.unlock(lock)
+
+	return int(count)
 }
 
 // Alive reports whether an entity is still alive.
diff --git a/examples/batch_ops/main.go b/examples/batch_ops/main.go
new file mode 100644
index 0000000..146ea54
--- /dev/null
+++ b/examples/batch_ops/main.go
@@ -0,0 +1,80 @@
+// Demonstrates batch-creation and batch-removal of entities.
+//
+// Batch operations are an optimization for creating and removing many entities in one go.
+package main
+
+import (
+	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
+)
+
+// Position component
+type Position struct {
+	X float64
+	Y float64
+}
+
+// Velocity component
+type Velocity struct {
+	X float64
+	Y float64
+}
+
+func main() {
+	// Create a World.
+	world := ecs.NewWorld()
+
+	// Run the simulation
+	run(&world)
+	// Run the simulation using generic access
+	runGeneric(&world)
+}
+
+// Makes use of the resource by ID access.
+func run(w *ecs.World) {
+	// Get component IDs
+	posID := ecs.ComponentID[Position](w)
+	velID := ecs.ComponentID[Velocity](w)
+
+	// Batch-create entities with components
+	w.Batch().NewEntities(100, posID, velID)
+
+	// Batch-create entities with components, and iterate them
+	query := w.Batch().NewEntitiesQuery(100, posID, velID)
+	for query.Next() {
+		pos := (*Position)(query.Get(posID))
+		pos.X = 1.0
+		pos.Y = 1.0
+	}
+
+	// Batch-remove all entities with exactly the given components
+	filterExact := ecs.All(posID, velID).Exact()
+	w.Batch().RemoveEntities(&filterExact)
+
+	// Batch-remove all entities with the given components (and potentially further components)
+	filter := ecs.All(posID, velID)
+	w.Batch().RemoveEntities(&filter)
+}
+
+// Makes use of the resource by generic access.
+func runGeneric(w *ecs.World) {
+	// Get component mapper
+	mapper := generic.NewMap2[Position, Velocity](w)
+
+	// Batch-create entities using the mapper
+	mapper.NewEntities(100)
+
+	// Batch-create entities using the mapper, and iterate them
+	query := mapper.NewEntitiesQuery(100)
+	for query.Next() {
+		pos, _ := query.Get()
+		pos.X = 1.0
+		pos.Y = 1.0
+	}
+
+	// Batch-remove all entities with exactly the given components
+	mapper.RemoveEntities(true)
+
+	// Batch-remove all entities with the given components (and potentially further components)
+	mapper.RemoveEntities(true)
+}
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index ab2dfa8..90ac792 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -115,12 +115,13 @@ func (m *Map{{ .Index }}{{ .Types }}) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map{{ .Index }}'s components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map{{ .Index }}{{ .Types }}) RemoveEntities(exact bool) {
+func (m *Map{{ .Index }}{{ .Types }}) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
\ No newline at end of file
diff --git a/generic/map_generated.go b/generic/map_generated.go
index d219019..199db23 100644
--- a/generic/map_generated.go
+++ b/generic/map_generated.go
@@ -119,14 +119,15 @@ func (m *Map1[A]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map1's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map1[A]) RemoveEntities(exact bool) {
+func (m *Map1[A]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -253,14 +254,15 @@ func (m *Map2[A, B]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map2's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map2[A, B]) RemoveEntities(exact bool) {
+func (m *Map2[A, B]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -396,14 +398,15 @@ func (m *Map3[A, B, C]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map3's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map3[A, B, C]) RemoveEntities(exact bool) {
+func (m *Map3[A, B, C]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -548,14 +551,15 @@ func (m *Map4[A, B, C, D]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map4's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map4[A, B, C, D]) RemoveEntities(exact bool) {
+func (m *Map4[A, B, C, D]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -709,14 +713,15 @@ func (m *Map5[A, B, C, D, E]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map5's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map5[A, B, C, D, E]) RemoveEntities(exact bool) {
+func (m *Map5[A, B, C, D, E]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -879,14 +884,15 @@ func (m *Map6[A, B, C, D, E, F]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map6's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map6[A, B, C, D, E, F]) RemoveEntities(exact bool) {
+func (m *Map6[A, B, C, D, E, F]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -1058,14 +1064,15 @@ func (m *Map7[A, B, C, D, E, F, G]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map7's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map7[A, B, C, D, E, F, G]) RemoveEntities(exact bool) {
+func (m *Map7[A, B, C, D, E, F, G]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -1246,12 +1253,13 @@ func (m *Map8[A, B, C, D, E, F, G, H]) Remove(entity ecs.Entity) {
 
 // RemoveEntities removes all components from the world that match the Map8's components.
 //
+// Returns the number of removed entities.
+//
 // See also [ecs.World.NewEntityWith].
-func (m *Map8[A, B, C, D, E, F, G, H]) RemoveEntities(exact bool) {
+func (m *Map8[A, B, C, D, E, F, G, H]) RemoveEntities(exact bool) int {
 	if exact {
 		filter := m.mask.Exact()
-		m.world.Batch().RemoveEntities(&filter)
-	} else {
-		m.world.Batch().RemoveEntities(m.mask)
+		return m.world.Batch().RemoveEntities(&filter)
 	}
+	return m.world.Batch().RemoveEntities(m.mask)
 }


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index dd2ae3b..8c795f9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -75,6 +75,7 @@ jobs:
       - name: Run examples
         run: |
           go run ./examples/base
+          go run ./examples/batch_ops
           go run ./examples/change_listener
           go run ./examples/filter
           go run ./examples/generic
diff --git a/ecs/world_test.go b/ecs/world_test.go
index f6136f2..fc4e6b9 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -535,7 +535,8 @@ func TestWorldRemoveEntities(t *testing.T) {
 	query.Close()
 
 	filter := All(posID).Exact()
-	world.removeEntities(&filter)
+	cnt := world.removeEntities(&filter)
+	assert.Equal(t, 100, cnt)
 	assert.Equal(t, 300, len(events))
 
 	query = world.Query(All())
diff --git a/generic/map_generated_test.go b/generic/map_generated_test.go
index 4475847..a3d0331 100644
--- a/generic/map_generated_test.go
+++ b/generic/map_generated_test.go
@@ -42,8 +42,10 @@ func TestMap1Generated(t *testing.T) {
 	q.Close()
 	q = mut.NewEntitiesWithQuery(2, &testStruct0{})
 	q.Close()
-	mut.RemoveEntities(true)
-	mut.RemoveEntities(false)
+	cnt := mut.RemoveEntities(true)
+	assert.Equal(t, 11, cnt)
+	cnt = mut.RemoveEntities(false)
+	assert.Equal(t, 0, cnt)
 }
 
 func TestMap2Generated(t *testing.T) {