Repository: mlange-42/arche
Pull request #266 (2023-05-03 17:08:53)

Code cleanups

coveralls: ## Pull Request Test Coverage Report for [Build 4874613059](https://coveralls.io/builds/59586943)

* **49** of **49**   **(100.0%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/59586943/badge)](https://coveralls.io/builds/59586943) |
| :-- | --: |
| Change from base [Build 4874413404](https://coveralls.io/builds/59585976): |  0.0% |
| Covered Lines: | 3773 |
| Relevant Lines: | 3773 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 537f14b..091e7ca 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -13,40 +13,73 @@ var layoutSize = unsafe.Sizeof(layout{})
 
 // archetypeNode is a node in the archetype graph.
 type archetypeNode struct {
-	mask              Mask                  // Mask of the archetype
+	Mask              Mask                  // Mask of the archetype
 	Ids               []ID                  // List of component IDs
+	Types             []reflect.Type        // Component type per column
 	archetype         *archetype            // The single archetype for nodes without entity
 	archetypes        pagedSlice[archetype] // Storage for archetypes in nodes with entity relation
 	archetypeMap      map[Entity]*archetype // Mapping from relation targets to archetypes
 	freeIndices       []int32               // Indices of free/inactive archetypes
 	TransitionAdd     idMap[*archetypeNode] // Mapping from component ID to add to the resulting archetype
 	TransitionRemove  idMap[*archetypeNode] // Mapping from component ID to remove to the resulting archetype
-	relation          int8                  // The node's relation component ID. Negative value stands for no relation
+	Relation          int8                  // The node's relation component ID. Negative value stands for no relation
 	zeroValue         []byte                // Used as source for setting storage to zero
 	zeroPointer       unsafe.Pointer        // Points to zeroValue for fast access
 	capacityIncrement uint32                // Capacity increment
+	IsActive          bool
 }
 
 // Creates a new archetypeNode
-func newArchetypeNode(mask Mask, relation int8, capacityIncrement int) archetypeNode {
+func newArchetypeNode(mask Mask, relation int8, capacityIncrement int, components []componentType) archetypeNode {
 	var arch map[Entity]*archetype
 	if relation >= 0 {
 		arch = map[Entity]*archetype{}
 	}
+	ids := make([]ID, len(components))
+	types := make([]reflect.Type, len(components))
+
+	var maxSize uintptr = 0
+	prev := -1
+	for i, c := range components {
+		if int(c.ID) <= prev {
+			panic("component arguments must be sorted by ID")
+		}
+		prev = int(c.ID)
+
+		ids[i] = c.ID
+		types[i] = c.Type
+		size, align := c.Type.Size(), uintptr(c.Type.Align())
+		size = (size + (align - 1)) / align * align
+		if size > maxSize {
+			maxSize = size
+		}
+	}
+
+	var zeroValue []byte
+	var zeroPointer unsafe.Pointer
+	if maxSize > 0 {
+		zeroValue = make([]byte, maxSize)
+		zeroPointer = unsafe.Pointer(&zeroValue[0])
+	}
+
 	return archetypeNode{
-		mask:              mask,
+		Mask:              mask,
+		Ids:               ids,
+		Types:             types,
 		archetypeMap:      arch,
 		TransitionAdd:     newIDMap[*archetypeNode](),
 		TransitionRemove:  newIDMap[*archetypeNode](),
-		relation:          relation,
+		Relation:          relation,
 		capacityIncrement: uint32(capacityIncrement),
+		zeroValue:         zeroValue,
+		zeroPointer:       zeroPointer,
 	}
 }
 
 // Matches the archetype node against a filter.
 // Ignores the relation target.
 func (a *archetypeNode) Matches(f Filter) bool {
-	return f.Matches(a.mask, nil)
+	return f.Matches(a.Mask, nil)
 }
 
 // Archetypes of the node.
@@ -66,7 +99,7 @@ func (a *archetypeNode) Archetypes() archetypes {
 //
 // The target is ignored if the node has no relation component.
 func (a *archetypeNode) GetArchetype(target Entity) *archetype {
-	if a.relation >= 0 {
+	if a.Relation >= 0 {
 		return a.archetypeMap[target]
 	}
 	return a.archetype
@@ -80,7 +113,7 @@ func (a *archetypeNode) SetArchetype(arch *archetype) {
 }
 
 // CreateArchetype creates a new archetype in nodes with relation component.
-func (a *archetypeNode) CreateArchetype(target Entity, components ...componentType) *archetype {
+func (a *archetypeNode) CreateArchetype(target Entity) *archetype {
 	var arch *archetype
 	var archIndex int32
 	lenFree := len(a.freeIndices)
@@ -93,7 +126,7 @@ func (a *archetypeNode) CreateArchetype(target Entity, components ...componentTy
 		a.archetypes.Add(archetype{})
 		archIndex := a.archetypes.Len() - 1
 		arch = a.archetypes.Get(archIndex)
-		arch.Init(a, archIndex, true, target, components...)
+		arch.Init(a, archIndex, true, target)
 	}
 	a.archetypeMap[target] = arch
 	return arch
@@ -102,7 +135,7 @@ func (a *archetypeNode) CreateArchetype(target Entity, components ...componentTy
 // RemoveArchetype de-activates an archetype.
 // The archetype will be re-used by CreateArchetype.
 func (a *archetypeNode) RemoveArchetype(arch *archetype) {
-	delete(a.archetypeMap, arch.Relation)
+	delete(a.archetypeMap, arch.RelationTarget)
 	idx := arch.index
 	a.freeIndices = append(a.freeIndices, idx)
 	a.archetypes.Get(idx).Deactivate()
@@ -110,12 +143,7 @@ func (a *archetypeNode) RemoveArchetype(arch *archetype) {
 
 // HasRelation returns whether the node has a relation component.
 func (a *archetypeNode) HasRelation() bool {
-	return a.relation >= 0
-}
-
-// IsActive returns whether the node is active, i.e. has archetypes.
-func (a *archetypeNode) IsActive() bool {
-	return a.Ids != nil
+	return a.Relation >= 0
 }
 
 // Stats generates statistics for an archetype node.
@@ -154,7 +182,7 @@ func (a *archetypeNode) Stats(reg *componentRegistry[ID]) stats.NodeStats {
 	return stats.NodeStats{
 		ArchetypeCount:       int(numArches),
 		ActiveArchetypeCount: int(numArches) - len(a.freeIndices),
-		IsActive:             a.IsActive(),
+		IsActive:             a.IsActive,
 		HasRelation:          a.HasRelation(),
 		Components:           aCompCount,
 		ComponentIDs:         ids,
@@ -169,7 +197,7 @@ func (a *archetypeNode) Stats(reg *componentRegistry[ID]) stats.NodeStats {
 
 // UpdateStats updates statistics for an archetype node.
 func (a *archetypeNode) UpdateStats(stats *stats.NodeStats, reg *componentRegistry[ID]) {
-	if !a.IsActive() {
+	if !a.IsActive {
 		return
 	}
 
@@ -216,13 +244,13 @@ type archetypeAccess struct {
 	Mask              Mask           // Archetype's mask
 	basePointer       unsafe.Pointer // Pointer to the first component column layout.
 	entityPointer     unsafe.Pointer // Pointer to the entity storage
-	Relation          Entity
+	RelationTarget    Entity
 	RelationComponent int8
 }
 
 // Matches checks if the archetype matches the given mask.
 func (a *archetype) Matches(f Filter) bool {
-	return f.Matches(a.Mask, &a.Relation)
+	return f.Matches(a.Mask, &a.RelationTarget)
 }
 
 // GetEntity returns the entity at the given index
@@ -235,11 +263,6 @@ func (a *archetypeAccess) Get(index uintptr, id ID) unsafe.Pointer {
 	return a.getLayout(id).Get(index)
 }
 
-// GetEntity returns the entity at the given index
-func (a *archetypeAccess) GetRelation() Entity {
-	return a.Relation
-}
-
 // HasComponent returns whether the archetype contains the given component ID.
 func (a *archetypeAccess) HasComponent(id ID) bool {
 	return a.getLayout(id).pointer != nil
@@ -283,28 +306,12 @@ type archetype struct {
 }
 
 // Init initializes an archetype
-func (a *archetype) Init(node *archetypeNode, index int32, forStorage bool, relation Entity, components ...componentType) {
-	var mask Mask
-	if !node.IsActive() {
-		node.Ids = make([]ID, len(components))
-
-		var maxSize uintptr = 0
-		for i, c := range components {
-			node.Ids[i] = c.ID
-			size, align := c.Type.Size(), uintptr(c.Type.Align())
-			size = (size + (align - 1)) / align * align
-			if size > maxSize {
-				maxSize = size
-			}
-		}
-
-		if maxSize > 0 {
-			node.zeroValue = make([]byte, maxSize)
-			node.zeroPointer = unsafe.Pointer(&node.zeroValue[0])
-		}
+func (a *archetype) Init(node *archetypeNode, index int32, forStorage bool, relation Entity) {
+	if !node.IsActive {
+		node.IsActive = true
 	}
 
-	a.buffers = make([]reflect.Value, len(components))
+	a.buffers = make([]reflect.Value, len(node.Ids))
 	a.layouts = make([]layout, MaskTotalBits)
 	a.indices = newIDMap[uint32]()
 	a.index = index
@@ -314,32 +321,26 @@ func (a *archetype) Init(node *archetypeNode, index int32, forStorage bool, rela
 		cap = int(node.capacityIncrement)
 	}
 
-	prev := -1
-	for i, c := range components {
-		if int(c.ID) <= prev {
-			panic("component arguments must be sorted by ID")
-		}
-		prev = int(c.ID)
-		mask.Set(c.ID, true)
-
-		size, align := c.Type.Size(), uintptr(c.Type.Align())
+	for i, id := range node.Ids {
+		tp := node.Types[i]
+		size, align := tp.Size(), uintptr(tp.Align())
 		size = (size + (align - 1)) / align * align
 
-		a.buffers[i] = reflect.New(reflect.ArrayOf(cap, c.Type)).Elem()
-		a.layouts[c.ID] = layout{
+		a.buffers[i] = reflect.New(reflect.ArrayOf(cap, tp)).Elem()
+		a.layouts[id] = layout{
 			a.buffers[i].Addr().UnsafePointer(),
 			size,
 		}
-		a.indices.Set(c.ID, uint32(i))
+		a.indices.Set(id, uint32(i))
 	}
 	a.entityBuffer = reflect.New(reflect.ArrayOf(cap, entityType)).Elem()
 
 	a.archetypeAccess = archetypeAccess{
 		basePointer:       unsafe.Pointer(&a.layouts[0]),
 		entityPointer:     a.entityBuffer.Addr().UnsafePointer(),
-		Mask:              mask,
-		Relation:          relation,
-		RelationComponent: node.relation,
+		Mask:              node.Mask,
+		RelationTarget:    relation,
+		RelationComponent: node.Relation,
 	}
 
 	a.graphNode = node
@@ -486,7 +487,7 @@ func (a *archetype) Deactivate() {
 // Activate reactivates a de-activated archetype.
 func (a *archetype) Activate(target Entity, index int32) {
 	a.index = index
-	a.Relation = target
+	a.RelationTarget = target
 }
 
 // IsActive returns whether the archetype is active.
diff --git a/ecs/batch.go b/ecs/batch.go
index 5b8afb2..c43d95a 100644
--- a/ecs/batch.go
+++ b/ecs/batch.go
@@ -45,7 +45,7 @@ func (b *Batch) Remove(filter Filter, callback func(Query), comps ...ID) {
 //   - when called for a component that is not a relation.
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
-// See also [World.SetRelation].
+// See also [Relations.Set] and [Relations.SetBatch].
 func (b *Batch) SetRelation(filter Filter, comp ID, target Entity, callback func(Query)) {
 	b.world.setRelationBatch(filter, comp, target, callback)
 }
diff --git a/ecs/query.go b/ecs/query.go
index d0ac888..fb8c842 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -114,7 +114,7 @@ func (q *Query) Relation(comp ID) Entity {
 	if q.access.RelationComponent != int8(comp) {
 		panic(fmt.Sprintf("entity has no component %v, or it is not a relation component", q.world.registry.Types[comp]))
 	}
-	return q.access.GetRelation()
+	return q.access.RelationTarget
 }
 
 // RelationUnchecked returns the target entity for an entity relation.
@@ -125,7 +125,7 @@ func (q *Query) Relation(comp ID) Entity {
 // GetRelationUnchecked is an optimized version of [Query.Relation].
 // Does not check that the component ID is applicable.
 func (q *Query) RelationUnchecked(comp ID) Entity {
-	return q.access.GetRelation()
+	return q.access.RelationTarget
 }
 
 // Step advances the query iterator by the given number of entities.
@@ -226,20 +226,19 @@ func (q *Query) nextNode() bool {
 			continue
 		}
 
-		if arches.Len() > 1 {
-			if rf, ok := q.filter.(*relationFilter); ok {
-				target := rf.Target
-				if arch, ok := n.archetypeMap[target]; ok && arch.Len() > 0 {
-					q.archetypes = nil
-					q.archIndex = arch.index
-					q.access = &arch.archetypeAccess
-					q.entityIndex = 0
-					q.entityIndexMax = uintptr(arch.Len()) - 1
-					return true
-				}
-				continue
+		if rf, ok := q.filter.(*relationFilter); ok {
+			target := rf.Target
+			if arch, ok := n.archetypeMap[target]; ok && arch.Len() > 0 {
+				q.archetypes = nil
+				q.archIndex = arch.index
+				q.access = &arch.archetypeAccess
+				q.entityIndex = 0
+				q.entityIndexMax = uintptr(arch.Len()) - 1
+				return true
 			}
+			continue
 		}
+
 		q.archetypes = arches
 		q.archIndex = -1
 		q.entityIndex = 0
@@ -278,25 +277,20 @@ func (q *Query) countEntities() int {
 	var i int32
 	for i = 0; i < len; i++ {
 		nd := q.nodes.Get(i)
-		if !nd.IsActive() {
+		if !nd.IsActive || !nd.Matches(q.filter) {
 			continue
 		}
-		if !nd.Matches(q.filter) {
-			continue
-		}
-		arches := nd.Archetypes()
-		nArch := arches.Len()
 
-		if nArch > 1 {
-			if rf, ok := q.filter.(*relationFilter); ok {
-				target := rf.Target
-				if arch, ok := nd.archetypeMap[target]; ok {
-					count += arch.Len()
-				}
-				continue
+		if rf, ok := q.filter.(*relationFilter); ok {
+			target := rf.Target
+			if arch, ok := nd.archetypeMap[target]; ok {
+				count += arch.Len()
 			}
+			continue
 		}
 
+		arches := nd.Archetypes()
+		nArch := arches.Len()
 		var j int32
 		for j = 0; j < nArch; j++ {
 			a := arches.Get(j)
diff --git a/ecs/relations.go b/ecs/relations.go
index b2d52c5..abfa278 100644
--- a/ecs/relations.go
+++ b/ecs/relations.go
@@ -40,3 +40,18 @@ func (r *Relations) GetUnchecked(entity Entity, comp ID) Entity {
 func (r *Relations) Set(entity Entity, comp ID, target Entity) {
 	r.world.setRelation(entity, comp, target)
 }
+
+// SetBatch sets the [Relation] target for many entities, matching a filter.
+//
+// If the callback argument is given, it is called with a [Query] over the affected entities,
+// one Query for each affected archetype.
+//
+// Panics:
+//   - when called for a missing component.
+//   - when called for a component that is not a relation.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Relations.Set] and [Batch.SetRelation].
+func (r *Relations) SetBatch(filter Filter, comp ID, target Entity, callback func(Query)) {
+	r.world.setRelationBatch(filter, comp, target, callback)
+}
diff --git a/ecs/util.go b/ecs/util.go
index 561b473..39aa558 100644
--- a/ecs/util.go
+++ b/ecs/util.go
@@ -26,6 +26,30 @@ func capacityU32(size, increment uint32) uint32 {
 	return cap
 }
 
+func maskToTypes(mask Mask, reg *componentRegistry[ID]) []componentType {
+	count := int(mask.TotalBitsSet())
+	types := make([]componentType, count)
+
+	start := 0
+	end := MaskTotalBits
+	if mask.Lo == 0 {
+		start = wordSize
+	}
+	if mask.Hi == 0 {
+		end = wordSize
+	}
+
+	idx := 0
+	for i := start; i < end; i++ {
+		id := ID(i)
+		if mask.Get(id) {
+			types[idx] = componentType{ID: id, Type: reg.Types[id]}
+			idx++
+		}
+	}
+	return types
+}
+
 // Manages locks by mask bits.
 //
 // The number of simultaneous locks at a given time is limited to [MaskTotalBits].
@@ -110,8 +134,8 @@ func debugPrintWorld(w *World) string {
 	var i int32
 	for i = 0; i < ln; i++ {
 		nd := w.graph.Get(i)
-		if !nd.IsActive() {
-			fmt.Fprint(&sb, "Node ??? (inactive)\n")
+		if !nd.IsActive {
+			fmt.Fprintf(&sb, "Node %v (inactive)\n", nd.Ids)
 			continue
 		}
 		nodeArches := nd.Archetypes()
@@ -121,9 +145,9 @@ func debugPrintWorld(w *World) string {
 		for j = 0; j < ln2; j++ {
 			a := nodeArches.Get(j)
 			if a.IsActive() {
-				fmt.Fprintf(&sb, "   Arch %v (%d entities)\n", a.Relation, a.Len())
+				fmt.Fprintf(&sb, "   Arch %v (%d entities)\n", a.RelationTarget, a.Len())
 			} else {
-				fmt.Fprintf(&sb, "   Arch %v (inactive)\n", a.Relation)
+				fmt.Fprintf(&sb, "   Arch %v (inactive)\n", a.RelationTarget)
 			}
 		}
 	}
diff --git a/ecs/world.go b/ecs/world.go
index e2f68c9..8cc1fc1 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -145,7 +145,7 @@ func (w *World) NewEntity(comps ...ID) Entity {
 	entity := w.createEntity(arch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -183,7 +183,7 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -210,7 +210,7 @@ func (w *World) newEntityTarget(targetID ID, target Entity, comps ...ID) Entity
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -243,7 +243,7 @@ func (w *World) newEntityTargetWith(targetID ID, target Entity, comps ...Compone
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -259,7 +259,7 @@ func (w *World) newEntities(count int, targetID int8, target Entity, comps ...ID
 		for i = 0; i < cnt; i++ {
 			idx := startIdx + i
 			entity := arch.GetEntity(uintptr(idx))
-			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 		}
 	}
 
@@ -290,7 +290,7 @@ func (w *World) newEntitiesWith(count int, targetID int8, target Entity, comps .
 		for i = 0; i < cnt; i++ {
 			idx := startIdx + i
 			entity := arch.GetEntity(uintptr(idx))
-			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.Relation, false})
+			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
 		}
 	}
 
@@ -328,7 +328,7 @@ func (w *World) RemoveEntity(entity Entity) {
 
 	if w.listener != nil {
 		lock := w.lock()
-		w.listener(&EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.graphNode.Ids, nil, -1, oldArch.Relation, Entity{}, false})
+		w.listener(&EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.graphNode.Ids, nil, -1, oldArch.RelationTarget, Entity{}, false})
 		w.unlock(lock)
 	}
 
@@ -367,7 +367,7 @@ func (w *World) removeEntities(filter Filter) int {
 	var n int32
 	for n = 0; n < numNodes; n++ {
 		node := w.graph.Get(n)
-		if !node.IsActive() {
+		if !node.IsActive {
 			continue
 		}
 		if !node.Matches(filter) {
@@ -395,7 +395,7 @@ func (w *World) removeEntities(filter Filter) int {
 			for j = 0; j < len; j++ {
 				entity := arch.GetEntity(j)
 				if w.listener != nil {
-					w.listener(&EntityEvent{entity, arch.Mask, Mask{}, nil, node.Ids, nil, -1, arch.Relation, Entity{}, false})
+					w.listener(&EntityEvent{entity, arch.Mask, Mask{}, nil, node.Ids, nil, -1, arch.RelationTarget, Entity{}, false})
 				}
 				index := &w.entities[entity.id]
 				index.arch = nil
@@ -511,6 +511,7 @@ func (w *World) Assign(entity Entity, comps ...Component) {
 	w.assign(entity, -1, Entity{}, comps...)
 }
 
+// assign with relation target.
 func (w *World) assign(entity Entity, relation int8, target Entity, comps ...Component) {
 	len := len(comps)
 	if len == 0 {
@@ -572,6 +573,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.exchange(entity, add, rem, -1, Entity{})
 }
 
+// exchange with relation target.
 func (w *World) exchange(entity Entity, add []ID, rem []ID, relation int8, target Entity) {
 	w.checkLocked()
 
@@ -586,7 +588,7 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation int8, targe
 	oldArch := index.arch
 
 	oldMask := oldArch.Mask
-	mask := w.getExchangeMask(oldArch, add, rem)
+	mask := w.getExchangeMask(oldMask, add, rem)
 
 	if relation >= 0 {
 		if !mask.Get(ID(relation)) {
@@ -596,7 +598,7 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation int8, targe
 			panic("can't add relation: this is not a relation component")
 		}
 	} else {
-		target = oldArch.Relation
+		target = oldArch.RelationTarget
 	}
 
 	oldIDs := oldArch.Components()
@@ -622,20 +624,20 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation int8, targe
 	w.cleanupArchetype(oldArch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.graphNode.Ids, 0, oldArch.Relation, arch.Relation, false})
+		w.listener(&EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.graphNode.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, false})
 	}
 }
 
-func (w *World) getExchangeMask(arch *archetype, add []ID, rem []ID) Mask {
-	mask := arch.Mask
+// Modify a mask by adding and removing IDs.
+func (w *World) getExchangeMask(mask Mask, add []ID, rem []ID) Mask {
 	for _, comp := range add {
-		if arch.HasComponent(comp) {
+		if mask.Get(comp) {
 			panic(fmt.Sprintf("entity already has component of type %v, can't add", w.registry.Types[comp]))
 		}
 		mask.Set(comp, true)
 	}
 	for _, comp := range rem {
-		if !arch.HasComponent(comp) {
+		if !mask.Get(comp) {
 			panic(fmt.Sprintf("entity does not have a component of type %v, can't remove", w.registry.Types[comp]))
 		}
 		mask.Set(comp, false)
@@ -691,9 +693,9 @@ func (w *World) exchangeBatch(filter Filter, add []ID, rem []ID, callback func(Q
 }
 
 func (w *World) exchangeArch(oldArch *archetype, oldArchLen uint32, add []ID, rem []ID) (*archetype, uint32) {
-	mask := w.getExchangeMask(oldArch, add, rem)
+	mask := w.getExchangeMask(oldArch.Mask, add, rem)
 	oldIDs := oldArch.Components()
-	arch := w.findOrCreateArchetype(oldArch, add, rem, oldArch.Relation)
+	arch := w.findOrCreateArchetype(oldArch, add, rem, oldArch.RelationTarget)
 
 	startIdx := uintptr(arch.Len())
 	count := uintptr(oldArchLen)
@@ -742,7 +744,7 @@ func (w *World) getRelation(entity Entity, comp ID) Entity {
 	index := &w.entities[entity.id]
 	w.checkRelation(index.arch, comp)
 
-	return index.arch.Relation
+	return index.arch.RelationTarget
 }
 
 // getRelationUnchecked returns the target entity for an entity relation.
@@ -751,7 +753,7 @@ func (w *World) getRelation(entity Entity, comp ID) Entity {
 // Does not check if the entity is alive or that the component ID is applicable.
 func (w *World) getRelationUnchecked(entity Entity, comp ID) Entity {
 	index := &w.entities[entity.id]
-	return index.arch.Relation
+	return index.arch.RelationTarget
 }
 
 // setRelation sets the target entity for an entity relation.
@@ -779,7 +781,7 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 
 	oldArch := index.arch
 
-	if index.arch.Relation == target {
+	if index.arch.RelationTarget == target {
 		return
 	}
 
@@ -806,10 +808,11 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 	w.cleanupArchetype(oldArch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, arch.Mask, arch.Mask, nil, nil, arch.graphNode.Ids, 0, oldArch.Relation, arch.Relation, true})
+		w.listener(&EntityEvent{entity, arch.Mask, arch.Mask, nil, nil, arch.graphNode.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, true})
 	}
 }
 
+// set relation target in batches.
 func (w *World) setRelationBatch(filter Filter, comp ID, target Entity, callback func(Query)) {
 	w.checkLocked()
 
@@ -850,7 +853,7 @@ func (w *World) setRelationBatch(filter Filter, comp ID, target Entity, callback
 func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID, target Entity) (*archetype, uint32, uint32) {
 	w.checkRelation(oldArch, comp)
 
-	if oldArch.Relation == target {
+	if oldArch.RelationTarget == target {
 		return oldArch, 0, oldArchLen
 	}
 	oldIDs := oldArch.Components()
@@ -890,7 +893,7 @@ func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID,
 }
 
 func (w *World) checkRelation(arch *archetype, comp ID) {
-	if arch.graphNode.relation != int8(comp) {
+	if arch.graphNode.Relation != int8(comp) {
 		w.relationError(arch, comp)
 	}
 }
@@ -921,7 +924,7 @@ func (w *World) Reset() {
 	var i int32
 	for i = 0; i < len; i++ {
 		node := w.graph.Get(i)
-		if !node.IsActive() {
+		if !node.IsActive {
 			continue
 		}
 		arches := node.Archetypes()
@@ -932,7 +935,7 @@ func (w *World) Reset() {
 			if !arch.IsActive() {
 				continue
 			}
-			if arch.HasRelation() && !arch.Relation.IsZero() {
+			if arch.HasRelation() && !arch.RelationTarget.IsZero() {
 				w.removeArchetype(arch)
 			} else {
 				arch.Reset()
@@ -1045,7 +1048,7 @@ func (w *World) Stats() *stats.WorldStats {
 		node := w.graph.Get(i)
 		nodeStats := &w.stats.Nodes[i]
 		node.UpdateStats(nodeStats, &w.registry)
-		if node.IsActive() {
+		if node.IsActive {
 			memory += nodeStats.Memory
 			cntActive++
 		}
@@ -1053,7 +1056,7 @@ func (w *World) Stats() *stats.WorldStats {
 	for i = cntOld; i < cntNew; i++ {
 		node := w.graph.Get(i)
 		w.stats.Nodes = append(w.stats.Nodes, node.Stats(&w.registry))
-		if node.IsActive() {
+		if node.IsActive {
 			memory += w.stats.Nodes[i].Memory
 			cntActive++
 		}
@@ -1273,7 +1276,7 @@ func (w *World) findArchetypeSlow(mask Mask) (*archetypeNode, bool) {
 	var i int32
 	for i = 0; i < length; i++ {
 		node := w.graph.Get(i)
-		if node.mask == mask {
+		if node.Mask == mask {
 			return node, true
 		}
 	}
@@ -1287,7 +1290,9 @@ func (w *World) createArchetypeNode(mask Mask, relation int8) *archetypeNode {
 		capInc = w.config.RelationCapacityIncrement
 	}
 
-	w.graph.Add(newArchetypeNode(mask, relation, capInc))
+	types := maskToTypes(mask, &w.registry)
+
+	w.graph.Add(newArchetypeNode(mask, relation, capInc, types))
 	node := w.graph.Get(w.graph.Len() - 1)
 	w.relationNodes = append(w.relationNodes, node)
 	return node
@@ -1297,36 +1302,14 @@ func (w *World) createArchetypeNode(mask Mask, relation int8) *archetypeNode {
 // Initializes the archetype with a capacity according to CapacityIncrement if forStorage is true,
 // and with a capacity of 1 otherwise.
 func (w *World) createArchetype(node *archetypeNode, target Entity, forStorage bool) *archetype {
-	mask := node.mask
-	count := int(mask.TotalBitsSet())
-	types := make([]componentType, count)
-
-	start := 0
-	end := MaskTotalBits
-	if mask.Lo == 0 {
-		start = wordSize
-	}
-	if mask.Hi == 0 {
-		end = wordSize
-	}
-
-	idx := 0
-	for i := start; i < end; i++ {
-		id := ID(i)
-		if mask.Get(id) {
-			types[idx] = componentType{ID: id, Type: w.registry.Types[id]}
-			idx++
-		}
-	}
-
 	var arch *archetype
 	if node.HasRelation() {
-		arch = node.CreateArchetype(target, types...)
+		arch = node.CreateArchetype(target)
 	} else {
 		w.archetypes.Add(archetype{})
 		archIndex := w.archetypes.Len() - 1
 		arch = w.archetypes.Get(archIndex)
-		arch.Init(node, archIndex, forStorage, target, types...)
+		arch.Init(node, archIndex, forStorage, target)
 		node.SetArchetype(arch)
 	}
 
@@ -1341,24 +1324,20 @@ func (w *World) getArchetypes(filter Filter) archetypePointers {
 	var i int32
 	for i = 0; i < ln; i++ {
 		nd := w.graph.Get(i)
-		if !nd.IsActive() {
+		if !nd.IsActive || !nd.Matches(filter) {
 			continue
 		}
-		if !nd.Matches(filter) {
+
+		if rf, ok := filter.(*relationFilter); ok {
+			target := rf.Target
+			if arch, ok := nd.archetypeMap[target]; ok {
+				arches = append(arches, arch)
+			}
 			continue
 		}
+
 		nodeArches := nd.Archetypes()
 		ln2 := int32(nodeArches.Len())
-		if ln2 > 1 {
-			if rf, ok := filter.(*relationFilter); ok {
-				target := rf.Target
-				if arch, ok := nd.archetypeMap[target]; ok {
-					arches = append(arches, arch)
-				}
-				continue
-			}
-		}
-
 		var j int32
 		for j = 0; j < ln2; j++ {
 			a := nodeArches.Get(j)
@@ -1376,7 +1355,7 @@ func (w *World) cleanupArchetype(arch *archetype) {
 	if arch.Len() > 0 || !arch.graphNode.HasRelation() {
 		return
 	}
-	target := arch.Relation
+	target := arch.RelationTarget
 	if target.IsZero() || w.Alive(target) {
 		return
 	}
@@ -1429,14 +1408,14 @@ func (w *World) notifyQuery(batchArch *batchArchetype) {
 
 	event := EntityEvent{
 		Entity{}, Mask{}, arch.Mask, batchArch.Added, batchArch.Removed, arch.graphNode.Ids, 1,
-		Entity{}, arch.Relation, false,
+		Entity{}, arch.RelationTarget, false,
 	}
 
 	oldArch := batchArch.OldArchetype
 	if oldArch != nil {
-		event.OldMask = oldArch.graphNode.mask
+		event.OldMask = oldArch.graphNode.Mask
 		event.AddedRemoved = 0
-		event.OldTarget = oldArch.Relation
+		event.OldTarget = oldArch.RelationTarget
 		event.TargetChanged = event.OldMask == event.NewMask
 	}
 
diff --git a/generic/compiled.go b/generic/compiled.go
index a595f4c..f7f674c 100644
--- a/generic/compiled.go
+++ b/generic/compiled.go
@@ -76,3 +76,20 @@ func (q *compiledQuery) Reset(targetOnly bool) {
 		q.compiled = false
 	}
 }
+
+// Register the compiledQuery for caching.
+func (q *compiledQuery) Register(w *ecs.World) {
+	q.cachedFilter = w.Cache().Register(q.filter)
+	q.filter = &q.cachedFilter
+	q.locked = true
+}
+
+// Unregister the compiledQuery from caching.
+func (q *compiledQuery) Unregister(w *ecs.World) {
+	if cf, ok := q.filter.(*ecs.CachedFilter); ok {
+		q.filter = w.Cache().Unregister(cf)
+	} else {
+		panic("can't unregister a filter that is not cached")
+	}
+	q.locked = false
+}
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index a984292..57a14ba 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -14,7 +14,7 @@
 type Map{{ .Index }}{{ .TypesFull }} struct {
 	world *ecs.World
 	mask ecs.Mask
-	target int8
+	relation int8
 	ids []ecs.ID
 	{{ .IDTypes }}
 }
@@ -29,9 +29,9 @@ func NewMap{{ .Index }}{{ .TypesFull }}(w *ecs.World, relation ...Comp) Map{{ .I
 	}
 	m.ids = []ecs.ID{ {{ .IDList }} }
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -60,13 +60,7 @@ func (m *Map{{ .Index }}{{ .Types }}) GetUnchecked(entity ecs.Entity) ({{ .Types
 //
 // See also [ecs.World.NewEntity].
 func (m *Map{{ .Index }}{{ .Types }}) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map{{ .Index }}'s components.
@@ -75,14 +69,7 @@ func (m *Map{{ .Index }}{{ .Types }}) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map{{ .Index }}.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map{{ .Index }}{{ .Types }}) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map{{ .Index }}'s components.
@@ -94,17 +81,7 @@ func (m *Map{{ .Index }}{{ .Types }}) NewBatch(count int, target ...ecs.Entity)
 //
 // See also [Map{{ .Index }}.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map{{ .Index }}{{ .Types }}) NewQuery(count int, target ...ecs.Entity) Query{{ .Index }}{{ .Types }} {
-	var query ecs.Query
-	
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query{{ .Index }}{{ .Types }}{
 		Query: query,
 		{{ .IDAssign2 }}
@@ -121,10 +98,10 @@ func (m *Map{{ .Index }}{{ .Types }}) NewWith({{ .Arguments }}, target ...ecs.En
 	if len(target) == 0 {
 		return m.world.NewEntityWith({{ .Components }})
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
-	return ecs.NewBuilderWith(m.world, {{ .Components }}).WithRelation(uint8(m.target)).New(target[0])
+	return ecs.NewBuilderWith(m.world, {{ .Components }}).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map{{ .Index }}'s components to the given entity.
diff --git a/generic/generate/query.go.txt b/generic/generate/query.go.txt
index b59f47a..857eb7d 100644
--- a/generic/generate/query.go.txt
+++ b/generic/generate/query.go.txt
@@ -104,21 +104,14 @@ func (q *Filter{{ .Index }}{{ .Types }}) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter{{ .Index }}{{ .Types }}) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter{{ .Index }}{{ .Types }}) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query{{ .Index }} is a generic query iterator for {{ .NumberStr }} components.
diff --git a/generic/map_generated.go b/generic/map_generated.go
index 3621591..eee8ca7 100644
--- a/generic/map_generated.go
+++ b/generic/map_generated.go
@@ -19,11 +19,11 @@ import (
 //	entity := mapper.NewEntity()
 //	a := mapper.Get(entity)
 type Map1[A any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
 }
 
 // NewMap1 creates a new Map1 object.
@@ -36,9 +36,9 @@ func NewMap1[A any](w *ecs.World, relation ...Comp) Map1[A] {
 	}
 	m.ids = []ecs.ID{m.id0}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -67,13 +67,7 @@ func (m *Map1[A]) GetUnchecked(entity ecs.Entity) *A {
 //
 // See also [ecs.World.NewEntity].
 func (m *Map1[A]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map1's components.
@@ -82,14 +76,7 @@ func (m *Map1[A]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map1.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map1[A]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map1's components.
@@ -101,17 +88,7 @@ func (m *Map1[A]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map1.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map1[A]) NewQuery(count int, target ...ecs.Entity) Query1[A] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query1[A]{
 		Query: query,
 		id0:   m.id0,
@@ -127,10 +104,10 @@ func (m *Map1[A]) NewWith(a *A, target ...ecs.Entity) ecs.Entity {
 	if len(target) == 0 {
 		return m.world.NewEntityWith(ecs.Component{ID: m.id0, Comp: a})
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
-	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a}).WithRelation(uint8(m.target)).New(target[0])
+	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a}).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map1's components to the given entity.
@@ -185,12 +162,12 @@ func (m *Map1[A]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b := mapper.Get(entity)
 type Map2[A any, B any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
 }
 
 // NewMap2 creates a new Map2 object.
@@ -204,9 +181,9 @@ func NewMap2[A any, B any](w *ecs.World, relation ...Comp) Map2[A, B] {
 	}
 	m.ids = []ecs.ID{m.id0, m.id1}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -237,13 +214,7 @@ func (m *Map2[A, B]) GetUnchecked(entity ecs.Entity) (*A, *B) {
 //
 // See also [ecs.World.NewEntity].
 func (m *Map2[A, B]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map2's components.
@@ -252,14 +223,7 @@ func (m *Map2[A, B]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map2.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map2[A, B]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map2's components.
@@ -271,17 +235,7 @@ func (m *Map2[A, B]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map2.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map2[A, B]) NewQuery(count int, target ...ecs.Entity) Query2[A, B] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query2[A, B]{
 		Query: query,
 		id0:   m.id0,
@@ -300,12 +254,12 @@ func (m *Map2[A, B]) NewWith(a *A, b *B, target ...ecs.Entity) ecs.Entity {
 			ecs.Component{ID: m.id1, Comp: b},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
 		ecs.Component{ID: m.id1, Comp: b},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map2's components to the given entity.
@@ -361,13 +315,13 @@ func (m *Map2[A, B]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c := mapper.Get(entity)
 type Map3[A any, B any, C any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
 }
 
 // NewMap3 creates a new Map3 object.
@@ -382,9 +336,9 @@ func NewMap3[A any, B any, C any](w *ecs.World, relation ...Comp) Map3[A, B, C]
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -417,13 +371,7 @@ func (m *Map3[A, B, C]) GetUnchecked(entity ecs.Entity) (*A, *B, *C) {
 //
 // See also [ecs.World.NewEntity].
 func (m *Map3[A, B, C]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map3's components.
@@ -432,14 +380,7 @@ func (m *Map3[A, B, C]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map3.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map3[A, B, C]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map3's components.
@@ -451,17 +392,7 @@ func (m *Map3[A, B, C]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map3.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map3[A, B, C]) NewQuery(count int, target ...ecs.Entity) Query3[A, B, C] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query3[A, B, C]{
 		Query: query,
 		id0:   m.id0,
@@ -482,13 +413,13 @@ func (m *Map3[A, B, C]) NewWith(a *A, b *B, c *C, target ...ecs.Entity) ecs.Enti
 			ecs.Component{ID: m.id2, Comp: c},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
 		ecs.Component{ID: m.id1, Comp: b},
 		ecs.Component{ID: m.id2, Comp: c},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map3's components to the given entity.
@@ -545,14 +476,14 @@ func (m *Map3[A, B, C]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c, d := mapper.Get(entity)
 type Map4[A any, B any, C any, D any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
-	id3    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
+	id3      ecs.ID
 }
 
 // NewMap4 creates a new Map4 object.
@@ -568,9 +499,9 @@ func NewMap4[A any, B any, C any, D any](w *ecs.World, relation ...Comp) Map4[A,
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2, m.id3}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -605,13 +536,7 @@ func (m *Map4[A, B, C, D]) GetUnchecked(entity ecs.Entity) (*A, *B, *C, *D) {
 //
 // See also [ecs.World.NewEntity].
 func (m *Map4[A, B, C, D]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map4's components.
@@ -620,14 +545,7 @@ func (m *Map4[A, B, C, D]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map4.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map4[A, B, C, D]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map4's components.
@@ -639,17 +557,7 @@ func (m *Map4[A, B, C, D]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map4.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map4[A, B, C, D]) NewQuery(count int, target ...ecs.Entity) Query4[A, B, C, D] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query4[A, B, C, D]{
 		Query: query,
 		id0:   m.id0,
@@ -672,14 +580,14 @@ func (m *Map4[A, B, C, D]) NewWith(a *A, b *B, c *C, d *D, target ...ecs.Entity)
 			ecs.Component{ID: m.id3, Comp: d},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
 		ecs.Component{ID: m.id1, Comp: b},
 		ecs.Component{ID: m.id2, Comp: c},
 		ecs.Component{ID: m.id3, Comp: d},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map4's components to the given entity.
@@ -737,15 +645,15 @@ func (m *Map4[A, B, C, D]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c, d, e := mapper.Get(entity)
 type Map5[A any, B any, C any, D any, E any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
-	id3    ecs.ID
-	id4    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
+	id3      ecs.ID
+	id4      ecs.ID
 }
 
 // NewMap5 creates a new Map5 object.
@@ -762,9 +670,9 @@ func NewMap5[A any, B any, C any, D any, E any](w *ecs.World, relation ...Comp)
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2, m.id3, m.id4}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -801,13 +709,7 @@ func (m *Map5[A, B, C, D, E]) GetUnchecked(entity ecs.Entity) (*A, *B, *C, *D, *
 //
 // See also [ecs.World.NewEntity].
 func (m *Map5[A, B, C, D, E]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map5's components.
@@ -816,14 +718,7 @@ func (m *Map5[A, B, C, D, E]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map5.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map5[A, B, C, D, E]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map5's components.
@@ -835,17 +730,7 @@ func (m *Map5[A, B, C, D, E]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map5.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map5[A, B, C, D, E]) NewQuery(count int, target ...ecs.Entity) Query5[A, B, C, D, E] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query5[A, B, C, D, E]{
 		Query: query,
 		id0:   m.id0,
@@ -870,7 +755,7 @@ func (m *Map5[A, B, C, D, E]) NewWith(a *A, b *B, c *C, d *D, e *E, target ...ec
 			ecs.Component{ID: m.id4, Comp: e},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
@@ -878,7 +763,7 @@ func (m *Map5[A, B, C, D, E]) NewWith(a *A, b *B, c *C, d *D, e *E, target ...ec
 		ecs.Component{ID: m.id2, Comp: c},
 		ecs.Component{ID: m.id3, Comp: d},
 		ecs.Component{ID: m.id4, Comp: e},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map5's components to the given entity.
@@ -937,16 +822,16 @@ func (m *Map5[A, B, C, D, E]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c, d, e, f := mapper.Get(entity)
 type Map6[A any, B any, C any, D any, E any, F any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
-	id3    ecs.ID
-	id4    ecs.ID
-	id5    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
+	id3      ecs.ID
+	id4      ecs.ID
+	id5      ecs.ID
 }
 
 // NewMap6 creates a new Map6 object.
@@ -964,9 +849,9 @@ func NewMap6[A any, B any, C any, D any, E any, F any](w *ecs.World, relation ..
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2, m.id3, m.id4, m.id5}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -1005,13 +890,7 @@ func (m *Map6[A, B, C, D, E, F]) GetUnchecked(entity ecs.Entity) (*A, *B, *C, *D
 //
 // See also [ecs.World.NewEntity].
 func (m *Map6[A, B, C, D, E, F]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map6's components.
@@ -1020,14 +899,7 @@ func (m *Map6[A, B, C, D, E, F]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map6.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map6[A, B, C, D, E, F]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map6's components.
@@ -1039,17 +911,7 @@ func (m *Map6[A, B, C, D, E, F]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map6.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map6[A, B, C, D, E, F]) NewQuery(count int, target ...ecs.Entity) Query6[A, B, C, D, E, F] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query6[A, B, C, D, E, F]{
 		Query: query,
 		id0:   m.id0,
@@ -1076,7 +938,7 @@ func (m *Map6[A, B, C, D, E, F]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F, tar
 			ecs.Component{ID: m.id5, Comp: f},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
@@ -1085,7 +947,7 @@ func (m *Map6[A, B, C, D, E, F]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F, tar
 		ecs.Component{ID: m.id3, Comp: d},
 		ecs.Component{ID: m.id4, Comp: e},
 		ecs.Component{ID: m.id5, Comp: f},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map6's components to the given entity.
@@ -1145,17 +1007,17 @@ func (m *Map6[A, B, C, D, E, F]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c, d, e, f, g := mapper.Get(entity)
 type Map7[A any, B any, C any, D any, E any, F any, G any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
-	id3    ecs.ID
-	id4    ecs.ID
-	id5    ecs.ID
-	id6    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
+	id3      ecs.ID
+	id4      ecs.ID
+	id5      ecs.ID
+	id6      ecs.ID
 }
 
 // NewMap7 creates a new Map7 object.
@@ -1174,9 +1036,9 @@ func NewMap7[A any, B any, C any, D any, E any, F any, G any](w *ecs.World, rela
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2, m.id3, m.id4, m.id5, m.id6}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -1217,13 +1079,7 @@ func (m *Map7[A, B, C, D, E, F, G]) GetUnchecked(entity ecs.Entity) (*A, *B, *C,
 //
 // See also [ecs.World.NewEntity].
 func (m *Map7[A, B, C, D, E, F, G]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map7's components.
@@ -1232,14 +1088,7 @@ func (m *Map7[A, B, C, D, E, F, G]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map7.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map7[A, B, C, D, E, F, G]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map7's components.
@@ -1251,17 +1100,7 @@ func (m *Map7[A, B, C, D, E, F, G]) NewBatch(count int, target ...ecs.Entity) {
 //
 // See also [Map7.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map7[A, B, C, D, E, F, G]) NewQuery(count int, target ...ecs.Entity) Query7[A, B, C, D, E, F, G] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query7[A, B, C, D, E, F, G]{
 		Query: query,
 		id0:   m.id0,
@@ -1290,7 +1129,7 @@ func (m *Map7[A, B, C, D, E, F, G]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F,
 			ecs.Component{ID: m.id6, Comp: g},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
@@ -1300,7 +1139,7 @@ func (m *Map7[A, B, C, D, E, F, G]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F,
 		ecs.Component{ID: m.id4, Comp: e},
 		ecs.Component{ID: m.id5, Comp: f},
 		ecs.Component{ID: m.id6, Comp: g},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map7's components to the given entity.
@@ -1361,18 +1200,18 @@ func (m *Map7[A, B, C, D, E, F, G]) RemoveEntities(exclusive bool) int {
 //	entity := mapper.NewEntity()
 //	a, b, c, d, e, f, g, h := mapper.Get(entity)
 type Map8[A any, B any, C any, D any, E any, F any, G any, H any] struct {
-	world  *ecs.World
-	mask   ecs.Mask
-	target int8
-	ids    []ecs.ID
-	id0    ecs.ID
-	id1    ecs.ID
-	id2    ecs.ID
-	id3    ecs.ID
-	id4    ecs.ID
-	id5    ecs.ID
-	id6    ecs.ID
-	id7    ecs.ID
+	world    *ecs.World
+	mask     ecs.Mask
+	relation int8
+	ids      []ecs.ID
+	id0      ecs.ID
+	id1      ecs.ID
+	id2      ecs.ID
+	id3      ecs.ID
+	id4      ecs.ID
+	id5      ecs.ID
+	id6      ecs.ID
+	id7      ecs.ID
 }
 
 // NewMap8 creates a new Map8 object.
@@ -1392,9 +1231,9 @@ func NewMap8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.Worl
 	}
 	m.ids = []ecs.ID{m.id0, m.id1, m.id2, m.id3, m.id4, m.id5, m.id6, m.id7}
 	m.mask = ecs.All(m.ids...)
-	m.target = -1
+	m.relation = -1
 	if len(relation) > 0 {
-		m.target = int8(ecs.TypeID(w, relation[0]))
+		m.relation = int8(ecs.TypeID(w, relation[0]))
 	}
 	return m
 }
@@ -1437,13 +1276,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) GetUnchecked(entity ecs.Entity) (*A, *B,
 //
 // See also [ecs.World.NewEntity].
 func (m *Map8[A, B, C, D, E, F, G, H]) New(target ...ecs.Entity) ecs.Entity {
-	if len(target) == 0 {
-		return m.world.NewEntity(m.ids...)
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	return ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).New(target[0])
+	return newEntity(m.world, m.ids, m.relation, target...)
 }
 
 // NewBatch creates entities with the Map8's components.
@@ -1452,14 +1285,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) New(target ...ecs.Entity) ecs.Entity {
 //
 // See also [Map8.NewQuery] and [ecs.Batch.NewBatch].
 func (m *Map8[A, B, C, D, E, F, G, H]) NewBatch(count int, target ...ecs.Entity) {
-	if len(target) == 0 {
-		ecs.NewBuilder(m.world, m.ids...).NewBatch(count)
-		return
-	}
-	if m.target < 0 {
-		panic("map has no relation defined")
-	}
-	ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewBatch(count, target[0])
+	newBatch(m.world, count, m.ids, m.relation, target...)
 }
 
 // NewQuery creates entities with the Map8's components.
@@ -1471,17 +1297,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewBatch(count int, target ...ecs.Entity)
 //
 // See also [Map8.NewBatch] and [ecs.Builder.NewQuery].
 func (m *Map8[A, B, C, D, E, F, G, H]) NewQuery(count int, target ...ecs.Entity) Query8[A, B, C, D, E, F, G, H] {
-	var query ecs.Query
-
-	if len(target) == 0 {
-		query = ecs.NewBuilder(m.world, m.ids...).NewQuery(count)
-	} else {
-		if m.target < 0 {
-			panic("map has no relation defined")
-		}
-		query = ecs.NewBuilder(m.world, m.ids...).WithRelation(uint8(m.target)).NewQuery(count, target[0])
-	}
-
+	query := newQuery(m.world, count, m.ids, m.relation, target...)
 	return Query8[A, B, C, D, E, F, G, H]{
 		Query: query,
 		id0:   m.id0,
@@ -1512,7 +1328,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewWith(a *A, b *B, c *C, d *D, e *E, f *
 			ecs.Component{ID: m.id7, Comp: h},
 		)
 	}
-	if m.target < 0 {
+	if m.relation < 0 {
 		panic("map has no relation defined")
 	}
 	return ecs.NewBuilderWith(m.world, ecs.Component{ID: m.id0, Comp: a},
@@ -1523,7 +1339,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewWith(a *A, b *B, c *C, d *D, e *E, f *
 		ecs.Component{ID: m.id5, Comp: f},
 		ecs.Component{ID: m.id6, Comp: g},
 		ecs.Component{ID: m.id7, Comp: h},
-	).WithRelation(uint8(m.target)).New(target[0])
+	).WithRelation(uint8(m.relation)).New(target[0])
 }
 
 // Add the Map8's components to the given entity.
diff --git a/generic/query_generated.go b/generic/query_generated.go
index a269850..f9a79c4 100644
--- a/generic/query_generated.go
+++ b/generic/query_generated.go
@@ -91,21 +91,14 @@ func (q *Filter0) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter0) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter0) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query0 is a generic query iterator for zero components.
@@ -255,21 +248,14 @@ func (q *Filter1[A]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter1[A]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter1[A]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query1 is a generic query iterator for one components.
@@ -429,21 +415,14 @@ func (q *Filter2[A, B]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter2[A, B]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter2[A, B]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query2 is a generic query iterator for two components.
@@ -607,21 +586,14 @@ func (q *Filter3[A, B, C]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter3[A, B, C]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter3[A, B, C]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query3 is a generic query iterator for three components.
@@ -789,21 +761,14 @@ func (q *Filter4[A, B, C, D]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter4[A, B, C, D]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter4[A, B, C, D]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query4 is a generic query iterator for four components.
@@ -975,21 +940,14 @@ func (q *Filter5[A, B, C, D, E]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter5[A, B, C, D, E]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter5[A, B, C, D, E]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query5 is a generic query iterator for five components.
@@ -1165,21 +1123,14 @@ func (q *Filter6[A, B, C, D, E, F]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter6[A, B, C, D, E, F]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter6[A, B, C, D, E, F]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query6 is a generic query iterator for six components.
@@ -1359,21 +1310,14 @@ func (q *Filter7[A, B, C, D, E, F, G]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter7[A, B, C, D, E, F, G]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter7[A, B, C, D, E, F, G]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query7 is a generic query iterator for seven components.
@@ -1557,21 +1501,14 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Filter(w *ecs.World) ecs.MaskFilter {
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter8[A, B, C, D, E, F, G, H]) Register(w *ecs.World) {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	q.compiled.cachedFilter = w.Cache().Register(q.compiled.filter)
-	q.compiled.filter = &q.compiled.cachedFilter
-	q.compiled.locked = true
+	q.compiled.Register(w)
 }
 
 // Unregister the filter from caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter8[A, B, C, D, E, F, G, H]) Unregister(w *ecs.World) {
-	if cf, ok := q.compiled.filter.(*ecs.CachedFilter); ok {
-		q.compiled.filter = w.Cache().Unregister(cf)
-	} else {
-		panic("can't unregister a filter that is not cached")
-	}
-	q.compiled.locked = false
+	q.compiled.Unregister(w)
 }
 
 // Query8 is a generic query iterator for eight components.
diff --git a/generic/util.go b/generic/util.go
index a83f5db..8f76507 100644
--- a/generic/util.go
+++ b/generic/util.go
@@ -54,3 +54,39 @@ func toMaskOptional(w *ecs.World, include []ecs.ID, optional []Comp) ecs.Mask {
 	}
 	return mask
 }
+
+func newEntity(w *ecs.World, ids []ecs.ID, relation int8, target ...ecs.Entity) ecs.Entity {
+	if len(target) == 0 {
+		return w.NewEntity(ids...)
+	}
+	if relation < 0 {
+		panic("map has no relation defined")
+	}
+	return ecs.NewBuilder(w, ids...).WithRelation(uint8(relation)).New(target[0])
+}
+
+func newBatch(w *ecs.World, count int, ids []ecs.ID, relation int8, target ...ecs.Entity) {
+	if len(target) == 0 {
+		ecs.NewBuilder(w, ids...).NewBatch(count)
+		return
+	}
+	if relation < 0 {
+		panic("map has no relation defined")
+	}
+	ecs.NewBuilder(w, ids...).WithRelation(uint8(relation)).NewBatch(count, target[0])
+}
+
+func newQuery(w *ecs.World, count int, ids []ecs.ID, relation int8, target ...ecs.Entity) ecs.Query {
+	var query ecs.Query
+
+	if len(target) == 0 {
+		query = ecs.NewBuilder(w, ids...).NewQuery(count)
+	} else {
+		if relation < 0 {
+			panic("map has no relation defined")
+		}
+		query = ecs.NewBuilder(w, ids...).WithRelation(uint8(relation)).NewQuery(count, target[0])
+	}
+
+	return query
+}


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index f14e604..6780236 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -13,9 +13,9 @@ func TestArchetype(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32)
+	node := newArchetypeNode(All(0, 1), -1, 32, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, false, Entity{}, comps...)
+	arch.Init(&node, 0, false, Entity{})
 
 	arch.Add(
 		newEntity(0),
@@ -71,13 +71,13 @@ func TestNewArchetype(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32)
+	node := newArchetypeNode(All(0, 1), -1, 32, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, true, Entity{}, comps...)
+	arch.Init(&node, 0, true, Entity{})
 	assert.Equal(t, 32, int(arch.Cap()))
 
 	arch = archetype{}
-	arch.Init(&node, 0, false, Entity{}, comps...)
+	arch.Init(&node, 0, false, Entity{})
 	assert.Equal(t, 1, int(arch.Cap()))
 
 	comps = []componentType{
@@ -85,9 +85,9 @@ func TestNewArchetype(t *testing.T) {
 		{ID: 0, Type: reflect.TypeOf(Position{})},
 	}
 	assert.Panics(t, func() {
-		node := newArchetypeNode(All(0, 1), -1, 32)
+		node := newArchetypeNode(All(0, 1), -1, 32, comps)
 		arch := archetype{}
-		arch.Init(&node, 0, true, Entity{}, comps...)
+		arch.Init(&node, 0, true, Entity{})
 	})
 }
 
@@ -97,9 +97,9 @@ func TestArchetypeExtend(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 8)
+	node := newArchetypeNode(All(0, 1), -1, 8, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, true, Entity{}, comps...)
+	arch.Init(&node, 0, true, Entity{})
 
 	assert.Equal(t, 8, int(arch.Cap()))
 	assert.Equal(t, 0, int(arch.Len()))
@@ -119,9 +119,9 @@ func TestArchetypeAlloc(t *testing.T) {
 		{ID: 0, Type: reflect.TypeOf(Position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
-	node := newArchetypeNode(All(0, 1), -1, 8)
+	node := newArchetypeNode(All(0, 1), -1, 8, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, true, Entity{}, comps...)
+	arch.Init(&node, 0, true, Entity{})
 
 	assert.Equal(t, 8, int(arch.Cap()))
 	assert.Equal(t, 0, int(arch.Len()))
@@ -144,9 +144,9 @@ func TestArchetypeAddGetSet(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(label{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 1)
+	node := newArchetypeNode(All(0, 1), -1, 1, comps)
 	a := archetype{}
-	a.Init(&node, 0, true, Entity{}, comps...)
+	a.Init(&node, 0, true, Entity{})
 
 	assert.Equal(t, 1, int(a.Cap()))
 	assert.Equal(t, 0, int(a.Len()))
@@ -179,9 +179,9 @@ func TestArchetypeReset(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32)
+	node := newArchetypeNode(All(0, 1), -1, 32, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, false, Entity{}, comps...)
+	arch.Init(&node, 0, false, Entity{})
 
 	arch.Add(
 		newEntity(0),
@@ -225,9 +225,9 @@ func TestArchetypeZero(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32)
+	node := newArchetypeNode(All(0, 1), -1, 32, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, false, Entity{}, comps...)
+	arch.Init(&node, 0, false, Entity{})
 
 	arch.Alloc(newEntity(0))
 	arch.Alloc(newEntity(1))
@@ -257,9 +257,9 @@ func BenchmarkIterArchetype_1000(b *testing.B) {
 		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
-	node := newArchetypeNode(All(0), -1, 32)
+	node := newArchetypeNode(All(0), -1, 32, comps)
 	arch := archetype{}
-	arch.Init(&node, 0, true, Entity{}, comps...)
+	arch.Init(&node, 0, true, Entity{})
 
 	for i := 0; i < 1000; i++ {
 		arch.Alloc(newEntity(eid(i)))
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 0b83293..c6c98e2 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -803,6 +803,8 @@ func TestWorldRelationSetBatch(t *testing.T) {
 
 	assert.Equal(t, 1304, len(events))
 
+	world.Relations().SetBatch(All(relID), relID, targ1, nil)
+
 	assert.Panics(t, func() {
 		world.Batch().SetRelation(All(relID), relID, targ2, func(q Query) {})
 	})
@@ -866,7 +868,7 @@ func TestWorldRelationRemove(t *testing.T) {
 	world.Relations().Set(e2, relID, targ3)
 
 	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
-	assert.Equal(t, targ3, world.graph.Get(2).archetypes.Get(1).Relation)
+	assert.Equal(t, targ3, world.graph.Get(2).archetypes.Get(1).RelationTarget)
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Batch().RemoveEntities(All())