Repository: mlange-42/arche
Pull request #47 (2023-02-09 20:50:13)

Generic queries etc.
Introduces generic variants of queries and world methods. Provides protection from users casting unsafe pointers to the wrong type.

Interestingly, generics have no or negigible performance impact when running the benchmarks locally.
In the GitHub CI, however, access time doubles for generics compared to ID-based access.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd6acc3..800577d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,17 @@
 
 ## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.2.0...main)
 
+### Features
+
+* Generic queries, for type safety and ergonomics (#47)
+* Generic add, assign and remove, for type safety and ergonomics (#47)
+* Generic get and has, for ergonomics but slow (#47)
+
 ### Other
 
 * Use of an archetype graph to speed up finding the target archetype for component addition/removal (#42)
 * Reduced dependencies by moving profiling and benchmarking to sub-modules (#46)
+* Smaller integer type for component identifiers (#47)
 
 ## [[v0.2.0]](https://github.com/mlange-42/arche/compare/v0.1.4...v0.2.0)
 
diff --git a/README.md b/README.md
index 856f20e..611e907 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,6 @@
 [Department for Ecological Modelling](https://www.ufz.de/index.php?en=34213) at the
 [Helmholtz Centre for Environmental Research](https://www.ufz.de).
 
-:warning: This project is in an early stage of development! :warning:
-
 ## Installations
 
 ```shell
@@ -22,11 +20,11 @@ go get github.com/mlange-42/arche
 ## Features
 
 * Minimal API. See the [API docs](https://pkg.go.dev/github.com/mlange-42/arche).
-* Very fast iteration and component access via `Query` (benchmarks for comparison in progress).
+* Fast iteration and component access via queries (≈2.5ns iterate + get).
 * Fast random access for components of arbitrary entities. Useful for hierarchies.
 * No systems. Use your own structure.
 * Not thread-safe. On purpose.
-* No dependencies (except for unit tests).
+* No dependencies. Except for unit tests (100% coverage).
 
 ## Usage example
 
@@ -60,22 +58,12 @@ func main() {
 	// Create a World.
 	world := ecs.NewWorld()
 
-	// Get component IDs.
-	// Registers component type if not already registered.
-	positionID := ecs.ComponentID[Position](&world)
-	velocityID := ecs.ComponentID[Velocity](&world)
-
 	// Create entities
 	for i := 0; i < 1000; i++ {
 		// Create a new Entity.
 		entity := world.NewEntity()
 		// Add components to it.
-		world.Add(entity, positionID, velocityID)
-
-		// Component access through the World.
-		// See below for faster access in queries.
-		pos := (*Position)(world.Get(entity, positionID))
-		vel := (*Velocity)(world.Get(entity, velocityID))
+		pos, vel := ecs.Add2[Position, Velocity](&world, entity)
 
 		// Initialize component fields.
 		pos.X = rand.Float64() * 100
@@ -88,15 +76,13 @@ func main() {
 	// Time loop.
 	for t := 0; t < 1000; t++ {
 		// Get a fresh query.
-		query := world.Query(positionID, velocityID)
+		// Generic queries support up to 8 components.
+		// For more components, use World.Query()
+		query := ecs.Query2[Position, Velocity](&world)
 		// Iterate it
 		for query.Next() {
 			// Component access through a Query.
-			// About 20-30% faster than access through the World.
-			// Can also fetch components not in the query.
-			pos := (*Position)(query.Get(positionID))
-			vel := (*Velocity)(query.Get(velocityID))
-
+			pos, vel := query.GetAll()
 			// Update component fields.
 			pos.X += vel.X
 			pos.Y += vel.Y
diff --git a/ecs/entity.go b/ecs/entity.go
index 8b51491..889ae49 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -5,12 +5,12 @@ package ecs
 //
 // Entities should only be created via the [World], using [World.NewEntity].
 type Entity struct {
-	id  ID
+	id  eid
 	gen uint16
 }
 
-func newEntity(id int) Entity {
-	return Entity{ID(id), 0}
+func newEntity(id eid) Entity {
+	return Entity{id, 0}
 }
 
 // IsZero returns whether this entity is the reserved zero entity.
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index 770bba7..021d45a 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -16,7 +16,7 @@ func newEntityPool(capacityIncrement int) entityPool {
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
 type entityPool struct {
 	entities          []Entity
-	next              ID
+	next              eid
 	available         uint32
 	capacityIncrement int
 }
@@ -24,7 +24,7 @@ type entityPool struct {
 // Get returns a fresh or recycled entity
 func (p *entityPool) Get() Entity {
 	if p.available == 0 {
-		e := newEntity(len(p.entities))
+		e := newEntity(eid(len(p.entities)))
 		if len(p.entities) == cap(p.entities) {
 			old := p.entities
 			p.entities = make([]Entity, len(p.entities), len(p.entities)+p.capacityIncrement)
diff --git a/ecs/generic.go b/ecs/generic.go
new file mode 100644
index 0000000..106bf2b
--- /dev/null
+++ b/ecs/generic.go
@@ -0,0 +1,182 @@
+package ecs
+
+import "reflect"
+
+// ComponentID returns the ID for a component type. Registers the type if it is not already registered.
+func ComponentID[T any](w *World) ID {
+	tp := reflect.TypeOf((*T)(nil)).Elem()
+	return w.componentID(tp)
+}
+
+// Getter provides a type-safe way to access components by entity ID.
+//
+// Create one with [NewGetter].
+type Getter[T any] struct {
+	id    ID
+	world *World
+}
+
+// NewGetter creates a new [Getter] for a component type.
+//
+// Getter provides a type-safe way to access components by entity ID.
+//
+// See also [World.Get] and [World.Has].
+func NewGetter[T any](w *World) Getter[T] {
+	return Getter[T]{
+		id:    ComponentID[T](w),
+		world: w,
+	}
+}
+
+func (g *Getter[T]) Get(entity Entity) *T {
+	return (*T)(g.world.Get(entity, g.id))
+}
+
+func (g *Getter[T]) Has(entity Entity) bool {
+	return g.world.Has(entity, g.id)
+}
+
+// Add adds a component type to an entity.
+//
+// See also [World.Add].
+func Add[A any](w *World, entity Entity) *A {
+	id := ComponentID[A](w)
+	w.Add(entity, id)
+	return (*A)(w.Get(entity, id))
+}
+
+// Add2 adds two component types to an entity.
+//
+// See also [World.Add].
+func Add2[A any, B any](w *World, entity Entity) (*A, *B) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	w.Add(entity, idA, idB)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
+}
+
+// Add3 adds three component types to an entity.
+//
+// See also [World.Add].
+func Add3[A any, B any, C any](w *World, entity Entity) (*A, *B, *C) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	w.Add(entity, idA, idB, idC)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
+}
+
+// Add4 adds four component types to an entity.
+//
+// See also [World.Add].
+func Add4[A any, B any, C any, D any](w *World, entity Entity) (*A, *B, *C, *D) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	idD := ComponentID[D](w)
+	w.Add(entity, idA, idB, idC, idD)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
+}
+
+// Add5 adds five component types to an entity.
+//
+// See also [World.Add].
+func Add5[A any, B any, C any, D any, E any](w *World, entity Entity) (*A, *B, *C, *D, *E) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	idD := ComponentID[D](w)
+	idE := ComponentID[E](w)
+	w.Add(entity, idA, idB, idC, idD, idE)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
+}
+
+// Assign adds a components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign[A any](w *World, entity Entity, a *A) *A {
+	idA := ComponentID[A](w)
+	w.Assign(entity, idA, a)
+	return (*A)(w.Get(entity, idA))
+}
+
+// Assign2 adds two components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign2[A any, B any](w *World, entity Entity, a *A, b *B) (*A, *B) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	w.AssignN(entity, Component{idA, a}, Component{idB, b})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
+}
+
+// Assign3 adds three components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign3[A any, B any, C any](w *World, entity Entity, a *A, b *B, c *C) (*A, *B, *C) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
+}
+
+// Assign4 adds four components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign4[A any, B any, C any, D any](w *World, entity Entity, a *A, b *B, c *C, d *D) (*A, *B, *C, *D) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	idD := ComponentID[D](w)
+	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c}, Component{idD, d})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
+}
+
+// Assign5 adds four components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign5[A any, B any, C any, D any, E any](w *World, entity Entity, a *A, b *B, c *C, d *D, e *E) (*A, *B, *C, *D, *E) {
+	idA := ComponentID[A](w)
+	idB := ComponentID[B](w)
+	idC := ComponentID[C](w)
+	idD := ComponentID[D](w)
+	idE := ComponentID[E](w)
+	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c}, Component{idD, d}, Component{idE, e})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
+}
+
+// Remove removes a component from an entity.
+//
+// See also [World.Remove].
+func Remove[A any](w *World, entity Entity) {
+	w.Remove(entity, ComponentID[A](w))
+}
+
+// Remove2 removes two components from an entity.
+//
+// See also [World.Remove].
+func Remove2[A any, B any](w *World, entity Entity) {
+	w.Remove(entity, ComponentID[A](w), ComponentID[B](w))
+}
+
+// Remove3 removes three components from an entity.
+//
+// See also [World.Remove].
+func Remove3[A any, B any, C any](w *World, entity Entity) {
+	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w))
+}
+
+// Remove4 removes four components from an entity.
+//
+// See also [World.Remove].
+func Remove4[A any, B any, C any, D any](w *World, entity Entity) {
+	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w))
+}
+
+// Remove5 removes five components from an entity.
+//
+// See also [World.Remove].
+func Remove5[A any, B any, C any, D any, E any](w *World, entity Entity) {
+	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w), ComponentID[E](w))
+}
diff --git a/ecs/query.go b/ecs/query.go
index 7959ad4..2c8ffd8 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -7,6 +7,7 @@ import (
 // Query is an iterator to iterate entities.
 //
 // Create queries through the [World] using [World.Query].
+// See also the generic variants [Query1], [Query2], [Query3], ...
 //
 // # Example:
 //
diff --git a/ecs/query_generic.go b/ecs/query_generic.go
new file mode 100644
index 0000000..7e87c5a
--- /dev/null
+++ b/ecs/query_generic.go
@@ -0,0 +1,418 @@
+package ecs
+
+// Q1 is a generic query for one component.
+//
+// Create one with [Query1]
+type Q1[A any] struct {
+	Query
+	id ID
+}
+
+// Query1 creates a generic query for one component.
+//
+// See also [World.Query].
+func Query1[A any](w *World) Q1[A] {
+	id := ComponentID[A](w)
+	return Q1[A]{
+		Query: w.Query(id),
+		id:    id,
+	}
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q1[A]) Get1() *A {
+	return (*A)(q.Query.Get(q.id))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q2 is a generic query for two components.
+//
+// Create one with [Query2]
+type Q2[A any, B any] struct {
+	Query
+	ids [2]ID
+}
+
+// Query2 creates a generic query for two components.
+//
+// See also [World.Query].
+func Query2[A any, B any](w *World) Q2[A, B] {
+	ids := [2]ID{ComponentID[A](w), ComponentID[B](w)}
+	return Q2[A, B]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q2[A, B]) GetAll() (*A, *B) {
+	return (*A)(q.Query.Get(q.ids[0])), (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q2[A, B]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q2[A, B]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q3 is a generic query for three components.
+//
+// Create one with [Query3]
+type Q3[A any, B any, C any] struct {
+	Query
+	ids [3]ID
+}
+
+// Query3 creates a generic query for three components.
+//
+// See also [World.Query].
+func Query3[A any, B any, C any](w *World) Q3[A, B, C] {
+	ids := [3]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w)}
+	return Q3[A, B, C]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q3[A, B, C]) GetAll() (*A, *B, *C) {
+	return (*A)(q.Query.Get(q.ids[0])), (*B)(q.Query.Get(q.ids[1])), (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q3[A, B, C]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q3[A, B, C]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q3[A, B, C]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q4 is a generic query for four components.
+//
+// Create one with [Query4]
+type Q4[A any, B any, C any, D any] struct {
+	Query
+	ids [4]ID
+}
+
+// Query4 creates a generic query for four components.
+//
+// See also [World.Query].
+func Query4[A any, B any, C any, D any](w *World) Q4[A, B, C, D] {
+	ids := [4]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w)}
+	return Q4[A, B, C, D]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q4[A, B, C, D]) GetAll() (*A, *B, *C, *D) {
+	return (*A)(q.Query.Get(q.ids[0])),
+		(*B)(q.Query.Get(q.ids[1])),
+		(*C)(q.Query.Get(q.ids[2])),
+		(*D)(q.Query.Get(q.ids[3]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q4[A, B, C, D]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q4[A, B, C, D]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q4[A, B, C, D]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get4 returns the fourth queried component for the current query position
+func (q *Q4[A, B, C, D]) Get4() *D {
+	return (*D)(q.Query.Get(q.ids[3]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q5 is a generic query for five components.
+//
+// Create one with [Query5]
+type Q5[A any, B any, C any, D any, E any] struct {
+	Query
+	ids [5]ID
+}
+
+// Query5 creates a generic query for five components.
+//
+// See also [World.Query].
+func Query5[A any, B any, C any, D any, E any](w *World) Q5[A, B, C, D, E] {
+	ids := [5]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w), ComponentID[E](w)}
+	return Q5[A, B, C, D, E]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q5[A, B, C, D, E]) GetAll() (*A, *B, *C, *D, *E) {
+	return (*A)(q.Query.Get(q.ids[0])),
+		(*B)(q.Query.Get(q.ids[1])),
+		(*C)(q.Query.Get(q.ids[2])),
+		(*D)(q.Query.Get(q.ids[3])),
+		(*E)(q.Query.Get(q.ids[4]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q5[A, B, C, D, E]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q5[A, B, C, D, E]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q5[A, B, C, D, E]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get4 returns the fourth queried component for the current query position
+func (q *Q5[A, B, C, D, E]) Get4() *D {
+	return (*D)(q.Query.Get(q.ids[3]))
+}
+
+// Get5 returns the fifth queried component for the current query position
+func (q *Q5[A, B, C, D, E]) Get5() *E {
+	return (*E)(q.Query.Get(q.ids[4]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q6 is a generic query for six components.
+//
+// Create one with [Query6]
+type Q6[A any, B any, C any, D any, E any, F any] struct {
+	Query
+	ids [6]ID
+}
+
+// Query6 creates a generic query for six components.
+//
+// See also [World.Query].
+func Query6[A any, B any, C any, D any, E any, F any](w *World) Q6[A, B, C, D, E, F] {
+	ids := [6]ID{
+		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
+		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
+	}
+	return Q6[A, B, C, D, E, F]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) GetAll() (*A, *B, *C, *D, *E, *F) {
+	return (*A)(q.Query.Get(q.ids[0])),
+		(*B)(q.Query.Get(q.ids[1])),
+		(*C)(q.Query.Get(q.ids[2])),
+		(*D)(q.Query.Get(q.ids[3])),
+		(*E)(q.Query.Get(q.ids[4])),
+		(*F)(q.Query.Get(q.ids[5]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get4 returns the fourth queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get4() *D {
+	return (*D)(q.Query.Get(q.ids[3]))
+}
+
+// Get5 returns the fifth queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get5() *E {
+	return (*E)(q.Query.Get(q.ids[4]))
+}
+
+// Get6 returns the sixth queried component for the current query position
+func (q *Q6[A, B, C, D, E, F]) Get6() *F {
+	return (*F)(q.Query.Get(q.ids[5]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q7 is a generic query for seven components.
+//
+// Create one with [Query7]
+type Q7[A any, B any, C any, D any, E any, F any, G any] struct {
+	Query
+	ids [7]ID
+}
+
+// Query7 creates a generic query for seven components.
+//
+// See also [World.Query].
+func Query7[A any, B any, C any, D any, E any, F any, G any](w *World) Q7[A, B, C, D, E, F, G] {
+	ids := [7]ID{
+		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
+		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
+		ComponentID[G](w),
+	}
+	return Q7[A, B, C, D, E, F, G]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) GetAll() (*A, *B, *C, *D, *E, *F, *G) {
+	return (*A)(q.Query.Get(q.ids[0])),
+		(*B)(q.Query.Get(q.ids[1])),
+		(*C)(q.Query.Get(q.ids[2])),
+		(*D)(q.Query.Get(q.ids[3])),
+		(*E)(q.Query.Get(q.ids[4])),
+		(*F)(q.Query.Get(q.ids[5])),
+		(*G)(q.Query.Get(q.ids[6]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get4 returns the fourth queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get4() *D {
+	return (*D)(q.Query.Get(q.ids[3]))
+}
+
+// Get5 returns the fifth queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get5() *E {
+	return (*E)(q.Query.Get(q.ids[4]))
+}
+
+// Get6 returns the sixth queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get6() *F {
+	return (*F)(q.Query.Get(q.ids[5]))
+}
+
+// Get7 returns the seventh queried component for the current query position
+func (q *Q7[A, B, C, D, E, F, G]) Get7() *G {
+	return (*G)(q.Query.Get(q.ids[6]))
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+// Q8 is a generic query for seven components.
+//
+// Create one with [Query8]
+type Q8[A any, B any, C any, D any, E any, F any, G any, H any] struct {
+	Query
+	ids [8]ID
+}
+
+// Query8 creates a generic query for seven components.
+//
+// See also [World.Query].
+func Query8[A any, B any, C any, D any, E any, F any, G any, H any](w *World) Q8[A, B, C, D, E, F, G, H] {
+	ids := [8]ID{
+		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
+		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
+		ComponentID[G](w), ComponentID[H](w),
+	}
+	return Q8[A, B, C, D, E, F, G, H]{
+		Query: w.Query(ids[:]...),
+		ids:   ids,
+	}
+}
+
+// GetAll returns all queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) GetAll() (*A, *B, *C, *D, *E, *F, *G, *H) {
+	return (*A)(q.Query.Get(q.ids[0])),
+		(*B)(q.Query.Get(q.ids[1])),
+		(*C)(q.Query.Get(q.ids[2])),
+		(*D)(q.Query.Get(q.ids[3])),
+		(*E)(q.Query.Get(q.ids[4])),
+		(*F)(q.Query.Get(q.ids[5])),
+		(*G)(q.Query.Get(q.ids[6])),
+		(*H)(q.Query.Get(q.ids[7]))
+}
+
+// Get1 returns the first queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get1() *A {
+	return (*A)(q.Query.Get(q.ids[0]))
+}
+
+// Get2 returns the second queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get2() *B {
+	return (*B)(q.Query.Get(q.ids[1]))
+}
+
+// Get3 returns the third queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get3() *C {
+	return (*C)(q.Query.Get(q.ids[2]))
+}
+
+// Get4 returns the fourth queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get4() *D {
+	return (*D)(q.Query.Get(q.ids[3]))
+}
+
+// Get5 returns the fifth queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get5() *E {
+	return (*E)(q.Query.Get(q.ids[4]))
+}
+
+// Get6 returns the sixth queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get6() *F {
+	return (*F)(q.Query.Get(q.ids[5]))
+}
+
+// Get7 returns the seventh queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get7() *G {
+	return (*G)(q.Query.Get(q.ids[6]))
+}
+
+// Get8 returns the eighth queried component for the current query position
+func (q *Q8[A, B, C, D, E, F, G, H]) Get8() *H {
+	return (*H)(q.Query.Get(q.ids[7]))
+}
diff --git a/ecs/types.go b/ecs/types.go
index fb5fab1..6ef441d 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -5,8 +5,11 @@ import (
 	"unsafe"
 )
 
+// eid is the entity identifier/index type
+type eid uint32
+
 // ID is the component identifier type
-type ID uint32
+type ID uint8
 
 // Component is a Component ID/Component pointer pair
 type Component struct {
diff --git a/ecs/world.go b/ecs/world.go
index 335e0e4..34edc5c 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -5,12 +5,6 @@ import (
 	"unsafe"
 )
 
-// ComponentID returns the ID for a component type. Registers the type if it is not already registered.
-func ComponentID[T any](w *World) ID {
-	tp := reflect.TypeOf((*T)(nil)).Elem()
-	return w.componentID(tp)
-}
-
 // NewWorld creates a new [World]
 func NewWorld() World {
 	return FromConfig(NewConfig())
@@ -88,10 +82,29 @@ func (w *World) RemEntity(entity Entity) {
 	w.entities[entity.id].arch = nil
 }
 
+// Query creates a [Query] iterator for the given components.
+//
+// Locks the world to prevent changes to component compositions.
+//
+// See also the generic variants [Query1], [Query2], [Query3], ...
+func (w *World) Query(comps ...ID) Query {
+	mask := newMask(comps...)
+	lock := w.bitPool.Get()
+	w.locks.Set(ID(lock), true)
+	return newQuery(w, mask, lock)
+}
+
+// Alive reports whether an entity is still alive.
+func (w *World) Alive(entity Entity) bool {
+	return w.entityPool.Alive(entity)
+}
+
 // Get returns a pointer th the given component of an [Entity].
 //
 // Returns `nil` if the entity has no such component.
 // Panics when called for an already removed entity.
+//
+// See also [Getter]/[NewGetter].
 func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	index := w.entities[entity.id]
 	arch := index.arch
@@ -106,6 +119,8 @@ func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 // Has returns whether an [Entity] has a given component.
 //
 // Panics when called for an already removed entity.
+//
+// See also [Getter]/[NewGetter].
 func (w *World) Has(entity Entity, comp ID) bool {
 	index := w.entities[entity.id]
 	return index.arch.HasComponent(comp)
@@ -115,6 +130,8 @@ func (w *World) Has(entity Entity, comp ID) bool {
 //
 // Panics when called on a locked world or for an already removed entity.
 // Do not use during [Query] iteration!
+//
+// See also the generic variants [Add], [Add2], [Add3], ...
 func (w *World) Add(entity Entity, comps ...ID) {
 	w.Exchange(entity, comps, []ID{})
 }
@@ -128,6 +145,8 @@ func (w *World) Add(entity Entity, comps ...ID) {
 //
 // Panics when called on a locked world or for an already removed entity.
 // Do not use during [Query] iteration!
+//
+// See also the generic variants [Assign], [Assign2], [Assign3], ...
 func (w *World) Assign(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 	w.Exchange(entity, []ID{id}, []ID{})
 	return w.copyTo(entity, id, comp)
@@ -141,6 +160,8 @@ func (w *World) Assign(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 //
 // Panics when called on a locked world or for an already removed entity.
 // Do not use during [Query] iteration!
+//
+// See also the generic variants [Assign], [Assign2], [Assign3], ...
 func (w *World) AssignN(entity Entity, comps ...Component) {
 	ids := make([]ID, len(comps))
 	for i, c := range comps {
@@ -157,6 +178,8 @@ func (w *World) AssignN(entity Entity, comps ...Component) {
 // Panics when called on a locked world or for an already removed entity.
 //
 // Do not use during [Query] iteration!
+//
+// See also the generic variants [Remove], [Remove2], [Remove3], ...
 func (w *World) Remove(entity Entity, comps ...ID) {
 	w.Exchange(entity, []ID{}, comps)
 }
@@ -222,6 +245,11 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.entities[entity.id] = entityIndex{arch, newIndex}
 }
 
+// IsLocked returns whether the world is locked by any queries.
+func (w *World) IsLocked() bool {
+	return w.locks != 0
+}
+
 func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 	if !w.Has(entity, id) {
 		panic("can't copy component into entity that has no such component type")
@@ -295,11 +323,6 @@ func (w *World) createArchetype(mask bitMask) *archetype {
 	return arch
 }
 
-// Alive reports whether an entity is still alive.
-func (w *World) Alive(entity Entity) bool {
-	return w.entityPool.Alive(entity)
-}
-
 // componentID returns the ID for a component type, and registers it if not already registered.
 func (w *World) componentID(tp reflect.Type) ID {
 	return w.registry.ComponentID(tp)
@@ -319,16 +342,6 @@ func (w *World) nextArchetype(mask bitMask, index int) (int, archetypeIter, bool
 	return len, archetypeIter{}, false
 }
 
-// Query creates a [Query] iterator for the given components.
-//
-// Locks the world to prevent changes to component compositions.
-func (w *World) Query(comps ...ID) Query {
-	mask := newMask(comps...)
-	lock := w.bitPool.Get()
-	w.locks.Set(ID(lock), true)
-	return newQuery(w, mask, lock)
-}
-
 // closeQuery closes a query and unlocks the world
 func (w *World) closeQuery(query *Query) {
 	l := query.lockBit
@@ -339,11 +352,6 @@ func (w *World) closeQuery(query *Query) {
 	w.bitPool.Recycle(l)
 }
 
-// IsLocked returns whether the world is locked by any queries.
-func (w *World) IsLocked() bool {
-	return w.locks != 0
-}
-
 func (w *World) checkLocked() {
 	if w.locks != 0 {
 		panic("attempt to modify a locked world")


Test patch:
diff --git a/benchmark/add/arche_test.go b/benchmark/add/arche_test.go
index 79728b8..52518bd 100644
--- a/benchmark/add/arche_test.go
+++ b/benchmark/add/arche_test.go
@@ -22,6 +22,19 @@ func addArcheWorld(b *testing.B, count int) {
 	}
 }
 
+func addArcheGeneric(b *testing.B, count int) {
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		world := ecs.NewWorld()
+		b.StartTimer()
+
+		for i := 0; i < count; i++ {
+			entity := world.NewEntity()
+			ecs.Add[position](&world, entity)
+		}
+	}
+}
+
 func BenchmarkArcheAdd_100(b *testing.B) {
 	addArcheWorld(b, 100)
 }
@@ -33,3 +46,15 @@ func BenchmarkArcheAdd_1000(b *testing.B) {
 func BenchmarkArcheAdd_10000(b *testing.B) {
 	addArcheWorld(b, 10000)
 }
+
+func BenchmarkArcheAddGeneric_100(b *testing.B) {
+	addArcheGeneric(b, 100)
+}
+
+func BenchmarkArcheAddGeneric_1000(b *testing.B) {
+	addArcheGeneric(b, 1000)
+}
+
+func BenchmarkArcheAddGeneric_10000(b *testing.B) {
+	addArcheGeneric(b, 10000)
+}
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index 930de6f..ac2a73b 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -30,6 +30,87 @@ func runArcheQuery(b *testing.B, count int) {
 	}
 }
 
+func runArcheQueryGeneric(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[position](&world)
+	rotID := ecs.ComponentID[rotation](&world)
+
+	for i := 0; i < count; i++ {
+		entity := world.NewEntity()
+		world.Add(entity, posID, rotID)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		query := ecs.Query2[position, rotation](&world)
+		b.StartTimer()
+		for query.Next() {
+			pos := query.Get1()
+			_ = pos
+		}
+	}
+}
+func runArcheQuery5C(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	id0 := ecs.ComponentID[testStruct0](&world)
+	id1 := ecs.ComponentID[testStruct1](&world)
+	id2 := ecs.ComponentID[testStruct2](&world)
+	id3 := ecs.ComponentID[testStruct3](&world)
+	id4 := ecs.ComponentID[testStruct4](&world)
+
+	for i := 0; i < count; i++ {
+		entity := world.NewEntity()
+		world.Add(entity, id0, id1, id2, id3, id4)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		query := world.Query(id0, id1, id2, id3, id4)
+		b.StartTimer()
+		for query.Next() {
+			t1 := (*testStruct0)(query.Get(id0))
+			t2 := (*testStruct0)(query.Get(id1))
+			t3 := (*testStruct0)(query.Get(id2))
+			t4 := (*testStruct0)(query.Get(id3))
+			t5 := (*testStruct0)(query.Get(id4))
+			_, _, _, _, _ = t1, t2, t3, t4, t5
+		}
+	}
+}
+
+func runArcheQueryGeneric5C(b *testing.B, count int) {
+	b.StopTimer()
+	world := ecs.NewWorld()
+
+	id0 := ecs.ComponentID[testStruct0](&world)
+	id1 := ecs.ComponentID[testStruct1](&world)
+	id2 := ecs.ComponentID[testStruct2](&world)
+	id3 := ecs.ComponentID[testStruct3](&world)
+	id4 := ecs.ComponentID[testStruct4](&world)
+
+	for i := 0; i < count; i++ {
+		entity := world.NewEntity()
+		world.Add(entity, id0, id1, id2, id3, id4)
+	}
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		query := ecs.Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&world)
+		b.StartTimer()
+		for query.Next() {
+			t1, t2, t3, t4, t5 := query.GetAll()
+			_, _, _, _, _ = t1, t2, t3, t4, t5
+		}
+	}
+}
+
 func runArcheQuery1kArch(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
@@ -66,7 +147,7 @@ func runArcheQuery1kArch(b *testing.B, count int) {
 	}
 }
 
-func runArcheQueryEntity(b *testing.B, count int) {
+func runArcheWorld(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
 
@@ -84,19 +165,22 @@ func runArcheQueryEntity(b *testing.B, count int) {
 		query := world.Query(posID, rotID)
 		b.StartTimer()
 		for query.Next() {
-			e := query.Entity()
-			_ = e
+			entity := query.Entity()
+			pos := (*position)(world.Get(entity, posID))
+			_ = pos
 		}
 	}
 }
 
-func runArcheWorld(b *testing.B, count int) {
+func runArcheWorldGeneric(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
 
 	posID := ecs.ComponentID[position](&world)
 	rotID := ecs.ComponentID[rotation](&world)
 
+	get := ecs.NewGetter[position](&world)
+
 	for i := 0; i < count; i++ {
 		entity := world.NewEntity()
 		world.Add(entity, posID, rotID)
@@ -109,56 +193,92 @@ func runArcheWorld(b *testing.B, count int) {
 		b.StartTimer()
 		for query.Next() {
 			entity := query.Entity()
-			pos := (*position)(world.Get(entity, posID))
+			pos := get.Get(entity)
 			_ = pos
 		}
 	}
 }
 
-func BenchmarkArcheIterQuery_1_000(b *testing.B) {
+func BenchmarkArcheIterQueryID_1_000(b *testing.B) {
 	runArcheQuery(b, 1000)
 }
 
-func BenchmarkArcheIterQuery_10_000(b *testing.B) {
+func BenchmarkArcheIterQueryID_10_000(b *testing.B) {
 	runArcheQuery(b, 10000)
 }
 
-func BenchmarkArcheIterQuery_100_000(b *testing.B) {
+func BenchmarkArcheIterQueryID_100_000(b *testing.B) {
 	runArcheQuery(b, 100000)
 }
 
-func BenchmarkArcheIterQueryEntity_1_000(b *testing.B) {
-	runArcheQueryEntity(b, 1000)
+func BenchmarkArcheIterQueryGeneric_1_000(b *testing.B) {
+	runArcheQueryGeneric(b, 1000)
+}
+
+func BenchmarkArcheIterQueryGeneric_10_000(b *testing.B) {
+	runArcheQueryGeneric(b, 10000)
+}
+
+func BenchmarkArcheIterQueryGeneric_100_000(b *testing.B) {
+	runArcheQueryGeneric(b, 100000)
 }
 
-func BenchmarkArcheIterQueryEntity_10_000(b *testing.B) {
-	runArcheQueryEntity(b, 10000)
+func BenchmarkArcheIterQueryID_5C_1_000(b *testing.B) {
+	runArcheQuery5C(b, 1000)
 }
 
-func BenchmarkArcheIterQueryEntity_100_000(b *testing.B) {
-	runArcheQueryEntity(b, 100000)
+func BenchmarkArcheIterQueryID_5C_10_000(b *testing.B) {
+	runArcheQuery5C(b, 10000)
 }
 
-func BenchmarkArcheIterWorld_1_000(b *testing.B) {
+func BenchmarkArcheIterQueryID_5C_100_000(b *testing.B) {
+	runArcheQuery5C(b, 100000)
+}
+
+func BenchmarkArcheIterQueryGeneric_5C_1_000(b *testing.B) {
+	runArcheQueryGeneric5C(b, 1000)
+}
+
+func BenchmarkArcheIterQueryGeneric_5C_10_000(b *testing.B) {
+	runArcheQueryGeneric5C(b, 10000)
+}
+
+func BenchmarkArcheIterQueryGeneric_5C_100_000(b *testing.B) {
+	runArcheQueryGeneric5C(b, 100000)
+}
+
+func BenchmarkArcheIterWorldID_1_000(b *testing.B) {
 	runArcheWorld(b, 1000)
 }
 
-func BenchmarkArcheIterWorld_10_000(b *testing.B) {
+func BenchmarkArcheIterWorldID_10_000(b *testing.B) {
 	runArcheWorld(b, 10000)
 }
 
-func BenchmarkArcheIterWorld_100_000(b *testing.B) {
+func BenchmarkArcheIterWorldID_100_000(b *testing.B) {
 	runArcheWorld(b, 100000)
 }
 
-func BenchmarkArcheIter1kArch_1_000(b *testing.B) {
+func BenchmarkArcheIterWorldGeneric_1_000(b *testing.B) {
+	runArcheWorldGeneric(b, 1000)
+}
+
+func BenchmarkArcheIterWorldGeneric_10_000(b *testing.B) {
+	runArcheWorldGeneric(b, 10000)
+}
+
+func BenchmarkArcheIterWorldGeneric_100_000(b *testing.B) {
+	runArcheWorldGeneric(b, 100000)
+}
+
+func BenchmarkArcheIter1kArchID_1_000(b *testing.B) {
 	runArcheQuery1kArch(b, 1000)
 }
 
-func BenchmarkArcheIter1kArch_10_000(b *testing.B) {
+func BenchmarkArcheIter1kArchID_10_000(b *testing.B) {
 	runArcheQuery1kArch(b, 10000)
 }
 
-func BenchmarkArcheIter1kArch_100_000(b *testing.B) {
+func BenchmarkArcheIter1kArchID_100_000(b *testing.B) {
 	runArcheQuery1kArch(b, 100000)
 }
diff --git a/benchmark/iterate/ggecs_test.go b/benchmark/iterate/ggecs_test.go
index 4cdcf8d..4f60c7c 100644
--- a/benchmark/iterate/ggecs_test.go
+++ b/benchmark/iterate/ggecs_test.go
@@ -30,6 +30,37 @@ func runGameEngineEcs(b *testing.B, count int) {
 	}
 }
 
+func runGameEngineEcs5C(b *testing.B, count int) {
+	b.StopTimer()
+	comps := []ecs.ComponentConfig{
+		{ID: 0, Component: testStruct0{}},
+		{ID: 1, Component: testStruct1{}},
+		{ID: 2, Component: testStruct2{}},
+		{ID: 3, Component: testStruct3{}},
+		{ID: 4, Component: testStruct4{}},
+	}
+	world := ecs.NewWorld(comps...)
+
+	for i := 0; i < count; i++ {
+		entity := world.NewEntity()
+		world.Assign(entity, 0, 1, 2, 3, 4)
+	}
+	filter := world.NewFilter(0, 1, 2, 3, 4)
+
+	b.StartTimer()
+
+	for i := 0; i < b.N; i++ {
+		for _, e := range filter.Entities() {
+			t1 := (*testStruct0)(world.Component(e, 0))
+			t2 := (*testStruct1)(world.Component(e, 1))
+			t3 := (*testStruct2)(world.Component(e, 2))
+			t4 := (*testStruct3)(world.Component(e, 3))
+			t5 := (*testStruct4)(world.Component(e, 4))
+			_, _, _, _, _ = t1, t2, t3, t4, t5
+		}
+	}
+}
+
 func runGameEngineEcs1kArch(b *testing.B, count int) {
 	b.StopTimer()
 	comps := []ecs.ComponentConfig{
@@ -88,6 +119,18 @@ func BenchmarkGGEcsIter_100_000(b *testing.B) {
 	runGameEngineEcs(b, 100000)
 }
 
+func BenchmarkGGEcsIter5C_1_000(b *testing.B) {
+	runGameEngineEcs5C(b, 1000)
+}
+
+func BenchmarkGGEcsIter5C_10_000(b *testing.B) {
+	runGameEngineEcs5C(b, 10000)
+}
+
+func BenchmarkGGEcsIter5C_100_000(b *testing.B) {
+	runGameEngineEcs5C(b, 100000)
+}
+
 func BenchmarkGGEcsIter1kArch_1_000(b *testing.B) {
 	runGameEngineEcs1kArch(b, 1000)
 }
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 9b27cca..53e2049 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -101,7 +101,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(
-			newEntity(i),
+			newEntity(eid(i)),
 			Component{ID: 0, Component: &testStruct{}},
 		)
 	}
diff --git a/ecs/generic_test.go b/ecs/generic_test.go
new file mode 100644
index 0000000..4daf4be
--- /dev/null
+++ b/ecs/generic_test.go
@@ -0,0 +1,50 @@
+package ecs
+
+import (
+	"testing"
+)
+
+func TestGenericAddRemove(t *testing.T) {
+	w := NewWorld()
+	get := NewGetter[testStruct0](&w)
+
+	e0 := w.NewEntity()
+
+	Add[testStruct0](&w, e0)
+	_ = get.Has(e0)
+	_ = get.Get(e0)
+	Remove[testStruct0](&w, e0)
+
+	Add2[testStruct0, testStruct1](&w, e0)
+	Remove2[testStruct0, testStruct1](&w, e0)
+
+	Add3[testStruct0, testStruct1, testStruct2](&w, e0)
+	Remove3[testStruct0, testStruct1, testStruct2](&w, e0)
+
+	Add4[testStruct0, testStruct1, testStruct2, testStruct3](&w, e0)
+	Remove4[testStruct0, testStruct1, testStruct2, testStruct3](&w, e0)
+
+	Add5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&w, e0)
+	Remove5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&w, e0)
+}
+
+func TestGenericAssignRemove(t *testing.T) {
+	w := NewWorld()
+
+	e0 := w.NewEntity()
+
+	Assign(&w, e0, &testStruct0{})
+	Remove[testStruct0](&w, e0)
+
+	Assign2(&w, e0, &testStruct0{}, &testStruct1{})
+	Remove2[testStruct0, testStruct1](&w, e0)
+
+	Assign3(&w, e0, &testStruct0{}, &testStruct1{}, &testStruct2{})
+	Remove3[testStruct0, testStruct1, testStruct2](&w, e0)
+
+	Assign4(&w, e0, &testStruct0{}, &testStruct1{}, &testStruct2{}, &testStruct3{})
+	Remove4[testStruct0, testStruct1, testStruct2, testStruct3](&w, e0)
+
+	Assign5(&w, e0, &testStruct0{}, &testStruct1{}, &testStruct2{}, &testStruct3{}, &testStruct4{})
+	Remove5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&w, e0)
+}
diff --git a/ecs/query_generic_test.go b/ecs/query_generic_test.go
new file mode 100644
index 0000000..a347b4a
--- /dev/null
+++ b/ecs/query_generic_test.go
@@ -0,0 +1,395 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestQuery1(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	cnt := 0
+	query := Query1[testStruct0](&w)
+	for query.Next() {
+		c0 := query.Get1()
+		assert.Equal(t, cnt+1, int(c0.val))
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery2(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	cnt := 0
+	query := Query2[testStruct0, testStruct1](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+
+		c12, c22 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery3(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	cnt := 0
+	query := Query3[testStruct0, testStruct1, testStruct2](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+
+		c12, c22, c32 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery4(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+	id3 := ComponentID[testStruct3](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	w.Assign(e0, id3, &testStruct3{4})
+	w.Assign(e1, id3, &testStruct3{5})
+
+	cnt := 0
+	query := Query4[testStruct0, testStruct1, testStruct2, testStruct3](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		c4 := query.Get4()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+		assert.Equal(t, cnt+4, int(c4.val))
+
+		c12, c22, c32, c42 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		assert.Equal(t, c4, c42)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery5(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+	id3 := ComponentID[testStruct3](&w)
+	id4 := ComponentID[testStruct4](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	w.Assign(e0, id3, &testStruct3{4})
+	w.Assign(e1, id3, &testStruct3{5})
+
+	w.Assign(e0, id4, &testStruct4{5})
+	w.Assign(e1, id4, &testStruct4{6})
+
+	cnt := 0
+	query := Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		c4 := query.Get4()
+		c5 := query.Get5()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+		assert.Equal(t, cnt+4, int(c4.val))
+		assert.Equal(t, cnt+5, int(c5.val))
+
+		c12, c22, c32, c42, c52 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		assert.Equal(t, c4, c42)
+		assert.Equal(t, c5, c52)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery6(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+	id3 := ComponentID[testStruct3](&w)
+	id4 := ComponentID[testStruct4](&w)
+	id5 := ComponentID[testStruct5](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	w.Assign(e0, id3, &testStruct3{4})
+	w.Assign(e1, id3, &testStruct3{5})
+
+	w.Assign(e0, id4, &testStruct4{5})
+	w.Assign(e1, id4, &testStruct4{6})
+
+	w.Assign(e0, id5, &testStruct5{6})
+	w.Assign(e1, id5, &testStruct5{7})
+
+	cnt := 0
+	query := Query6[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		c4 := query.Get4()
+		c5 := query.Get5()
+		c6 := query.Get6()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+		assert.Equal(t, cnt+4, int(c4.val))
+		assert.Equal(t, cnt+5, int(c5.val))
+		assert.Equal(t, cnt+6, int(c6.val))
+
+		c12, c22, c32, c42, c52, c62 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		assert.Equal(t, c4, c42)
+		assert.Equal(t, c5, c52)
+		assert.Equal(t, c6, c62)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery7(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+	id3 := ComponentID[testStruct3](&w)
+	id4 := ComponentID[testStruct4](&w)
+	id5 := ComponentID[testStruct5](&w)
+	id6 := ComponentID[testStruct6](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	w.Assign(e0, id3, &testStruct3{4})
+	w.Assign(e1, id3, &testStruct3{5})
+
+	w.Assign(e0, id4, &testStruct4{5})
+	w.Assign(e1, id4, &testStruct4{6})
+
+	w.Assign(e0, id5, &testStruct5{6})
+	w.Assign(e1, id5, &testStruct5{7})
+
+	w.Assign(e0, id6, &testStruct5{7})
+	w.Assign(e1, id6, &testStruct5{8})
+
+	cnt := 0
+	query := Query7[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5, testStruct6](&w)
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		c4 := query.Get4()
+		c5 := query.Get5()
+		c6 := query.Get6()
+		c7 := query.Get7()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+		assert.Equal(t, cnt+4, int(c4.val))
+		assert.Equal(t, cnt+5, int(c5.val))
+		assert.Equal(t, cnt+6, int(c6.val))
+		assert.Equal(t, cnt+7, int(c7.val))
+
+		c12, c22, c32, c42, c52, c62, c72 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		assert.Equal(t, c4, c42)
+		assert.Equal(t, c5, c52)
+		assert.Equal(t, c6, c62)
+		assert.Equal(t, c7, c72)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
+
+func TestQuery8(t *testing.T) {
+	w := NewWorld()
+
+	id0 := ComponentID[testStruct0](&w)
+	id1 := ComponentID[testStruct1](&w)
+	id2 := ComponentID[testStruct2](&w)
+	id3 := ComponentID[testStruct3](&w)
+	id4 := ComponentID[testStruct4](&w)
+	id5 := ComponentID[testStruct5](&w)
+	id6 := ComponentID[testStruct6](&w)
+	id7 := ComponentID[testStruct7](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	w.Assign(e0, id0, &testStruct0{1})
+	w.Assign(e1, id0, &testStruct0{2})
+
+	w.Assign(e0, id1, &testStruct1{2})
+	w.Assign(e1, id1, &testStruct1{3})
+
+	w.Assign(e0, id2, &testStruct2{3})
+	w.Assign(e1, id2, &testStruct2{4})
+
+	w.Assign(e0, id3, &testStruct3{4})
+	w.Assign(e1, id3, &testStruct3{5})
+
+	w.Assign(e0, id4, &testStruct4{5})
+	w.Assign(e1, id4, &testStruct4{6})
+
+	w.Assign(e0, id5, &testStruct5{6})
+	w.Assign(e1, id5, &testStruct5{7})
+
+	w.Assign(e0, id6, &testStruct5{7})
+	w.Assign(e1, id6, &testStruct5{8})
+
+	w.Assign(e0, id7, &testStruct5{8})
+	w.Assign(e1, id7, &testStruct5{9})
+
+	cnt := 0
+	query := Query8[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7](&w)
+
+	for query.Next() {
+		c1 := query.Get1()
+		c2 := query.Get2()
+		c3 := query.Get3()
+		c4 := query.Get4()
+		c5 := query.Get5()
+		c6 := query.Get6()
+		c7 := query.Get7()
+		c8 := query.Get8()
+		assert.Equal(t, cnt+1, int(c1.val))
+		assert.Equal(t, cnt+2, int(c2.val))
+		assert.Equal(t, cnt+3, int(c3.val))
+		assert.Equal(t, cnt+4, int(c4.val))
+		assert.Equal(t, cnt+5, int(c5.val))
+		assert.Equal(t, cnt+6, int(c6.val))
+		assert.Equal(t, cnt+7, int(c7.val))
+		assert.Equal(t, cnt+8, int(c8.val))
+
+		c12, c22, c32, c42, c52, c62, c72, c82 := query.GetAll()
+		assert.Equal(t, c1, c12)
+		assert.Equal(t, c2, c22)
+		assert.Equal(t, c3, c32)
+		assert.Equal(t, c4, c42)
+		assert.Equal(t, c5, c52)
+		assert.Equal(t, c6, c62)
+		assert.Equal(t, c7, c72)
+		assert.Equal(t, c8, c82)
+		cnt++
+	}
+	assert.Equal(t, 2, cnt)
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index e90b65b..ac104cd 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -95,6 +95,10 @@ func TestWorldComponents(t *testing.T) {
 	// No-op add/remove
 	w.Add(e0)
 	w.Remove(e0)
+
+	w.RemEntity(e0)
+	assert.Panics(t, func() { w.Has(Entity{1, 0}, posID) })
+	assert.Panics(t, func() { w.Get(Entity{1, 0}, posID) })
 }
 
 func TestWorldLabels(t *testing.T) {
@@ -386,6 +390,8 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSize[storage]()
 	printTypeSize[Query]()
 	printTypeSize[archetypeIter]()
+	printTypeSizeName[Q1[testStruct0]]("Q1")
+	printTypeSizeName[Q8[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5, testStruct6, testStruct7]]("Q8")
 }
 
 func printTypeSize[T any]() {