Repository: mlange-42/arche
Pull request #98 (2023-02-17 00:59:04)

Doc, naming and profiling tweaks
Renames `generic.Mutate` to `generic.Exchange`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e9f043..dc630fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@
 * Entities can be initialized with components, via ID as well as using generics (#76)
 * A listener function can be registered to the world, for notification on entity changes (#77)
 * Support for up to 128 distinct component types per world (was limited to 64 before) (#78)
-* Generic entity manipulation through types `Map1`, `Map2`, ... and `Mutate` (#79, #84, #87)
+* Generic entity manipulation through types `Map1`, `Map2`, ... and `Exchange` (#79, #84, #87)
 
 ### Other
 
diff --git a/README.md b/README.md
index 5940630..d20987d 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 [![GitHub](https://img.shields.io/badge/github-repo-blue?logo=github)](https://github.com/mlange-42/arche)
 [![MIT license](https://img.shields.io/github/license/mlange-42/arche)](https://github.com/mlange-42/arche/blob/main/LICENSE)
 
-*Arche* is an archetype-based Entity Component System for Go.
+*Arche* is an archetype-based [Entity Component System](https://en.wikipedia.org/wiki/Entity_component_system) for [Go](https://go.dev/).
 
 *Arche* is designed for the use in simulation models of the
 [Department for Ecological Modelling](https://www.ufz.de/index.php?en=34213) at the
@@ -163,10 +163,11 @@ In the illustration, the first archetype holds all components for all entities w
 The exact composition of each archetype is encoded in a bitmask for fast comparison.
 Thus, queries can easily identify their relevant archetypes (i.e. query bitmask contained in archetype bitmask), and then iterate entities linearly, and very fast. Components can be accessed through the query in a very efficient way.
 
-For getting components by entity ID, e.g. for hierarchies, the world contains a list that is indexed by the entity ID, and references the entity's archetype and index in the archetype. This way, getting components for entity IDs (i.e. random access) is fast, although not as fast as in queries.
+For getting components by entity ID, e.g. for hierarchies, the world contains a list that is indexed by the entity ID, and references the entity's archetype and index in the archetype. This way, getting components for entity IDs (i.e. random access) is fast, although not as fast as in queries (≈2ns vs. 1ns).
 
 Obviously, archetypes are an optimization for iteration speed.
 But they also come with a downside. Adding or removing components to/from an entity requires moving all the components of the entity to another archetype.
+This takes around 20ns per involved component.
 It is therefore recommended to add/remove/exchange multiple components at the same time rather than one after the other.
 
 ## Generic vs. ID access
diff --git a/benchmark/profile/iter/main.go b/benchmark/profile/iter/main.go
index 303a0c1..61dafce 100644
--- a/benchmark/profile/iter/main.go
+++ b/benchmark/profile/iter/main.go
@@ -52,7 +52,7 @@ func run(rounds, iters, entities int) {
 			query := world.Query(ecs.All(posID, rotID))
 			for query.Next() {
 				pos := (*position)(query.Get(posID))
-				_ = pos
+				pos.X = 1
 			}
 		}
 	}
diff --git a/benchmark/profile/world/main.go b/benchmark/profile/world/main.go
index ef72126..fa2571d 100644
--- a/benchmark/profile/world/main.go
+++ b/benchmark/profile/world/main.go
@@ -52,7 +52,7 @@ func run(rounds, iters, entityCount int) {
 		for j := 0; j < iters; j++ {
 			for _, e := range entities {
 				pos := (*position)(world.Get(e, posID))
-				_ = pos
+				pos.X = 1
 			}
 		}
 	}
diff --git a/ecs/bitmask.go b/ecs/bitmask.go
index 242d8ed..6f5e0cc 100644
--- a/ecs/bitmask.go
+++ b/ecs/bitmask.go
@@ -71,7 +71,7 @@ func (b BitMask) Contains(other BitMask) bool {
 	return b.Lo&other.Lo == other.Lo && b.Hi&other.Hi == other.Hi
 }
 
-// ContainsAny reports if any bit of other mask is a subset of this mask.
+// ContainsAny reports if any bit of other mask is in this mask.
 func (b BitMask) ContainsAny(other BitMask) bool {
 	return b.Lo&other.Lo != 0 || b.Hi&other.Hi != 0
 }
diff --git a/ecs/doc.go b/ecs/doc.go
index 52ceb1e..b3bc217 100644
--- a/ecs/doc.go
+++ b/ecs/doc.go
@@ -1,2 +1,4 @@
 // Package ecs contains Arche's core API.
+//
+// See the top level module [github.com/mlange-42/arche] for an overview.
 package ecs
diff --git a/ecs/query.go b/ecs/query.go
index dbd116e..638d183 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -51,23 +51,6 @@ func (f MaskPair) Matches(bits BitMask) bool {
 	return bits.Contains(f.Mask.BitMask) && (ex.IsZero() || !bits.ContainsAny(ex))
 }
 
-// QueryIter is the interface for iterable queries
-type QueryIter interface {
-	// Next proceeds to the next [Entity] in the Query.
-	Next() bool
-	// Has returns whether the current [Entity] has the given component
-	Has(comp ID) bool
-	// Get returns the pointer to the given component at the iterator's current [Entity]
-	Get(comp ID) unsafe.Pointer
-	// Entity returns the [Entity] at the iterator's position
-	Entity() Entity
-	// Close closes the Query and unlocks the world.
-	//
-	// Automatically called when iteration finishes.
-	// Needs to be called only if breaking out of the query iteration.
-	Close()
-}
-
 // Query is an advanced iterator to iterate entities.
 //
 // Create queries through the [World] using [World.Query].
diff --git a/ecs/world.go b/ecs/world.go
index efd6bd0..6c0e4c1 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -163,18 +163,21 @@ func (w *World) RemoveEntity(entity Entity) {
 
 // Query creates a [Query] iterator.
 //
-// Locks the world to prevent changes to component compositions.
+// The [ecs] core package provides only the filter [All] for querying the given components.
+// Further, it can be chained with [Mask.Without] (see the examples) to exclude components.
 //
-// # Example:
+// Example:
 //
-//	query := world.Query(All(idA, idB).Not(idC))
+//	query := world.Query(All(idA, idB).Without(idC))
 //	for query.Next() {
 //	    pos := (*position)(query.Get(posID))
 //	    pos.X += 1.0
 //	}
 //
-// For the use of generics for queries, see package [github.com/mlange-42/arche/generic].
+// For type-safe generics queries, see package [github.com/mlange-42/arche/generic].
 // For advanced filtering, see package [github.com/mlange-42/arche/filter].
+//
+// Locks the world to prevent changes to component compositions.
 func (w *World) Query(filter Filter) Query {
 	lock := w.bitPool.Get()
 	w.locks.Set(ID(lock), true)
diff --git a/filter/doc.go b/filter/doc.go
index f30f130..e0bc6d7 100644
--- a/filter/doc.go
+++ b/filter/doc.go
@@ -1,2 +1,4 @@
 // Package filter contains Arche's advanced logic filtering API.
+//
+// See the top level module [github.com/mlange-42/arche] for an overview.
 package filter
diff --git a/generic/doc.go b/generic/doc.go
index e5f7cb5..7a5d2e4 100644
--- a/generic/doc.go
+++ b/generic/doc.go
@@ -1,2 +1,4 @@
-// Package generic contains Arche's generic query API.
+// Package generic contains Arche's generic API.
+//
+// See the top level module [github.com/mlange-42/arche] for an overview.
 package generic
diff --git a/generic/exchange.go b/generic/exchange.go
new file mode 100644
index 0000000..7b8889a
--- /dev/null
+++ b/generic/exchange.go
@@ -0,0 +1,73 @@
+package generic
+
+import "github.com/mlange-42/arche/ecs"
+
+// Exchange is a helper for adding, removing and exchanging components.
+//
+// Set it up like this:
+//
+//	ex := NewExchange(&world).
+//	          Adds(T3[A, B, C]()...).
+//	          Removes(T2[D, E]()...)
+//
+// For only adding or removing components, see also [Map1], [Map2] etc.
+type Exchange struct {
+	add    []ecs.ID
+	remove []ecs.ID
+	world  *ecs.World
+}
+
+// NewExchange creates a new Exchange object.
+func NewExchange(w *ecs.World) *Exchange {
+	return &Exchange{
+		world: w,
+	}
+}
+
+// Adds sets components to add in calls to [Exchange.Add] and [Exchange.Exchange].
+//
+// Create the required mask items with [T].
+func (m *Exchange) Adds(add ...Comp) *Exchange {
+	m.add = toIds(m.world, add)
+	return m
+}
+
+// Removes sets components to remove in calls to [Exchange.Remove] and [Exchange.Exchange].
+//
+// Create the required mask items with [T].
+func (m *Exchange) Removes(remove ...Comp) *Exchange {
+	m.remove = toIds(m.world, remove)
+	return m
+}
+
+// NewEntity creates a new [ecs.Entity] with the components set via [Exchange.Adds].
+//
+// See also [ecs.World.NewEntity].
+func (m *Exchange) NewEntity() ecs.Entity {
+	entity := m.world.NewEntity(m.add...)
+	return entity
+}
+
+// Add the components set via [Exchange.Adds] to the given entity.
+//
+// See also [ecs.World.Add].
+func (m *Exchange) Add(entity ecs.Entity) {
+	m.world.Add(entity, m.add...)
+}
+
+// Remove the components set via [Exchange.Removes] from the given entity.
+//
+// See also [ecs.World.Remove].
+func (m *Exchange) Remove(entity ecs.Entity) {
+	m.world.Remove(entity, m.remove...)
+}
+
+// Exchange components on an entity.
+//
+// Removes the components set via [Exchange.Removes].
+// Adds the components set via [Exchange.Adds].
+//
+// See also [ecs.World.Exchange].
+func (m *Exchange) Exchange(entity ecs.Entity) {
+	m.world.Exchange(entity, m.add, m.remove)
+}
diff --git a/generic/generate/main.go b/generic/generate/main.go
index e064c5e..0376d7a 100644
--- a/generic/generate/main.go
+++ b/generic/generate/main.go
@@ -43,7 +43,7 @@ func generateMaps() {
 		panic(err)
 	}
 
-	mutate, err := template.ParseFiles("./generate/map.go.txt")
+	maps, err := template.ParseFiles("./generate/map.go.txt")
 	if err != nil {
 		panic(err)
 	}
@@ -87,7 +87,7 @@ func generateMaps() {
 			Components:  components,
 			Arguments:   arguments,
 		}
-		err = mutate.Execute(&text, data)
+		err = maps.Execute(&text, data)
 		if err != nil {
 			panic(err)
 		}
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index b7790df..31addc3 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -19,7 +19,7 @@ func (m *Map{{ .Index }}{{ .Types }}) Get(entity ecs.Entity) ({{ .TypesReturn }}
 	return {{ .ReturnAll }}
 }
 
-// NewEntity creates a new [ecs.Entity] with the Mutate{{ .Index }}'s components.
+// NewEntity creates a new [ecs.Entity] with the Map{{ .Index }}'s components.
 //
 // See also [ecs.World.NewEntity].
 func (m *Map{{ .Index }}{{ .Types }}) NewEntity() (ecs.Entity, {{ .TypesReturn }}) {
diff --git a/generic/mutate.go b/generic/mutate.go
deleted file mode 100644
index 936bb98..0000000
--- a/generic/mutate.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package generic
-
-import "github.com/mlange-42/arche/ecs"
-
-// Mutate is a helper for mutating components.
-type Mutate struct {
-	add    []ecs.ID
-	remove []ecs.ID
-	world  *ecs.World
-}
-
-// NewMutate creates a new Mutate object.
-func NewMutate(w *ecs.World) *Mutate {
-	return &Mutate{
-		world: w,
-	}
-}
-
-// Adds sets components to add in calls to [Mutate.Add] and [Mutate.Exchange].
-//
-// Create the required mask items with [T].
-func (m *Mutate) Adds(add ...Comp) *Mutate {
-	m.add = toIds(m.world, add)
-	return m
-}
-
-// Removes sets components to remove in calls to [Mutate.Remove] and [Mutate.Exchange].
-//
-// Create the required mask items with [T].
-func (m *Mutate) Removes(remove ...Comp) *Mutate {
-	m.remove = toIds(m.world, remove)
-	return m
-}
-
-// NewEntity creates a new [ecs.Entity] with the components set via [Mutate.ToAdd].
-//
-// See also [ecs.World.NewEntity].
-func (m *Mutate) NewEntity() ecs.Entity {
-	entity := m.world.NewEntity(m.add...)
-	return entity
-}
-
-// Add the components set via [Mutate.Adds] to the given entity.
-//
-// See also [ecs.World.Add].
-func (m *Mutate) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.add...)
-}
-
-// Remove the components set via [Mutate.Removes] from the given entity.
-//
-// See also [ecs.World.Remove].
-func (m *Mutate) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.remove...)
-}
-
-// Exchange components on an entity.
-//
-// Removes the components set via [Mutate.Removes].
-// Adds the components set via [Mutate.Adds].
-//
-// See also [ecs.World.Exchange].
-func (m *Mutate) Exchange(entity ecs.Entity) {
-	m.world.Exchange(entity, m.add, m.remove)
-}


Test patch:
diff --git a/ecs/query_test.go b/ecs/query_test.go
index f663423..5a60b01 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -94,14 +94,3 @@ func TestQuery(t *testing.T) {
 	}
 	assert.Equal(t, 1, cnt)
 }
-
-func TestInterface(t *testing.T) {
-	w := NewWorld()
-
-	posID := ComponentID[position](&w)
-	rotID := ComponentID[rotation](&w)
-
-	q := w.Query(All(posID, rotID))
-	var q2 QueryIter = &q
-	_ = q2
-}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 1a61f9d..641898e 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -557,7 +557,7 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSizeName[pagedArr32[archetype]]("PagedArr32")
 	printTypeSize[archetype]()
 	printTypeSize[storage]()
-	printTypeSize[QueryIter]()
+	printTypeSize[Query]()
 	printTypeSize[archetypeIter]()
 }
 
diff --git a/filter/filter_test.go b/filter/filter_test.go
index ee50c7d..b00a87f 100644
--- a/filter/filter_test.go
+++ b/filter/filter_test.go
@@ -195,7 +195,7 @@ func TestInterface(t *testing.T) {
 	posID := ecs.ComponentID[position](&w)
 
 	f := w.Query(All(posID))
-	var f2 ecs.QueryIter = &f
+	var f2 *ecs.Query = &f
 	_ = f2
 }
 
diff --git a/generic/mutate_test.go b/generic/exchange_test.go
similarity index 80%
rename from generic/mutate_test.go
rename to generic/exchange_test.go
index 6d98556..0d95b71 100644
--- a/generic/mutate_test.go
+++ b/generic/exchange_test.go
@@ -7,13 +7,13 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestMutate(t *testing.T) {
+func TestExchange(t *testing.T) {
 	w := ecs.NewWorld()
 	registerAll(&w)
 
-	mut := NewMutate(&w).Adds(T2[testStruct0, testStruct1]()...)
-	mutR := NewMutate(&w).Removes(T2[testStruct0, testStruct1]()...)
-	mutEx := NewMutate(&w).
+	mut := NewExchange(&w).Adds(T2[testStruct0, testStruct1]()...)
+	mutR := NewExchange(&w).Removes(T2[testStruct0, testStruct1]()...)
+	mutEx := NewExchange(&w).
 		Removes(T2[testStruct0, testStruct1]()...).
 		Adds(T2[testStruct2, testStruct3]()...)
 
diff --git a/generic/map_test.go b/generic/map_test.go
index c7fcc8c..f975c50 100644
--- a/generic/map_test.go
+++ b/generic/map_test.go
@@ -10,7 +10,7 @@ import (
 func TestGenericMap(t *testing.T) {
 	w := ecs.NewWorld()
 	get := NewMap[testStruct0](&w)
-	mut := NewMutate(&w).Adds(T[testStruct0]())
+	mut := NewExchange(&w).Adds(T[testStruct0]())
 
 	assert.Equal(t, ecs.ComponentID[testStruct0](&w), get.ID())