Repository: mlange-42/arche
Pull request #58 (2023-02-12 12:00:11)

Unify filters and queries


Patch:
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 34487d0..85cf7b4 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -12,12 +12,11 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.19.x'
+          go-version: '1.20.x'
       - name: Install dependencies
         run: go get .
       - name: Benchmarks
         run: |
-          go test -benchmem -run=^$ -bench ^.*$ ./internal/base
           go test -benchmem -run=^$ -bench ^.*$ ./...
           cd benchmark
           go test -benchmem -run=^$ -bench ^.*$ ./...
diff --git a/ecs/archetype.go b/ecs/archetype.go
index e0355e1..4369fae 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -3,13 +3,11 @@ package ecs
 import (
 	"reflect"
 	"unsafe"
-
-	"github.com/mlange-42/arche/internal/base"
 )
 
 // archetype represents an ECS archetype
 type archetype struct {
-	Mask base.BitMask
+	Mask BitMask
 	Ids  []ID
 	// Indirection to avoid a fixed-size array of storages
 	// Increases access time by 50-100%
@@ -23,8 +21,8 @@ type archetype struct {
 var entityType = reflect.TypeOf(Entity{})
 
 // Init initializes an archetype
-func (a *archetype) Init(capacityIncrement int, components ...base.ComponentType) {
-	var mask base.BitMask
+func (a *archetype) Init(capacityIncrement int, components ...componentType) {
+	var mask BitMask
 	a.Ids = make([]ID, len(components))
 	comps := make([]storage, len(components))
 
@@ -84,7 +82,7 @@ func (a *archetype) Add(entity Entity, components ...Component) uint32 {
 }
 
 // AddPointer adds an entity with components to the archetype, using pointers
-func (a *archetype) AddPointer(entity Entity, components ...base.ComponentPointer) uint32 {
+func (a *archetype) AddPointer(entity Entity, components ...componentPointer) uint32 {
 	if len(components) != len(a.Ids) {
 		panic("Invalid number of components")
 	}
diff --git a/internal/base/bitmask.go b/ecs/bitmask.go
similarity index 94%
rename from internal/base/bitmask.go
rename to ecs/bitmask.go
index 8e60720..53a1fdb 100644
--- a/internal/base/bitmask.go
+++ b/ecs/bitmask.go
@@ -1,9 +1,9 @@
-package base
+package ecs
 
-// MaskTotalBits is the size of Mask in bits.
+// maskTotalBits is the size of Mask in bits.
 //
 // It is the maximum number of component types that may exist in any [World].
-const MaskTotalBits = 64
+const maskTotalBits = 64
 
 // BitMask is a bitmask.
 type BitMask uint64
diff --git a/ecs/query.go b/ecs/query.go
index 512a59c..335f9bf 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -2,13 +2,52 @@ package ecs
 
 import (
 	"unsafe"
-
-	"github.com/mlange-42/arche/internal/base"
 )
 
-// MaskFilter is the interface for logic filters
-type MaskFilter interface {
-	Matches(mask base.BitMask) bool
+// Filter is the interface for logic filters
+type Filter interface {
+	Matches(bits BitMask) bool
+}
+
+// Mask is a mask for a combination of components.
+type Mask struct {
+	BitMask BitMask
+}
+
+// All creates a new Mask from a list of IDs.
+//
+// If any ID is bigger or equal [MaskTotalBits], it'll not be added to the mask.
+func All(ids ...ID) Mask {
+	var mask BitMask
+	for _, id := range ids {
+		mask.Set(id, true)
+	}
+	return Mask{mask}
+}
+
+// Matches matches a filter against a bitmask
+func (f Mask) Matches(bits BitMask) bool {
+	return bits.Contains(f.BitMask)
+}
+
+// ButNot excludes the given components.
+func (f Mask) ButNot(comps ...ID) MaskPair {
+	return MaskPair{
+		Mask:    f,
+		Exclude: All(comps...),
+	}
+}
+
+// MaskPair is a filter for including an excluding components
+type MaskPair struct {
+	Mask    Mask
+	Exclude Mask
+}
+
+// Matches matches a filter against a mask
+func (f MaskPair) Matches(bits BitMask) bool {
+	ex := f.Exclude.BitMask
+	return bits.Contains(f.Mask.BitMask) && (ex == 0 || !bits.Contains(ex))
 }
 
 // EntityIter is the interface for iterable queries
@@ -28,89 +67,20 @@ type EntityIter interface {
 	Close()
 }
 
-// Query is a simple iterator to iterate entities.
+// Query is an advanced iterator to iterate entities.
 //
 // Create queries through the [World] using [World.Query].
-// See also the generic alternatives [Query1], [Query2], [Query3], ...
-//
-// # Example:
+// See also the generic alternatives [generic.Query1],  [generic.Query2], etc.
 //
-//	Query := world.Query(posID, rotID)
-//	for Query.Next() {
-//	    pos := (*position)(Query.Get(posID))
-//	    pos.X += 1.0
-//	}
+// For advanced filtering, see package [filter]
 type Query struct {
 	queryIter
-	Mask    bitMask
-	Exclude bitMask
+	filter Filter
 }
 
-// newQuery creates a new Query
-func newQuery(world *World, mask, exclude bitMask, lockBit uint8) Query {
+// newQuery creates a new Filter
+func newQuery(world *World, filter Filter, lockBit uint8) Query {
 	return Query{
-		queryIter: queryIter{
-			world:   world,
-			index:   -1,
-			lockBit: lockBit,
-		},
-		Mask:    mask,
-		Exclude: exclude,
-	}
-}
-
-// Not excludes components from the query.
-// Entities with these components will be skipped.
-//
-// # Example:
-//
-//	query := world.Query(idA, isB).Not(idC, isD)
-func (q Query) Not(comps ...ID) Query {
-	q.Exclude = base.NewBitMask(comps...)
-	return q
-}
-
-// Next proceeds to the next [Entity] in the Query.
-func (q *Query) Next() bool {
-	if q.archetype.Next() {
-		return true
-	}
-	i, a, ok := q.world.nextArchetype(q.Mask, q.Exclude, q.index)
-	q.index = i
-	if ok {
-		q.archetype = a
-		return true
-	}
-	q.done = true
-	q.world.closeQuery(&q.queryIter)
-	return false
-}
-
-// Filter is an advanced iterator to iterate entities.
-//
-// Create filter through the [World] using [World.Query].
-// There is no generic alternative for filters.
-//
-// # Example:
-//
-//	filter := world.Filter(
-//	    And {
-//	        All(idA, idB, idC),
-//	        Not(OneOf(idD, idE)),
-//	    }
-//	)
-//	for filter.Next() {
-//	    pos := (*position)(filter.Get(posID))
-//	    pos.X += 1.0
-//	}
-type Filter struct {
-	queryIter
-	filter MaskFilter
-}
-
-// newFilter creates a new Filter
-func newFilter(world *World, filter MaskFilter, lockBit uint8) Filter {
-	return Filter{
 		queryIter: queryIter{
 			world:   world,
 			index:   -1,
@@ -121,11 +91,11 @@ func newFilter(world *World, filter MaskFilter, lockBit uint8) Filter {
 }
 
 // Next proceeds to the next [Entity] in the Query.
-func (q *Filter) Next() bool {
+func (q *Query) Next() bool {
 	if q.archetype.Next() {
 		return true
 	}
-	i, a, ok := q.world.nextArchetypeFilter(q.filter, q.index)
+	i, a, ok := q.world.nextArchetype(q.filter, q.index)
 	q.index = i
 	if ok {
 		q.archetype = a
diff --git a/ecs/types.go b/ecs/types.go
index cbf5424..96b5551 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -1,28 +1,30 @@
 package ecs
 
 import (
-	"github.com/mlange-42/arche/internal/base"
+	"reflect"
+	"unsafe"
 )
 
 // Eid is the entity identifier/index type
 type eid uint32
 
 // ID is the component identifier type
-type ID = base.ID
-
-// bitMask is a bitmask.
-type bitMask = base.BitMask
-
-// maskTotalBits is the size of Mask in bits.
-//
-// It is the maximum number of component types that may exist in any [World].
-const maskTotalBits = base.MaskTotalBits
-
-// Mask is a mask for a combination of components.
-type Mask = base.Mask
+type ID = uint8
 
 // Component is a Component ID/Component pointer pair
 type Component struct {
 	ID
 	Component interface{}
 }
+
+// componentType is a component ID with a data type
+type componentType struct {
+	ID
+	Type reflect.Type
+}
+
+// componentPointer is a component ID with a pointer in a storage
+type componentPointer struct {
+	ID
+	Pointer unsafe.Pointer
+}
diff --git a/ecs/world.go b/ecs/world.go
index 94c9fbd..21ba465 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -3,16 +3,19 @@ package ecs
 import (
 	"reflect"
 	"unsafe"
-
-	"github.com/mlange-42/arche/internal/base"
 )
 
-// ComponentID returns the ID for a component type. Registers the type if it is not already registered.
+// ComponentID returns the ID for a component type via generics. 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)
 }
 
+// TypeID returns the ID for a component type. Registers the type if it is not already registered.
+func TypeID(w *World, tp reflect.Type) ID {
+	return w.componentID(tp)
+}
+
 // World is the central type holding [Entity] and component data.
 type World struct {
 	config     Config
@@ -21,7 +24,7 @@ type World struct {
 	entityPool entityPool
 	bitPool    bitPool
 	registry   componentRegistry
-	locks      bitMask
+	locks      BitMask
 }
 
 // NewWorld creates a new [World]
@@ -42,7 +45,7 @@ func FromConfig(conf Config) World {
 		bitPool:    newBitPool(),
 		registry:   newComponentRegistry(),
 		archetypes: arches,
-		locks:      bitMask(0),
+		locks:      BitMask(0),
 	}
 }
 
@@ -90,27 +93,23 @@ func (w *World) RemEntity(entity Entity) {
 	w.entities[entity.id].arch = nil
 }
 
-// Query creates a [Query] iterator for the given components.
+// Query creates a [Query] iterator.
 //
 // Locks the world to prevent changes to component compositions.
 //
-// See also the generic alternatives [Query1], [Query2], [Query3], ...
-func (w *World) Query(comps ...ID) Query {
-	mask := base.NewBitMask(comps...)
-	lock := w.bitPool.Get()
-	w.locks.Set(ID(lock), true)
-	return newQuery(w, mask, 0, lock)
-}
-
-// Filter creates an advanced [Filter] iterator.
+// # Example:
 //
-// Locks the world to prevent changes to component compositions.
+//	query := world.Query(All(idA, idB).Not(idC))
+//	for query.Next() {
+//	    pos := (*position)(query.Get(posID))
+//	    pos.X += 1.0
+//	}
 //
-// There is no generic alternative for filters.
-func (w *World) Filter(filter MaskFilter) Filter {
+// For advanced filtering, see package [filter]
+func (w *World) Query(filter Filter) Query {
 	lock := w.bitPool.Get()
 	w.locks.Set(ID(lock), true)
-	return newFilter(w, filter, lock)
+	return newQuery(w, filter, lock)
 }
 
 // Alive reports whether an entity is still alive.
@@ -254,13 +253,13 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 
 	arch := w.findOrCreateArchetype(oldArch, addIDs, rem)
 
-	allComps := make([]base.ComponentPointer, 0, len(keepIDs)+len(addIDs))
+	allComps := make([]componentPointer, 0, len(keepIDs)+len(addIDs))
 	for _, id := range keepIDs {
 		comp := oldArch.Get(int(index.index), id)
-		allComps = append(allComps, base.ComponentPointer{ID: id, Pointer: comp})
+		allComps = append(allComps, componentPointer{ID: id, Pointer: comp})
 	}
 	for _, id := range addIDs {
-		allComps = append(allComps, base.ComponentPointer{ID: id, Pointer: nil})
+		allComps = append(allComps, componentPointer{ID: id, Pointer: nil})
 	}
 
 	newIndex := arch.AddPointer(entity, allComps...)
@@ -315,14 +314,14 @@ func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID) *arc
 	return curr
 }
 
-func (w *World) findOrCreateArchetypeSlow(mask bitMask) (*archetype, bool) {
+func (w *World) findOrCreateArchetypeSlow(mask BitMask) (*archetype, bool) {
 	if arch, ok := w.findArchetype(mask); ok {
 		return arch, false
 	}
 	return w.createArchetype(mask), true
 }
 
-func (w *World) findArchetype(mask bitMask) (*archetype, bool) {
+func (w *World) findArchetype(mask BitMask) (*archetype, bool) {
 	length := w.archetypes.Len()
 	for i := 0; i < length; i++ {
 		arch := w.archetypes.Get(i)
@@ -333,15 +332,15 @@ func (w *World) findArchetype(mask bitMask) (*archetype, bool) {
 	return nil, false
 }
 
-func (w *World) createArchetype(mask bitMask) *archetype {
+func (w *World) createArchetype(mask BitMask) *archetype {
 	count := int(mask.TotalBitsSet())
-	types := make([]base.ComponentType, count)
+	types := make([]componentType, count)
 
 	idx := 0
 	for i := 0; i < maskTotalBits; i++ {
 		id := ID(i)
 		if mask.Get(id) {
-			types[idx] = base.ComponentType{ID: id, Type: w.registry.Types[id]}
+			types[idx] = componentType{ID: id, Type: w.registry.Types[id]}
 			idx++
 		}
 	}
@@ -356,21 +355,7 @@ func (w *World) componentID(tp reflect.Type) ID {
 	return w.registry.ComponentID(tp)
 }
 
-func (w *World) nextArchetype(mask, exclude bitMask, index int) (int, archetypeIter, bool) {
-	len := w.archetypes.Len()
-	if index >= len {
-		panic("exceeded end of query")
-	}
-	for i := index + 1; i < len; i++ {
-		a := w.archetypes.Get(i)
-		if a.Len() > 0 && a.Mask.Contains(mask) && !a.Mask.ContainsAny(exclude) {
-			return i, newArchetypeIter(a), true
-		}
-	}
-	return len, archetypeIter{}, false
-}
-
-func (w *World) nextArchetypeFilter(filter MaskFilter, index int) (int, archetypeIter, bool) {
+func (w *World) nextArchetype(filter Filter, index int) (int, archetypeIter, bool) {
 	len := w.archetypes.Len()
 	if index >= len {
 		panic("exceeded end of query")
diff --git a/examples/base/main.go b/examples/base/main.go
index 0c35fdc..ecdb24b 100644
--- a/examples/base/main.go
+++ b/examples/base/main.go
@@ -45,7 +45,7 @@ func main() {
 	// Time loop.
 	for t := 0; t < 1000; t++ {
 		// Get a fresh query.
-		query := world.Query(posID, velID)
+		query := world.Query(ecs.All(posID, velID))
 		// Iterate it
 		for query.Next() {
 			// Component access through a Query.
diff --git a/examples/filter/main.go b/examples/filter/main.go
index 3846700..3071009 100644
--- a/examples/filter/main.go
+++ b/examples/filter/main.go
@@ -75,7 +75,7 @@ func main() {
 	// Time loop.
 	for t := 0; t < 1000; t++ {
 		// Get a fresh filter query.
-		query := world.Filter(filter)
+		query := world.Query(filter)
 		// Iterate it
 		for query.Next() {
 			// Component access through a Query.
diff --git a/examples/generic/main.go b/examples/generic/main.go
index 98790dc..d22b1b9 100644
--- a/examples/generic/main.go
+++ b/examples/generic/main.go
@@ -38,16 +38,18 @@ func main() {
 		vel.Y = rand.NormFloat64()
 	}
 
+	query := generic.Query2[Position, Velocity]()
+
 	// Time loop.
 	for t := 0; t < 1000; t++ {
 		// Get a fresh query.
 		// Generic queries support up to 8 components.
 		// For more components, use World.Query()
-		query := generic.Query2[Position, Velocity](&world)
+		q := query.Build(&world)
 		// Iterate it
-		for query.Next() {
+		for q.Next() {
 			// Component access through a Query.
-			pos, vel := query.GetAll()
+			pos, vel := q.GetAll()
 			// Update component fields.
 			pos.X += vel.X
 			pos.Y += vel.Y
diff --git a/filter/filter.go b/filter/filter.go
index 660aaec..b23ffbc 100644
--- a/filter/filter.go
+++ b/filter/filter.go
@@ -2,96 +2,126 @@ package filter
 
 import (
 	"github.com/mlange-42/arche/ecs"
-	"github.com/mlange-42/arche/internal/base"
 )
 
 // Mask is a mask for a combination of components.
-type Mask = base.Mask
+type Mask = ecs.Mask
+
+// OneOf matches any of the two components.
+//
+// Like [Or] for combining individual components.
+func OneOf(compA ecs.ID, compB ecs.ID) *OR {
+	return &OR{ecs.All(compA), ecs.All(compB)}
+}
+
+// ALL is a filter including entities with all the given components
+type ALL Mask
 
 // All matches all the given components.
 //
 // Like [And] for combining individual components.
-func All(comps ...base.ID) Mask {
-	return base.NewMask(comps...)
+func All(comps ...ecs.ID) ALL {
+	return ALL(ecs.All(comps...))
 }
 
-// OneOf matches any of the two components.
-//
-// Like [Or] for combining individual components.
-func OneOf(compA base.ID, compB base.ID) *OR {
-	return &OR{base.NewMask(compA), base.NewMask(compB)}
+// Not inverts this filter to exclude entities with all the given components
+func (f ALL) Not() NOT {
+	return NOT(f)
+}
+
+// Matches matches a filter against a bitmask
+func (f ALL) Matches(bits ecs.BitMask) bool {
+	return bits.Contains(f.BitMask)
+}
+
+// ANY is a filter including entities with any of the given components
+type ANY Mask
+
+// Any constructs a NotANY filter
+func Any(comps ...ecs.ID) ANY {
+	return ANY(ecs.All(comps...))
+}
+
+// Not inverts this filter to exclude entities with any of the given components
+func (f ANY) Not() NOT {
+	return NOT(f)
+}
+
+// Matches matches a filter against a bitmask
+func (f ANY) Matches(bits ecs.BitMask) bool {
+	return bits.ContainsAny(f.BitMask)
+}
+
+// NOT is a filter for excluding entities with all given components
+type NOT Mask
+
+// Not constructs a NOT filter
+func Not(comps ...ecs.ID) NOT {
+	return NOT(ecs.All(comps...))
+}
+
+// Matches matches a filter against a bitmask
+func (f NOT) Matches(bits ecs.BitMask) bool {
+	return !bits.Contains(f.BitMask)
+}
+
+// NotANY is a filter for excluding entities with any of the given components
+type NotANY Mask
+
+// NotAny constructs a NotANY filter
+func NotAny(comps ...ecs.ID) NotANY {
+	return NotANY(ecs.All(comps...))
+}
+
+// Matches matches a filter against a bitmask
+func (f NotANY) Matches(bits ecs.BitMask) bool {
+	return !bits.ContainsAny(f.BitMask)
 }
 
 // AND is a filter for ANDing together components
 type AND struct {
-	L ecs.MaskFilter
-	R ecs.MaskFilter
+	L ecs.Filter
+	R ecs.Filter
 }
 
 // And constructs a pointer to a AND filter
-func And(l, r ecs.MaskFilter) *AND {
+func And(l, r ecs.Filter) *AND {
 	return &AND{L: l, R: r}
 }
 
-// Matches matches a filter against a mask
-func (f *AND) Matches(mask base.BitMask) bool {
-	return f.L.Matches(mask) && f.R.Matches(mask)
+// Matches matches a filter against a bitmask
+func (f *AND) Matches(bits ecs.BitMask) bool {
+	return f.L.Matches(bits) && f.R.Matches(bits)
 }
 
 // OR is a filter for ORing together components
 type OR struct {
-	L ecs.MaskFilter
-	R ecs.MaskFilter
+	L ecs.Filter
+	R ecs.Filter
 }
 
 // Or constructs a pointer to a OR filter
-func Or(l, r ecs.MaskFilter) *OR {
+func Or(l, r ecs.Filter) *OR {
 	return &OR{L: l, R: r}
 }
 
-// Matches matches a filter against a mask
-func (f *OR) Matches(mask base.BitMask) bool {
-	return f.L.Matches(mask) || f.R.Matches(mask)
+// Matches matches a filter against a bitmask
+func (f *OR) Matches(bits ecs.BitMask) bool {
+	return f.L.Matches(bits) || f.R.Matches(bits)
 }
 
 // XOR is a filter for XORing together components
 type XOR struct {
-	L ecs.MaskFilter
-	R ecs.MaskFilter
+	L ecs.Filter
+	R ecs.Filter
 }
 
 // XOr constructs a pointer to a XOR filter
-func XOr(l, r ecs.MaskFilter) *XOR {
+func XOr(l, r ecs.Filter) *XOR {
 	return &XOR{L: l, R: r}
 }
 
-// Matches matches a filter against a mask
-func (f *XOR) Matches(mask base.BitMask) bool {
-	return f.L.Matches(mask) != f.R.Matches(mask)
-}
-
-// NOT is a filter for excluding entities with all given components
-type NOT Mask
-
-// Not constructs a NOT filter
-func Not(comps ...base.ID) NOT {
-	return NOT(base.NewMask(comps...))
-}
-
-// Matches matches a filter against a mask
-func (f NOT) Matches(mask base.BitMask) bool {
-	return !mask.Contains(f.BitMask)
-}
-
-// NotANY is a filter for excluding entities with any of the the given components
-type NotANY Mask
-
-// NotAny constructs a NotANY filter
-func NotAny(comps ...base.ID) NotANY {
-	return NotANY(base.NewMask(comps...))
-}
-
-// Matches matches a filter against a mask
-func (f NotANY) Matches(mask base.BitMask) bool {
-	return !mask.ContainsAny(f.BitMask)
+// Matches matches a filter against a bitmask
+func (f *XOR) Matches(bits ecs.BitMask) bool {
+	return f.L.Matches(bits) != f.R.Matches(bits)
 }
diff --git a/generic/generic.go b/generic/generic.go
index 1104baf..9e8cd12 100644
--- a/generic/generic.go
+++ b/generic/generic.go
@@ -47,10 +47,10 @@ func (g *Map[T]) Set(entity ecs.Entity, comp *T) *T {
 	return (*T)(g.world.Set(entity, g.id, comp))
 }
 
-// Add adds a component type to an entity.
+// Add1 adds a component type to an entity.
 //
-// See also [World.Add].
-func Add[A any](w *ecs.World, entity ecs.Entity) *A {
+// See also [World.Add1].
+func Add1[A any](w *ecs.World, entity ecs.Entity) *A {
 	id := ecs.ComponentID[A](w)
 	w.Add(entity, id)
 	return (*A)(w.Get(entity, id))
@@ -102,10 +102,10 @@ func Add5[A any, B any, C any, D any, E any](w *ecs.World, entity ecs.Entity) (*
 	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.
+// Assign1 adds a components to an entity.
 //
-// See also [World.Assign] and [World.AssignN].
-func Assign[A any](w *ecs.World, entity ecs.Entity, a *A) *A {
+// See also [World.Assign1] and [World.AssignN].
+func Assign1[A any](w *ecs.World, entity ecs.Entity, a *A) *A {
 	idA := ecs.ComponentID[A](w)
 	w.Assign(entity, idA, a)
 	return (*A)(w.Get(entity, idA))
@@ -157,10 +157,10 @@ func Assign5[A any, B any, C any, D any, E any](w *ecs.World, entity ecs.Entity,
 	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.
+// Remove1 removes a component from an entity.
 //
-// See also [World.Remove].
-func Remove[A any](w *ecs.World, entity ecs.Entity) {
+// See also [World.Remove1].
+func Remove1[A any](w *ecs.World, entity ecs.Entity) {
 	w.Remove(entity, ecs.ComponentID[A](w))
 }
 
diff --git a/generic/query_generic.go b/generic/query_generic.go
index f1a3409..255aa91 100644
--- a/generic/query_generic.go
+++ b/generic/query_generic.go
@@ -1,61 +1,111 @@
 package generic
 
 import (
+	"reflect"
+
 	"github.com/mlange-42/arche/ecs"
-	"github.com/mlange-42/arche/internal/base"
 )
 
-// Q0 is a generic query for no components.
-//
-// Create one with [Query0]
-type Q0 struct {
-	ecs.Query
+func typeOf[T any]() reflect.Type {
+	return reflect.TypeOf((*T)(nil)).Elem()
+}
+
+func toIds(w *ecs.World, types []reflect.Type) []ecs.ID {
+	ids := make([]ecs.ID, len(types))
+	for i, t := range types {
+		ids[i] = ecs.TypeID(w, t)
+	}
+	return ids
+}
+
+func toMask(w *ecs.World, types []reflect.Type) ecs.Mask {
+	mask := ecs.BitMask(0)
+	for _, t := range types {
+		mask.Set(ecs.TypeID(w, t), true)
+	}
+	return ecs.Mask{BitMask: mask}
+}
+
+// Q0Builder builds a Q0 query
+type Q0Builder struct {
+	exclude []reflect.Type
 }
 
 // Query0 creates a generic query for no components.
 //
 // See also [World.Query].
-func Query0(w *ecs.World) Q0 {
-	return Q0{
-		Query: w.Query(),
-	}
+func Query0() Q0Builder {
+	return Q0Builder{}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q0) Not(mask ecs.Mask) Q0 {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q0Builder) Not(mask []reflect.Type) Q0Builder {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
-// Q1 is a generic query for one component.
+// Build builds a Q0 query for iteration.
+func (q Q0Builder) Build(w *ecs.World) Q0 {
+	return Q0{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(),
+			Exclude: toMask(w, q.exclude),
+		}),
+	}
+}
+
+// Q0 is a generic query for no components.
 //
-// Create one with [Query1]
-type Q1[A any] struct {
+// Create one with [Query0]
+type Q0 struct {
 	ecs.Query
-	id ecs.ID
+}
+
+// Q1Builder builds a Q1 query
+type Q1Builder[A any] struct {
+	include reflect.Type
+	exclude []reflect.Type
 }
 
 // Query1 creates a generic query for one component.
 //
 // See also [World.Query].
-func Query1[A any](w *ecs.World) Q1[A] {
-	id := ecs.ComponentID[A](w)
-	return Q1[A]{
-		Query: w.Query(id),
-		id:    id,
+func Query1[A any]() Q1Builder[A] {
+	return Q1Builder[A]{
+		include: typeOf[A](),
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q1[A]) Not(mask ecs.Mask) Q1[A] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q1Builder[A]) Not(mask []reflect.Type) Q1Builder[A] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q1 query for iteration.
+func (q Q1Builder[A]) Build(w *ecs.World) Q1[A] {
+	id := ecs.TypeID(w, q.include)
+	return Q1[A]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(id),
+			Exclude: toMask(w, q.exclude),
+		}),
+		id,
+	}
+}
+
+// Q1 is a generic query for one component.
+//
+// Create one with [Query1]
+type Q1[A any] struct {
+	ecs.Query
+	id ecs.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))
@@ -63,33 +113,49 @@ func (q *Q1[A]) Get1() *A {
 
 //////////////////////////////////////////////////////////////////////////
 
-// Q2 is a generic query for two components.
-//
-// Create one with [Query2]
-type Q2[A any, B any] struct {
-	ecs.Query
-	ids [2]ecs.ID
+// Q2Builder builds a Q2 query
+type Q2Builder[A any, B any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // Query2 creates a generic query for two components.
 //
 // See also [World.Query].
-func Query2[A any, B any](w *ecs.World) Q2[A, B] {
-	ids := [2]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w)}
-	return Q2[A, B]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query2[A any, B any]() Q2Builder[A, B] {
+	return Q2Builder[A, B]{
+		include: []reflect.Type{typeOf[A](), typeOf[B]()},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q2[A, B]) Not(mask ecs.Mask) Q2[A, B] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q2Builder[A, B]) Not(mask []reflect.Type) Q2Builder[A, B] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q2 query for iteration.
+func (q Q2Builder[A, B]) Build(w *ecs.World) Q2[A, B] {
+	ids := toIds(w, q.include)
+	return Q2[A, B]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// Q2 is a generic query for two components.
+//
+// Create one with [Query2]
+type Q2[A any, B any] struct {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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]))
@@ -107,33 +173,49 @@ func (q *Q2[A, B]) Get2() *B {
 
 //////////////////////////////////////////////////////////////////////////
 
-// Q3 is a generic query for three components.
-//
-// Create one with [Query3]
-type Q3[A any, B any, C any] struct {
-	ecs.Query
-	ids [3]ecs.ID
+// Q3Builder builds a Q3 query
+type Q3Builder[A any, B any, C any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // Query3 creates a generic query for three components.
 //
 // See also [World.Query].
-func Query3[A any, B any, C any](w *ecs.World) Q3[A, B, C] {
-	ids := [3]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w)}
-	return Q3[A, B, C]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query3[A any, B any, C any]() Q3Builder[A, B, C] {
+	return Q3Builder[A, B, C]{
+		include: []reflect.Type{typeOf[A](), typeOf[B](), typeOf[C]()},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q3[A, B, C]) Not(mask ecs.Mask) Q3[A, B, C] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q3Builder[A, B, C]) Not(mask []reflect.Type) Q3Builder[A, B, C] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q3 query for iteration.
+func (q Q3Builder[A, B, C]) Build(w *ecs.World) Q3[A, B, C] {
+	ids := toIds(w, q.include)
+	return Q3[A, B, C]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// Q3 is a generic query for three components.
+//
+// Create one with [Query3]
+type Q3[A any, B any, C any] struct {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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]))
@@ -156,33 +238,49 @@ func (q *Q3[A, B, C]) Get3() *C {
 
 //////////////////////////////////////////////////////////////////////////
 
-// Q4 is a generic query for four components.
-//
-// Create one with [Query4]
-type Q4[A any, B any, C any, D any] struct {
-	ecs.Query
-	ids [4]ecs.ID
+// Q4Builder builds a Q4 query
+type Q4Builder[A any, B any, C any, D any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // Query4 creates a generic query for four components.
 //
 // See also [World.Query].
-func Query4[A any, B any, C any, D any](w *ecs.World) Q4[A, B, C, D] {
-	ids := [4]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w)}
-	return Q4[A, B, C, D]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query4[A any, B any, C any, D any]() Q4Builder[A, B, C, D] {
+	return Q4Builder[A, B, C, D]{
+		include: []reflect.Type{typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D]()},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q4[A, B, C, D]) Not(mask ecs.Mask) Q4[A, B, C, D] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q4Builder[A, B, C, D]) Not(mask []reflect.Type) Q4Builder[A, B, C, D] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q4 query for iteration.
+func (q Q4Builder[A, B, C, D]) Build(w *ecs.World) Q4[A, B, C, D] {
+	ids := toIds(w, q.include)
+	return Q4[A, B, C, D]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// Q4 is a generic query for four components.
+//
+// Create one with [Query4]
+type Q4[A any, B any, C any, D any] struct {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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])),
@@ -213,33 +311,52 @@ func (q *Q4[A, B, C, D]) Get4() *D {
 
 //////////////////////////////////////////////////////////////////////////
 
-// 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 {
-	ecs.Query
-	ids [5]ecs.ID
+// Q5Builder builds a Q5 query
+type Q5Builder[A any, B any, C any, D any, E any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // 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 *ecs.World) Q5[A, B, C, D, E] {
-	ids := [5]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w), ecs.ComponentID[E](w)}
-	return Q5[A, B, C, D, E]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query5[A any, B any, C any, D any, E any]() Q5Builder[A, B, C, D, E] {
+	return Q5Builder[A, B, C, D, E]{
+		include: []reflect.Type{
+			typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+			typeOf[E](),
+		},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q5[A, B, C, D, E]) Not(mask ecs.Mask) Q5[A, B, C, D, E] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q5Builder[A, B, C, D, E]) Not(mask []reflect.Type) Q5Builder[A, B, C, D, E] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q5 query for iteration.
+func (q Q5Builder[A, B, C, D, E]) Build(w *ecs.World) Q5[A, B, C, D, E] {
+	ids := toIds(w, q.include)
+	return Q5[A, B, C, D, E]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// 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 {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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])),
@@ -276,36 +393,52 @@ func (q *Q5[A, B, C, D, E]) Get5() *E {
 
 //////////////////////////////////////////////////////////////////////////
 
-// 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 {
-	ecs.Query
-	ids [6]ecs.ID
+// Q6Builder builds a Q6 query
+type Q6Builder[A any, B any, C any, D any, E any, F any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // 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 *ecs.World) Q6[A, B, C, D, E, F] {
-	ids := [6]ecs.ID{
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-	}
-	return Q6[A, B, C, D, E, F]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query6[A any, B any, C any, D any, E any, F any]() Q6Builder[A, B, C, D, E, F] {
+	return Q6Builder[A, B, C, D, E, F]{
+		include: []reflect.Type{
+			typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+			typeOf[E](), typeOf[F](),
+		},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q6[A, B, C, D, E, F]) Not(mask ecs.Mask) Q6[A, B, C, D, E, F] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q6Builder[A, B, C, D, E, F]) Not(mask []reflect.Type) Q6Builder[A, B, C, D, E, F] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q6 query for iteration.
+func (q Q6Builder[A, B, C, D, E, F]) Build(w *ecs.World) Q6[A, B, C, D, E, F] {
+	ids := toIds(w, q.include)
+	return Q6[A, B, C, D, E, F]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// 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 {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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])),
@@ -348,29 +481,52 @@ func (q *Q6[A, B, C, D, E, F]) Get6() *F {
 
 //////////////////////////////////////////////////////////////////////////
 
-// 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 {
-	ecs.Query
-	ids [7]ecs.ID
+// Q7Builder builds a Q7 query
+type Q7Builder[A any, B any, C any, D any, E any, F any, G any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
 // 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 *ecs.World) Q7[A, B, C, D, E, F, G] {
-	ids := [7]ecs.ID{
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-		ecs.ComponentID[G](w),
+func Query7[A any, B any, C any, D any, E any, F any, G any]() Q7Builder[A, B, C, D, E, F, G] {
+	return Q7Builder[A, B, C, D, E, F, G]{
+		include: []reflect.Type{
+			typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+			typeOf[E](), typeOf[F](), typeOf[G](),
+		},
 	}
+}
+
+// Not excludes entities with the given components from the query.
+//
+// Create the required mask with [Mask1], [Mask2], etc.
+func (q Q7Builder[A, B, C, D, E, F, G]) Not(mask []reflect.Type) Q7Builder[A, B, C, D, E, F, G] {
+	q.exclude = append(q.exclude, mask...)
+	return q
+}
+
+// Build builds a Q7 query for iteration.
+func (q Q7Builder[A, B, C, D, E, F, G]) Build(w *ecs.World) Q7[A, B, C, D, E, F, G] {
+	ids := toIds(w, q.include)
 	return Q7[A, B, C, D, E, F, G]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
 	}
 }
 
+// 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 {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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])),
@@ -382,14 +538,6 @@ func (q *Q7[A, B, C, D, E, F, G]) GetAll() (*A, *B, *C, *D, *E, *F, *G) {
 		(*G)(q.Query.Get(q.ids[6]))
 }
 
-// Not excludes entities with the given components from the query.
-//
-// Create the required mask with [Mask1], [Mask2], etc.
-func (q Q7[A, B, C, D, E, F, G]) Not(mask ecs.Mask) Q7[A, B, C, D, E, F, G] {
-	q.Exclude = q.Exclude | mask.BitMask
-	return q
-}
-
 // 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]))
@@ -427,37 +575,52 @@ func (q *Q7[A, B, C, D, E, F, G]) Get7() *G {
 
 //////////////////////////////////////////////////////////////////////////
 
-// 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 {
-	ecs.Query
-	ids [8]ecs.ID
+// Q8Builder builds a Q8 query
+type Q8Builder[A any, B any, C any, D any, E any, F any, G any, H any] struct {
+	include []reflect.Type
+	exclude []reflect.Type
 }
 
-// Query8 creates a generic query for seven components.
+// Query8 creates a generic query for eight components.
 //
 // See also [World.Query].
-func Query8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.World) Q8[A, B, C, D, E, F, G, H] {
-	ids := [8]ecs.ID{
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-		ecs.ComponentID[G](w), ecs.ComponentID[H](w),
-	}
-	return Q8[A, B, C, D, E, F, G, H]{
-		Query: w.Query(ids[:]...),
-		ids:   ids,
+func Query8[A any, B any, C any, D any, E any, F any, G any, H any]() Q8Builder[A, B, C, D, E, F, G, H] {
+	return Q8Builder[A, B, C, D, E, F, G, H]{
+		include: []reflect.Type{
+			typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+			typeOf[E](), typeOf[F](), typeOf[G](), typeOf[H](),
+		},
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q8[A, B, C, D, E, F, G, H]) Not(mask ecs.Mask) Q8[A, B, C, D, E, F, G, H] {
-	q.Exclude = q.Exclude | mask.BitMask
+func (q Q8Builder[A, B, C, D, E, F, G, H]) Not(mask []reflect.Type) Q8Builder[A, B, C, D, E, F, G, H] {
+	q.exclude = append(q.exclude, mask...)
 	return q
 }
 
+// Build builds a Q8 query for iteration.
+func (q Q8Builder[A, B, C, D, E, F, G, H]) Build(w *ecs.World) Q8[A, B, C, D, E, F, G, H] {
+	ids := toIds(w, q.include)
+	return Q8[A, B, C, D, E, F, G, H]{
+		w.Query(ecs.MaskPair{
+			Mask:    ecs.All(ids...),
+			Exclude: toMask(w, q.exclude),
+		}),
+		ids,
+	}
+}
+
+// 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 {
+	ecs.Query
+	ids []ecs.ID
+}
+
 // GetAll returns all queried components 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])),
@@ -510,63 +673,58 @@ func (q *Q8[A, B, C, D, E, F, G, H]) Get8() *H {
 	return (*H)(q.Query.Get(q.ids[7]))
 }
 
-// Mask1 creates a component [Mask] for one component type.
-func Mask1[A any](w *ecs.World) ecs.Mask {
-	return base.NewMask(ecs.ComponentID[A](w))
+// Mask1 creates a component type list for one component type.
+func Mask1[A any]() []reflect.Type {
+	return []reflect.Type{typeOf[A]()}
 }
 
-// Mask2 creates a component [Mask] for two component types.
-func Mask2[A any, B any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w),
-	)
+// Mask2 creates a component type list for two component types.
+func Mask2[A any, B any]() []reflect.Type {
+	return []reflect.Type{typeOf[A](), typeOf[B]()}
 }
 
-// Mask3 creates a component [Mask] for three component types.
-func Mask3[A any, B any, C any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-	)
+// Mask3 creates a component type list for three component types.
+func Mask3[A any, B any, C any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](),
+	}
 }
 
-// Mask4 creates a component [Mask] for four component types.
-func Mask4[A any, B any, C any, D any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w),
-	)
+// Mask4 creates a component type list for four component types.
+func Mask4[A any, B any, C any, D any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+	}
 }
 
-// Mask5 creates a component [Mask] for five component types.
-func Mask5[A any, B any, C any, D any, E any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w),
-	)
+// Mask5 creates a component type list for five component types.
+func Mask5[A any, B any, C any, D any, E any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+		typeOf[E](),
+	}
 }
 
-// Mask6 creates a component [Mask] for six component types.
-func Mask6[A any, B any, C any, D any, E any, F any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-	)
+// Mask6 creates a component type list for six component types.
+func Mask6[A any, B any, C any, D any, E any, F any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+		typeOf[E](), typeOf[F](),
+	}
 }
 
-// Mask7 creates a component [Mask] for seven component types.
-func Mask7[A any, B any, C any, D any, E any, F any, G any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-		ecs.ComponentID[G](w),
-	)
+// Mask7 creates a component type list for seven component types.
+func Mask7[A any, B any, C any, D any, E any, F any, G any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+		typeOf[E](), typeOf[F](), typeOf[G](),
+	}
 }
 
-// Mask8 creates a component [Mask] for eight component types.
-func Mask8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.World) ecs.Mask {
-	return base.NewMask(
-		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
-		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
-		ecs.ComponentID[G](w), ecs.ComponentID[H](w),
-	)
+// Mask8 creates a component type list for eight component types.
+func Mask8[A any, B any, C any, D any, E any, F any, G any, H any]() []reflect.Type {
+	return []reflect.Type{
+		typeOf[A](), typeOf[B](), typeOf[C](), typeOf[D](),
+		typeOf[E](), typeOf[F](), typeOf[G](), typeOf[H](),
+	}
 }
diff --git a/internal/base/types.go b/internal/base/types.go
deleted file mode 100644
index 7fba07e..0000000
--- a/internal/base/types.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package base
-
-import (
-	"reflect"
-	"unsafe"
-)
-
-// ID is the component identifier type
-type ID uint8
-
-// Mask is a mask for a combination of components.
-type Mask struct {
-	BitMask
-}
-
-// NewMask creates a new Mask from a list of IDs.
-//
-// If any ID is bigger or equal [MaskTotalBits], it'll not be added to the mask.
-func NewMask(ids ...ID) Mask {
-	var mask BitMask
-	for _, id := range ids {
-		mask.Set(id, true)
-	}
-	return Mask{mask}
-}
-
-// Matches matches a filter against a mask
-func (f Mask) Matches(mask BitMask) bool {
-	return mask.Contains(f.BitMask)
-}
-
-// ComponentType is a component ID with a data type
-type ComponentType struct {
-	ID
-	Type reflect.Type
-}
-
-// ComponentPointer is a component ID with a pointer in a storage
-type ComponentPointer struct {
-	ID
-	Pointer unsafe.Pointer
-}
diff --git a/profile/main/profile.go b/profile/main/profile.go
index d25efe3..1d87a65 100644
--- a/profile/main/profile.go
+++ b/profile/main/profile.go
@@ -50,7 +50,7 @@ func run(rounds, iters, entities int) {
 		}
 
 		for j := 0; j < iters; j++ {
-			query := world.Query(posID, rotID)
+			query := world.Query(ecs.All(posID, rotID))
 			for query.Next() {
 				pos := (*position)(query.Get(posID))
 				_ = pos


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2770c37..10695f3 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -12,7 +12,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.19.x'
+          go-version: '1.20.x'
       - name: Install dependencies
         run: go get .
       - name: Build Linux
@@ -30,12 +30,12 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.19.x'
+          go-version: '1.20.x'
       - name: Install dependencies
         run: go get .
       - name: Test
         run: |
-          go test -v ./internal/base ./... -covermode=count -coverprofile="coverage.out"
+          go test -v ./... -covermode=count -coverprofile="coverage.out"
           go tool cover -func="coverage.out"
 
   examples:
@@ -46,7 +46,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.19.x'
+          go-version: '1.20.x'
       - name: Install dependencies
         run: go get .
       - name: Test
@@ -63,7 +63,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.19.x'
+          go-version: '1.20.x'
       - name: Install dependencies
         run: |
           go get .
@@ -74,6 +74,6 @@ jobs:
             exit 1
           fi
       - name: Lint with vet
-        run: go vet ./internal/base ./...
+        run: go vet ./...
       - name: Lint with staticcheck
-        run: staticcheck ./internal/base ./...
+        run: staticcheck ./...
diff --git a/benchmark/add/arche_test.go b/benchmark/add/arche_test.go
index 012a84b..b052737 100644
--- a/benchmark/add/arche_test.go
+++ b/benchmark/add/arche_test.go
@@ -31,7 +31,7 @@ func addArcheGeneric(b *testing.B, count int) {
 
 		for i := 0; i < count; i++ {
 			entity := world.NewEntity()
-			generic.Add[position](&world, entity)
+			generic.Add1[position](&world, entity)
 		}
 	}
 }
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index aebb075..8b3f053 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -23,7 +23,7 @@ func runArcheQuery(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Query(posID, rotID)
+		query := world.Query(ecs.All(posID, rotID))
 		b.StartTimer()
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
@@ -47,7 +47,7 @@ func runArcheFilter(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Filter(filter.All(posID, rotID))
+		query := world.Query(filter.All(posID, rotID))
 		b.StartTimer()
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
@@ -67,18 +67,20 @@ func runArcheQueryGeneric(b *testing.B, count int) {
 		entity := world.NewEntity()
 		world.Add(entity, posID, rotID)
 	}
+	query := generic.Query2[position, rotation]()
 	b.StartTimer()
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := generic.Query2[position, rotation](&world)
+		q := query.Build(&world)
 		b.StartTimer()
-		for query.Next() {
-			pos := query.Get1()
+		for q.Next() {
+			pos := q.Get1()
 			_ = pos
 		}
 	}
 }
+
 func runArcheQuery5C(b *testing.B, count int) {
 	b.StopTimer()
 	world := ecs.NewWorld()
@@ -97,7 +99,7 @@ func runArcheQuery5C(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Query(id0, id1, id2, id3, id4)
+		query := world.Query(ecs.All(id0, id1, id2, id3, id4))
 		b.StartTimer()
 		for query.Next() {
 			t1 := (*testStruct0)(query.Get(id0))
@@ -124,14 +126,16 @@ func runArcheQueryGeneric5C(b *testing.B, count int) {
 		entity := world.NewEntity()
 		world.Add(entity, id0, id1, id2, id3, id4)
 	}
+
+	query := generic.Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4]()
 	b.StartTimer()
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := generic.Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&world)
+		q := query.Build(&world)
 		b.StartTimer()
-		for query.Next() {
-			t1, t2, t3, t4, t5 := query.GetAll()
+		for q.Next() {
+			t1, t2, t3, t4, t5 := q.GetAll()
 			_, _, _, _, _ = t1, t2, t3, t4, t5
 		}
 	}
@@ -164,7 +168,7 @@ func runArcheQuery1kArch(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Query(6)
+		query := world.Query(ecs.All(6))
 		b.StartTimer()
 		for query.Next() {
 			pos := (*position)(query.Get(6))
@@ -200,7 +204,7 @@ func runArcheFilter1kArch(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Filter(filter.All(6))
+		query := world.Query(filter.All(6))
 		b.StartTimer()
 		for query.Next() {
 			pos := (*position)(query.Get(6))
@@ -224,7 +228,7 @@ func runArcheWorld(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Query(posID, rotID)
+		query := world.Query(ecs.All(posID, rotID))
 		b.StartTimer()
 		for query.Next() {
 			entity := query.Entity()
@@ -251,7 +255,7 @@ func runArcheWorldGeneric(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Query(posID, rotID)
+		query := world.Query(ecs.All(posID, rotID))
 		b.StartTimer()
 		for query.Next() {
 			entity := query.Entity()
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index f7b7a85..9de0822 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -4,12 +4,11 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestArchetype(t *testing.T) {
-	comps := []base.ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
@@ -74,14 +73,14 @@ func TestArchetype(t *testing.T) {
 }
 
 func TestNewArchetype(t *testing.T) {
-	comps := []base.ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 	arch := archetype{}
 	arch.Init(32, comps...)
 
-	comps = []base.ComponentType{
+	comps = []componentType{
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 		{ID: 0, Type: reflect.TypeOf(position{})},
 	}
@@ -93,7 +92,7 @@ func TestNewArchetype(t *testing.T) {
 
 func BenchmarkArchetypeAccess_1000(b *testing.B) {
 	b.StopTimer()
-	comps := []base.ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
@@ -120,7 +119,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 
 func BenchmarkArchetypeAccessUnsafe_1000(b *testing.B) {
 	b.StopTimer()
-	comps := []base.ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
diff --git a/internal/base/bitmask_test.go b/ecs/bitmask_test.go
similarity index 98%
rename from internal/base/bitmask_test.go
rename to ecs/bitmask_test.go
index 6febfc2..8191835 100644
--- a/internal/base/bitmask_test.go
+++ b/ecs/bitmask_test.go
@@ -1,4 +1,4 @@
-package base
+package ecs
 
 import (
 	"testing"
diff --git a/ecs/query_test.go b/ecs/query_test.go
index ec7825d..b13a79a 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -6,6 +6,16 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestMask(t *testing.T) {
+	filter := All(0, 2, 4)
+	other := NewBitMask(0, 1, 2)
+
+	assert.False(t, filter.Matches(other))
+
+	other = NewBitMask(0, 1, 2, 3, 4)
+	assert.True(t, filter.Matches(other))
+}
+
 func TestQuery(t *testing.T) {
 	w := NewWorld()
 
@@ -24,7 +34,7 @@ func TestQuery(t *testing.T) {
 	w.Add(e3, rotID)
 	w.Add(e4, rotID)
 
-	q := w.Query(posID, rotID)
+	q := w.Query(All(posID, rotID))
 	cnt := 0
 	for q.Next() {
 		ent := q.Entity()
@@ -37,7 +47,7 @@ func TestQuery(t *testing.T) {
 	}
 	assert.Equal(t, 2, cnt)
 
-	q = w.Query(posID)
+	q = w.Query(All(posID))
 	cnt = 0
 	for q.Next() {
 		ent := q.Entity()
@@ -48,7 +58,7 @@ func TestQuery(t *testing.T) {
 	}
 	assert.Equal(t, 3, cnt)
 
-	q = w.Query(rotID)
+	q = w.Query(All(rotID))
 	cnt = 0
 	for q.Next() {
 		ent := q.Entity()
@@ -63,7 +73,7 @@ func TestQuery(t *testing.T) {
 
 	assert.Panics(t, func() { q.Next() })
 
-	q = w.Query(rotID).Not(posID)
+	q = w.Query(All(rotID).ButNot(posID))
 
 	cnt = 0
 	for q.Next() {
@@ -79,7 +89,7 @@ func TestInterface(t *testing.T) {
 	posID := ComponentID[position](&w)
 	rotID := ComponentID[rotation](&w)
 
-	q := w.Query(posID, rotID)
+	q := w.Query(All(posID, rotID))
 	var q2 EntityIter = &q
 	_ = q2
 }
diff --git a/ecs/types_test.go b/ecs/types_test.go
deleted file mode 100644
index dc9c02f..0000000
--- a/ecs/types_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package ecs
-
-import (
-	"testing"
-
-	"github.com/mlange-42/arche/internal/base"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestBitMask(t *testing.T) {
-	mask := base.NewBitMask(ID(1), ID(2), ID(13), ID(27))
-
-	assert.Equal(t, uint(4), mask.TotalBitsSet())
-
-	assert.True(t, mask.Get(1))
-	assert.True(t, mask.Get(2))
-	assert.True(t, mask.Get(13))
-	assert.True(t, mask.Get(27))
-
-	assert.False(t, mask.Get(0))
-	assert.False(t, mask.Get(3))
-}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index a7c9fe3..613e91e 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -5,7 +5,6 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -62,10 +61,10 @@ func TestWorldComponents(t *testing.T) {
 
 	w.Remove(e2, posID)
 
-	maskNone := base.NewBitMask()
-	maskPos := base.NewBitMask(posID)
-	maskRot := base.NewBitMask(rotID)
-	maskPosRot := base.NewBitMask(posID, rotID)
+	maskNone := NewBitMask()
+	maskPos := NewBitMask(posID)
+	maskRot := NewBitMask(rotID)
+	maskPosRot := NewBitMask(posID, rotID)
 
 	archNone, ok := w.findArchetype(maskNone)
 	assert.True(t, ok)
@@ -256,7 +255,7 @@ func TestWorldIter(t *testing.T) {
 	}
 
 	for i := 0; i < 100; i++ {
-		query := world.Query(posID, rotID)
+		query := world.Query(All(posID, rotID))
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
 			_ = pos
@@ -265,16 +264,16 @@ func TestWorldIter(t *testing.T) {
 	}
 
 	for i := 0; i < maskTotalBits-1; i++ {
-		query := world.Query(posID, rotID)
+		query := world.Query(All(posID, rotID))
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
 			_ = pos
 			break
 		}
 	}
-	query := world.Query(posID, rotID)
+	query := world.Query(All(posID, rotID))
 
-	assert.Panics(t, func() { world.Query(posID, rotID) })
+	assert.Panics(t, func() { world.Query(All(posID, rotID)) })
 
 	query.Close()
 	assert.Panics(t, func() { query.Close() })
@@ -292,15 +291,15 @@ func TestWorldLock(t *testing.T) {
 		world.Add(entity, posID)
 	}
 
-	query1 := world.Query(posID)
-	query2 := world.Query(posID)
+	query1 := world.Query(All(posID))
+	query2 := world.Query(All(posID))
 	assert.True(t, world.IsLocked())
 	query1.Close()
 	assert.True(t, world.IsLocked())
 	query2.Close()
 	assert.False(t, world.IsLocked())
 
-	query1 = world.Query(posID)
+	query1 = world.Query(All(posID))
 
 	assert.Panics(t, func() { world.NewEntity() })
 	assert.Panics(t, func() { world.RemEntity(entity) })
@@ -366,7 +365,7 @@ func Test1000Archetypes(t *testing.T) {
 	ids[9] = ComponentID[testStruct9](&w)
 
 	for i := 0; i < 1024; i++ {
-		mask := bitMask(i)
+		mask := BitMask(i)
 		add := make([]ID, 0, 10)
 		for j := 0; j < 10; j++ {
 			id := ID(j)
@@ -380,7 +379,7 @@ func Test1000Archetypes(t *testing.T) {
 	assert.Equal(t, 1024, w.archetypes.Len())
 
 	cnt := 0
-	query := w.Query(0, 7)
+	query := w.Query(All(0, 7))
 	for query.Next() {
 		cnt++
 	}
diff --git a/filter/filter_test.go b/filter/filter_test.go
index 06b69ef..f22050e 100644
--- a/filter/filter_test.go
+++ b/filter/filter_test.go
@@ -18,12 +18,12 @@ type rotation struct {
 
 func TestLogicFilters(t *testing.T) {
 
-	hasA := All(0)
-	hasB := All(1)
-	hasAll := All(0, 1)
-	hasNone := All()
+	hasA := ecs.All(0)
+	hasB := ecs.All(1)
+	hasAll := ecs.All(0, 1)
+	hasNone := ecs.All()
 
-	var filter ecs.MaskFilter
+	var filter ecs.Filter
 	filter = hasA
 	assert.True(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
@@ -117,7 +117,7 @@ func TestFilter(t *testing.T) {
 	w.Add(e3, rotID)
 	w.Add(e4, rotID)
 
-	q := w.Filter(All(posID, rotID))
+	q := w.Query(All(posID, rotID))
 	cnt := 0
 	for q.Next() {
 		ent := q.Entity()
@@ -130,7 +130,7 @@ func TestFilter(t *testing.T) {
 	}
 	assert.Equal(t, 2, cnt)
 
-	q = w.Filter(All(posID))
+	q = w.Query(All(posID))
 	cnt = 0
 	for q.Next() {
 		ent := q.Entity()
@@ -141,7 +141,7 @@ func TestFilter(t *testing.T) {
 	}
 	assert.Equal(t, 3, cnt)
 
-	q = w.Filter(All(rotID))
+	q = w.Query(All(rotID))
 	cnt = 0
 	for q.Next() {
 		ent := q.Entity()
@@ -156,7 +156,7 @@ func TestFilter(t *testing.T) {
 
 	assert.Panics(t, func() { q.Next() })
 
-	q = w.Filter(&AND{L: All(rotID), R: NotANY(All(posID))})
+	q = w.Query(&AND{L: All(rotID), R: NotANY(All(posID))})
 
 	cnt = 0
 	for q.Next() {
@@ -166,7 +166,7 @@ func TestFilter(t *testing.T) {
 	assert.Equal(t, 2, cnt)
 }
 
-func match(f ecs.MaskFilter, m Mask) bool {
+func match(f ecs.Filter, m Mask) bool {
 	return f.Matches(m.BitMask)
 }
 
@@ -175,7 +175,7 @@ func TestInterface(t *testing.T) {
 
 	posID := ecs.ComponentID[position](&w)
 
-	f := w.Filter(All(posID))
+	f := w.Query(All(posID))
 	var f2 ecs.EntityIter = &f
 	_ = f2
 }
diff --git a/generic/generic_test.go b/generic/generic_test.go
index 0322c78..f46711d 100644
--- a/generic/generic_test.go
+++ b/generic/generic_test.go
@@ -7,28 +7,99 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-type testStruct0 struct{ val int32 }
+//lint:ignore U1000 test type
+type testStruct0 struct{ val int8 }
+
+//lint:ignore U1000 test type
 type testStruct1 struct{ val int32 }
-type testStruct2 struct{ val int32 }
-type testStruct3 struct{ val int32 }
-type testStruct4 struct{ val int32 }
-type testStruct5 struct{ val int32 }
-type testStruct6 struct{ val int32 }
-type testStruct7 struct{ val int32 }
-type testStruct8 struct{ val int32 }
-type testStruct9 struct{ val int32 }
+
+//lint:ignore U1000 test type
+type testStruct2 struct {
+	val  int32
+	val2 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct3 struct {
+	val  int32
+	val2 int32
+	val3 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct4 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct5 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+	val5 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct6 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+	val5 int32
+	val6 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct7 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+	val5 int32
+	val6 int32
+	val7 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct8 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+	val5 int32
+	val6 int32
+	val7 int32
+	val8 int32
+}
+
+//lint:ignore U1000 test type
+type testStruct9 struct {
+	val  int32
+	val2 int32
+	val3 int32
+	val4 int32
+	val5 int32
+	val6 int32
+	val7 int32
+	val8 int32
+	val9 int32
+}
 
 func registerAll(w *ecs.World) []ecs.ID {
-	_ = testStruct0{1}
-	_ = testStruct1{1}
-	_ = testStruct2{1}
-	_ = testStruct3{1}
-	_ = testStruct4{1}
-	_ = testStruct5{1}
-	_ = testStruct6{1}
-	_ = testStruct7{1}
-	_ = testStruct8{1}
-	_ = testStruct9{1}
+	_ = testStruct0{}
+	_ = testStruct1{}
+	_ = testStruct2{}
+	_ = testStruct3{}
+	_ = testStruct4{}
+	_ = testStruct5{}
+	_ = testStruct6{}
+	_ = testStruct7{}
+	_ = testStruct8{}
+	_ = testStruct9{}
 
 	ids := make([]ecs.ID, 10)
 	ids[0] = ecs.ComponentID[testStruct0](w)
@@ -51,7 +122,7 @@ func TestGenericMap(t *testing.T) {
 
 	e0 := w.NewEntity()
 
-	Add[testStruct0](&w, e0)
+	Add1[testStruct0](&w, e0)
 	has := get.Has(e0)
 	_ = get.Get(e0)
 	assert.True(t, has)
@@ -71,10 +142,10 @@ func TestGenericAddRemove(t *testing.T) {
 
 	e0 := w.NewEntity()
 
-	Add[testStruct0](&w, e0)
+	Add1[testStruct0](&w, e0)
 	_ = get.Has(e0)
 	_ = get.Get(e0)
-	Remove[testStruct0](&w, e0)
+	Remove1[testStruct0](&w, e0)
 
 	Add2[testStruct0, testStruct1](&w, e0)
 	Remove2[testStruct0, testStruct1](&w, e0)
@@ -94,8 +165,8 @@ func TestGenericAssignRemove(t *testing.T) {
 
 	e0 := w.NewEntity()
 
-	Assign(&w, e0, &testStruct0{})
-	Remove[testStruct0](&w, e0)
+	Assign1(&w, e0, &testStruct0{})
+	Remove1[testStruct0](&w, e0)
 
 	Assign2(&w, e0, &testStruct0{}, &testStruct1{})
 	Remove2[testStruct0, testStruct1](&w, e0)
diff --git a/generic/query_generic_test.go b/generic/query_generic_test.go
index 7ba3a3f..0a7f569 100644
--- a/generic/query_generic_test.go
+++ b/generic/query_generic_test.go
@@ -1,10 +1,10 @@
 package generic
 
 import (
+	"reflect"
 	"testing"
 
 	"github.com/mlange-42/arche/ecs"
-	"github.com/mlange-42/arche/internal/base"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -23,9 +23,7 @@ func TestQuery0(t *testing.T) {
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
-	query :=
-		Query0(&w).
-			Not(Mask1[testStruct9](&w))
+	query := Query0().Not(Mask1[testStruct9]()).Build(&w)
 
 	for query.Next() {
 		cnt++
@@ -48,9 +46,7 @@ func TestQuery1(t *testing.T) {
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
-	query :=
-		Query1[testStruct0](&w).
-			Not(Mask1[testStruct9](&w))
+	query := Query1[testStruct0]().Not(Mask1[testStruct9]()).Build(&w)
 
 	for query.Next() {
 		c0 := query.Get1()
@@ -80,9 +76,7 @@ func TestQuery2(t *testing.T) {
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
-	query :=
-		Query2[testStruct0, testStruct1](&w).
-			Not(Mask1[testStruct9](&w))
+	query := Query2[testStruct0, testStruct1]().Not(Mask1[testStruct9]()).Build(&w)
 
 	for query.Next() {
 		c1 := query.Get1()
@@ -115,16 +109,17 @@ func TestQuery3(t *testing.T) {
 	w.Assign(e1, 1, &testStruct1{3})
 	w.Assign(e2, 1, &testStruct1{4})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
-	w.Assign(e2, 2, &testStruct2{5})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
+	w.Assign(e2, 2, &testStruct2{5, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
 	query :=
-		Query3[testStruct0, testStruct1, testStruct2](&w).
-			Not(Mask1[testStruct9](&w))
+		Query3[testStruct0, testStruct1, testStruct2]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
 
 	for query.Next() {
 		c1 := query.Get1()
@@ -160,20 +155,22 @@ func TestQuery4(t *testing.T) {
 	w.Assign(e1, 1, &testStruct1{3})
 	w.Assign(e2, 1, &testStruct1{3})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
-	w.Assign(e2, 2, &testStruct2{4})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
+	w.Assign(e2, 2, &testStruct2{4, 0})
 
-	w.Assign(e0, 3, &testStruct3{4})
-	w.Assign(e1, 3, &testStruct3{5})
-	w.Assign(e2, 3, &testStruct3{5})
+	w.Assign(e0, 3, &testStruct3{4, 0, 0})
+	w.Assign(e1, 3, &testStruct3{5, 0, 0})
+	w.Assign(e2, 3, &testStruct3{5, 0, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
 	query :=
-		Query4[testStruct0, testStruct1, testStruct2, testStruct3](&w).
-			Not(Mask1[testStruct9](&w))
+		Query4[testStruct0, testStruct1, testStruct2, testStruct3]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
+
 	for query.Next() {
 		c1 := query.Get1()
 		c2 := query.Get2()
@@ -211,24 +208,26 @@ func TestQuery5(t *testing.T) {
 	w.Assign(e1, 1, &testStruct1{3})
 	w.Assign(e2, 1, &testStruct1{3})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
-	w.Assign(e2, 2, &testStruct2{4})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
+	w.Assign(e2, 2, &testStruct2{4, 0})
 
-	w.Assign(e0, 3, &testStruct3{4})
-	w.Assign(e1, 3, &testStruct3{5})
-	w.Assign(e2, 3, &testStruct3{5})
+	w.Assign(e0, 3, &testStruct3{4, 0, 0})
+	w.Assign(e1, 3, &testStruct3{5, 0, 0})
+	w.Assign(e2, 3, &testStruct3{5, 0, 0})
 
-	w.Assign(e0, 4, &testStruct4{5})
-	w.Assign(e1, 4, &testStruct4{6})
-	w.Assign(e2, 4, &testStruct4{6})
+	w.Assign(e0, 4, &testStruct4{5, 0, 0, 0})
+	w.Assign(e1, 4, &testStruct4{6, 0, 0, 0})
+	w.Assign(e2, 4, &testStruct4{6, 0, 0, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
 	query :=
-		Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&w).
-			Not(Mask1[testStruct9](&w))
+		Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
+
 	for query.Next() {
 		c1 := query.Get1()
 		c2 := query.Get2()
@@ -269,28 +268,30 @@ func TestQuery6(t *testing.T) {
 	w.Assign(e1, 1, &testStruct1{3})
 	w.Assign(e2, 1, &testStruct1{3})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
-	w.Assign(e2, 2, &testStruct2{4})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
+	w.Assign(e2, 2, &testStruct2{4, 0})
 
-	w.Assign(e0, 3, &testStruct3{4})
-	w.Assign(e1, 3, &testStruct3{5})
-	w.Assign(e2, 3, &testStruct3{5})
+	w.Assign(e0, 3, &testStruct3{4, 0, 0})
+	w.Assign(e1, 3, &testStruct3{5, 0, 0})
+	w.Assign(e2, 3, &testStruct3{5, 0, 0})
 
-	w.Assign(e0, 4, &testStruct4{5})
-	w.Assign(e1, 4, &testStruct4{6})
-	w.Assign(e2, 4, &testStruct4{6})
+	w.Assign(e0, 4, &testStruct4{5, 0, 0, 0})
+	w.Assign(e1, 4, &testStruct4{6, 0, 0, 0})
+	w.Assign(e2, 4, &testStruct4{6, 0, 0, 0})
 
-	w.Assign(e0, 5, &testStruct5{6})
-	w.Assign(e1, 5, &testStruct5{7})
-	w.Assign(e2, 5, &testStruct5{7})
+	w.Assign(e0, 5, &testStruct5{6, 0, 0, 0, 0})
+	w.Assign(e1, 5, &testStruct5{7, 0, 0, 0, 0})
+	w.Assign(e2, 5, &testStruct5{7, 0, 0, 0, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
 	query :=
-		Query6[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5](&w).
-			Not(Mask1[testStruct9](&w))
+		Query6[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
+
 	for query.Next() {
 		c1 := query.Get1()
 		c2 := query.Get2()
@@ -334,32 +335,37 @@ func TestQuery7(t *testing.T) {
 	w.Assign(e1, 1, &testStruct1{3})
 	w.Assign(e2, 1, &testStruct1{3})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
-	w.Assign(e2, 2, &testStruct2{4})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
+	w.Assign(e2, 2, &testStruct2{4, 0})
 
-	w.Assign(e0, 3, &testStruct3{4})
-	w.Assign(e1, 3, &testStruct3{5})
-	w.Assign(e2, 3, &testStruct3{5})
+	w.Assign(e0, 3, &testStruct3{4, 0, 0})
+	w.Assign(e1, 3, &testStruct3{5, 0, 0})
+	w.Assign(e2, 3, &testStruct3{5, 0, 0})
 
-	w.Assign(e0, 4, &testStruct4{5})
-	w.Assign(e1, 4, &testStruct4{6})
-	w.Assign(e2, 4, &testStruct4{6})
+	w.Assign(e0, 4, &testStruct4{5, 0, 0, 0})
+	w.Assign(e1, 4, &testStruct4{6, 0, 0, 0})
+	w.Assign(e2, 4, &testStruct4{6, 0, 0, 0})
 
-	w.Assign(e0, 5, &testStruct5{6})
-	w.Assign(e1, 5, &testStruct5{7})
-	w.Assign(e2, 5, &testStruct5{7})
+	w.Assign(e0, 5, &testStruct5{6, 0, 0, 0, 0})
+	w.Assign(e1, 5, &testStruct5{7, 0, 0, 0, 0})
+	w.Assign(e2, 5, &testStruct5{7, 0, 0, 0, 0})
 
-	w.Assign(e0, 6, &testStruct5{7})
-	w.Assign(e1, 6, &testStruct5{8})
-	w.Assign(e2, 6, &testStruct5{8})
+	w.Assign(e0, 6, &testStruct5{7, 0, 0, 0, 0})
+	w.Assign(e1, 6, &testStruct5{8, 0, 0, 0, 0})
+	w.Assign(e2, 6, &testStruct5{8, 0, 0, 0, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
 	cnt := 0
 	query :=
-		Query7[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5, testStruct6](&w).
-			Not(Mask1[testStruct9](&w))
+		Query7[
+			testStruct0, testStruct1, testStruct2, testStruct3, testStruct4,
+			testStruct5, testStruct6,
+		]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
+
 	for query.Next() {
 		c1 := query.Get1()
 		c2 := query.Get2()
@@ -404,23 +410,23 @@ func TestQuery8(t *testing.T) {
 	w.Assign(e0, 1, &testStruct1{2})
 	w.Assign(e1, 1, &testStruct1{3})
 
-	w.Assign(e0, 2, &testStruct2{3})
-	w.Assign(e1, 2, &testStruct2{4})
+	w.Assign(e0, 2, &testStruct2{3, 0})
+	w.Assign(e1, 2, &testStruct2{4, 0})
 
-	w.Assign(e0, 3, &testStruct3{4})
-	w.Assign(e1, 3, &testStruct3{5})
+	w.Assign(e0, 3, &testStruct3{4, 0, 0})
+	w.Assign(e1, 3, &testStruct3{5, 0, 0})
 
-	w.Assign(e0, 4, &testStruct4{5})
-	w.Assign(e1, 4, &testStruct4{6})
+	w.Assign(e0, 4, &testStruct4{5, 0, 0, 0})
+	w.Assign(e1, 4, &testStruct4{6, 0, 0, 0})
 
-	w.Assign(e0, 5, &testStruct5{6})
-	w.Assign(e1, 5, &testStruct5{7})
+	w.Assign(e0, 5, &testStruct5{6, 0, 0, 0, 0})
+	w.Assign(e1, 5, &testStruct5{7, 0, 0, 0, 0})
 
-	w.Assign(e0, 6, &testStruct5{7})
-	w.Assign(e1, 6, &testStruct5{8})
+	w.Assign(e0, 6, &testStruct5{7, 0, 0, 0, 0})
+	w.Assign(e1, 6, &testStruct5{8, 0, 0, 0, 0})
 
-	w.Assign(e0, 7, &testStruct5{8})
-	w.Assign(e1, 7, &testStruct5{9})
+	w.Assign(e0, 7, &testStruct5{8, 0, 0, 0, 0})
+	w.Assign(e1, 7, &testStruct5{9, 0, 0, 0, 0})
 
 	w.Assign(e2, 9, &testStruct9{})
 
@@ -428,8 +434,10 @@ func TestQuery8(t *testing.T) {
 	query :=
 		Query8[
 			testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5, testStruct6, testStruct7](&w).
-			Not(Mask1[testStruct9](&w))
+			testStruct4, testStruct5, testStruct6, testStruct7,
+		]().
+			Not(Mask1[testStruct9]()).
+			Build(&w)
 
 	for query.Next() {
 		c1 := query.Get1()
@@ -464,49 +472,116 @@ func TestQuery8(t *testing.T) {
 }
 
 func TestGenericMask(t *testing.T) {
-	w := ecs.NewWorld()
-
 	assert.Equal(t,
-		Mask1[testStruct0](&w).BitMask,
-		base.NewBitMask(0),
+		Mask1[testStruct0](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+		},
 	)
 
 	assert.Equal(t,
-		Mask2[testStruct0, testStruct1](&w).BitMask,
-		base.NewBitMask(0, 1),
+		Mask2[testStruct0, testStruct1](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+		},
 	)
 
 	assert.Equal(t,
-		Mask3[testStruct0, testStruct1, testStruct2](&w).BitMask,
-		base.NewBitMask(0, 1, 2),
+		Mask3[testStruct0, testStruct1, testStruct2](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+		},
 	)
 
 	assert.Equal(t,
-		Mask4[testStruct0, testStruct1, testStruct2, testStruct3](&w).BitMask,
-		base.NewBitMask(0, 1, 2, 3),
+		Mask4[testStruct0, testStruct1, testStruct2, testStruct3](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+			typeOf[testStruct3](),
+		},
 	)
 
 	assert.Equal(t,
 		Mask5[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4](&w).BitMask,
-		base.NewBitMask(0, 1, 2, 3, 4),
+			testStruct4](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+			typeOf[testStruct3](),
+			typeOf[testStruct4](),
+		},
 	)
 
 	assert.Equal(t,
 		Mask6[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5](&w).BitMask,
-		base.NewBitMask(0, 1, 2, 3, 4, 5),
+			testStruct4, testStruct5](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+			typeOf[testStruct3](),
+			typeOf[testStruct4](),
+			typeOf[testStruct5](),
+		},
 	)
 
 	assert.Equal(t,
 		Mask7[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5, testStruct6](&w).BitMask,
-		base.NewBitMask(0, 1, 2, 3, 4, 5, 6),
+			testStruct4, testStruct5, testStruct6](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+			typeOf[testStruct3](),
+			typeOf[testStruct4](),
+			typeOf[testStruct5](),
+			typeOf[testStruct6](),
+		},
 	)
 
 	assert.Equal(t,
 		Mask8[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5, testStruct6, testStruct7](&w).BitMask,
-		base.NewBitMask(0, 1, 2, 3, 4, 5, 6, 7),
+			testStruct4, testStruct5, testStruct6, testStruct7](),
+		[]reflect.Type{
+			typeOf[testStruct0](),
+			typeOf[testStruct1](),
+			typeOf[testStruct2](),
+			typeOf[testStruct3](),
+			typeOf[testStruct4](),
+			typeOf[testStruct5](),
+			typeOf[testStruct6](),
+			typeOf[testStruct7](),
+		},
 	)
 }
+
+func TestQueryGeneric(t *testing.T) {
+	count := 1000
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[testStruct2](&world)
+	rotID := ecs.ComponentID[testStruct3](&world)
+
+	for i := 0; i < count; i++ {
+		entity := world.NewEntity()
+		world.Add(entity, posID, rotID)
+	}
+	query := Query2[testStruct2, testStruct3]()
+
+	q := query.Build(&world)
+	cnt := 0
+	for q.Next() {
+		s1 := q.Get1()
+		s2 := q.Get2()
+		_ = s1
+		_ = s2
+		cnt++
+	}
+	assert.Equal(t, count, cnt)
+}
diff --git a/internal/base/types_test.go b/internal/base/types_test.go
deleted file mode 100644
index 29d902c..0000000
--- a/internal/base/types_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package base
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestMask(t *testing.T) {
-	filter := NewMask(0, 2, 4)
-	other := NewBitMask(0, 1, 2)
-
-	assert.False(t, filter.Matches(other))
-
-	other = NewBitMask(0, 1, 2, 3, 4)
-	assert.True(t, filter.Matches(other))
-}