Repository: mlange-42/arche
Pull request #342 (2024-01-18 17:48:52)

Exchange with relation targets, more batch ops
Adds methods to allow for exchanging components and setting relation targets at the same time. Includes batch operations and generic API.

* adds methods `Relations.Exchange()`, `Relations.ExchangeBatch()`, `Relations.ExchangeBatchQ()` for exchange with relation target
* adds `Exchange.WithRelation()` and optional target argument for operations with relation target
* adds `MapX.AddBatch()`, `MapX.AddBatchQ()`, `MapX.RemoveBatch()`and `MapX.RemoveBatchQ()`
* adds optional relation target argument to most `MapX` methods
* adds `FilterX.Filter()` to get an `ecs.Filter` from a generic one

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


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

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65158492/badge)](https://coveralls.io/builds/65158492) |
| :-- | --: |
| Change from base [Build 7573644274](https://coveralls.io/builds/65157725): |  0.0% |
| Covered Lines: | 5298 |
| Relevant Lines: | 5298 |

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b4c222..db2f8b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,11 @@ This change was necessary to get the same performance as before, despite the mor
 * Adds functions `ComponentIDs(*World)` and `ResourceIDs(*World)` to get all registered IDs (#329)
 * Adds methods `Mask.And`, `Mask.Or` and `Mask.Xor` (#335)
 * Adds build tag `tiny` to restrict to 64 components for an extra bit of performance (#338)
+* Adds methods `Relations.Exchange()`, `Relations.ExchangeBatch()`, `Relations.ExchangeBatchQ()` for exchange with relation target (#342)
+* Generic API adds `Exchange.WithRelation()` and optional target argument for operations with relation target (#342)
+* Generic API adds `MapX.AddBatch()`, `MapX.AddBatchQ()`, `MapX.RemoveBatch()`and `MapX.RemoveBatchQ()` (#342)
+* Generic API adds optional relation target argument to most `MapX` methods (#342)
+* Generic API adds `FilterX.Filter()` to get an `ecs.Filter` from a generic one (#342)
 
 ### Performance
 
diff --git a/ecs/batch.go b/ecs/batch.go
index b8dbae1..9953cb1 100644
--- a/ecs/batch.go
+++ b/ecs/batch.go
@@ -15,7 +15,7 @@ type Batch struct {
 //
 // See also [Batch.AddQ] and [World.Add].
 func (b *Batch) Add(filter Filter, comps ...ID) {
-	b.world.exchangeBatch(filter, comps, nil)
+	b.world.exchangeBatch(filter, comps, nil, ID{}, false, Entity{})
 }
 
 // AddQ adds components to many entities, matching a filter.
@@ -27,7 +27,7 @@ func (b *Batch) Add(filter Filter, comps ...ID) {
 //
 // See also [Batch.Add] and [World.Add].
 func (b *Batch) AddQ(filter Filter, comps ...ID) Query {
-	return b.world.exchangeBatchQuery(filter, comps, nil)
+	return b.world.exchangeBatchQuery(filter, comps, nil, ID{}, false, Entity{})
 }
 
 // Remove removes components from many entities, matching a filter.
@@ -38,7 +38,7 @@ func (b *Batch) AddQ(filter Filter, comps ...ID) Query {
 //
 // See also [Batch.RemoveQ] and [World.Remove].
 func (b *Batch) Remove(filter Filter, comps ...ID) {
-	b.world.exchangeBatch(filter, nil, comps)
+	b.world.exchangeBatch(filter, nil, comps, ID{}, false, Entity{})
 }
 
 // RemoveQ removes components from many entities, matching a filter.
@@ -50,7 +50,7 @@ func (b *Batch) Remove(filter Filter, comps ...ID) {
 //
 // See also [Batch.Remove] and [World.Remove].
 func (b *Batch) RemoveQ(filter Filter, comps ...ID) Query {
-	return b.world.exchangeBatchQuery(filter, nil, comps)
+	return b.world.exchangeBatchQuery(filter, nil, comps, ID{}, false, Entity{})
 }
 
 // SetRelation sets the [Relation] target for many entities, matching a filter.
@@ -94,8 +94,9 @@ func (b *Batch) SetRelationQ(filter Filter, comp ID, target Entity) Query {
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
 // See also [Batch.ExchangeQ] and [World.Exchange].
+// For batch-exchange with a relation target, see [Relations.ExchangeBatch].
 func (b *Batch) Exchange(filter Filter, add []ID, rem []ID) {
-	b.world.exchangeBatch(filter, add, rem)
+	b.world.exchangeBatch(filter, add, rem, ID{}, false, Entity{})
 }
 
 // ExchangeQ exchanges components for many entities, matching a filter.
@@ -109,8 +110,9 @@ func (b *Batch) Exchange(filter Filter, add []ID, rem []ID) {
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
 // See also [Batch.Exchange] and [World.Exchange].
+// For batch-exchange with a relation target, see [Relations.ExchangeBatchQ].
 func (b *Batch) ExchangeQ(filter Filter, add []ID, rem []ID) Query {
-	return b.world.exchangeBatchQuery(filter, add, rem)
+	return b.world.exchangeBatchQuery(filter, add, rem, ID{}, false, Entity{})
 }
 
 // RemoveEntities removes and recycles all entities matching a filter.
diff --git a/ecs/builder.go b/ecs/builder.go
index 6bf844c..f5fe37d 100644
--- a/ecs/builder.go
+++ b/ecs/builder.go
@@ -2,11 +2,11 @@ package ecs
 
 // Builder for more flexible and batched entity creation.
 type Builder struct {
-	world     *World
-	ids       []ID
-	comps     []Component
-	hasTarget bool
-	targetID  ID
+	world       *World
+	ids         []ID
+	comps       []Component
+	hasRelation bool
+	relationID  ID
 }
 
 // NewBuilder creates a builder from component IDs.
@@ -33,8 +33,8 @@ func NewBuilderWith(w *World, comps ...Component) *Builder {
 //
 // See [Relation] for details and examples.
 func (b *Builder) WithRelation(comp ID) *Builder {
-	b.hasTarget = true
-	b.targetID = comp
+	b.hasRelation = true
+	b.relationID = comp
 	return b
 }
 
@@ -44,13 +44,13 @@ func (b *Builder) WithRelation(comp ID) *Builder {
 // See [Builder.WithRelation].
 func (b *Builder) New(target ...Entity) Entity {
 	if len(target) > 0 {
-		if !b.hasTarget {
+		if !b.hasRelation {
 			panic("can't set target entity: builder has no relation")
 		}
 		if b.comps == nil {
-			return b.world.newEntityTarget(b.targetID, target[0], b.ids...)
+			return b.world.newEntityTarget(b.relationID, target[0], b.ids...)
 		}
-		return b.world.newEntityTargetWith(b.targetID, target[0], b.comps...)
+		return b.world.newEntityTargetWith(b.relationID, target[0], b.comps...)
 	}
 	if b.comps == nil {
 		return b.world.NewEntity(b.ids...)
@@ -64,14 +64,14 @@ func (b *Builder) New(target ...Entity) Entity {
 // See [Builder.WithRelation].
 func (b *Builder) NewBatch(count int, target ...Entity) {
 	if len(target) > 0 {
-		if !b.hasTarget {
+		if !b.hasRelation {
 			panic("can't set target entity: builder has no relation")
 		}
 		if b.comps == nil {
-			b.world.newEntities(count, b.targetID, true, target[0], b.ids...)
+			b.world.newEntities(count, b.relationID, true, target[0], b.ids...)
 			return
 		}
-		b.world.newEntitiesWith(count, b.targetID, true, target[0], b.comps...)
+		b.world.newEntitiesWith(count, b.relationID, true, target[0], b.comps...)
 		return
 	}
 	if b.comps == nil {
@@ -87,13 +87,13 @@ func (b *Builder) NewBatch(count int, target ...Entity) {
 // See [Builder.WithRelation].
 func (b *Builder) NewBatchQ(count int, target ...Entity) Query {
 	if len(target) > 0 {
-		if !b.hasTarget {
+		if !b.hasRelation {
 			panic("can't set target entity: builder has no relation")
 		}
 		if b.comps == nil {
-			return b.world.newEntitiesQuery(count, b.targetID, true, target[0], b.ids...)
+			return b.world.newEntitiesQuery(count, b.relationID, true, target[0], b.ids...)
 		}
-		return b.world.newEntitiesWithQuery(count, b.targetID, true, target[0], b.comps...)
+		return b.world.newEntitiesWithQuery(count, b.relationID, true, target[0], b.comps...)
 	}
 	if b.comps == nil {
 		return b.world.newEntitiesQuery(count, ID{}, false, Entity{}, b.ids...)
@@ -107,14 +107,14 @@ func (b *Builder) NewBatchQ(count int, target ...Entity) Query {
 // See [Builder.WithRelation].
 func (b *Builder) Add(entity Entity, target ...Entity) {
 	if len(target) > 0 {
-		if !b.hasTarget {
+		if !b.hasRelation {
 			panic("can't set target entity: builder has no relation")
 		}
 		if b.comps == nil {
-			b.world.exchange(entity, b.ids, nil, b.targetID, b.hasTarget, target[0])
+			b.world.exchange(entity, b.ids, nil, b.relationID, b.hasRelation, target[0])
 			return
 		}
-		b.world.assign(entity, b.targetID, b.hasTarget, target[0], b.comps...)
+		b.world.assign(entity, b.relationID, b.hasRelation, target[0], b.comps...)
 		return
 	}
 	if b.comps == nil {
diff --git a/ecs/event/event.go b/ecs/event/event.go
index 812cd4e..513cf7c 100644
--- a/ecs/event/event.go
+++ b/ecs/event/event.go
@@ -1,8 +1,21 @@
 // Package event contains a mask type and bit switches for listener subscriptions.
 //
-// See also ecs.Listener and ecs.EntityEvent.
+// See also [github.com/mlange-42/arche/ecs.Listener] and [github.com/mlange-42/arche/ecs.EntityEvent].
 package event
 
+// Subscription bits for an [github.com/mlange-42/arche/ecs.Listener]
+type Subscription uint8
+
+// Contains checks whether all the argument's bits are contained in this Subscription.
+func (s Subscription) Contains(bits Subscription) bool {
+	return (bits & s) == bits
+}
+
+// ContainsAny checks whether any of the argument's bits are contained in this Subscription.
+func (s Subscription) ContainsAny(bits Subscription) bool {
+	return (bits & s) != 0
+}
+
 // Subscription bits for individual events.
 const (
 	// EntityCreated subscription bit.
@@ -75,16 +88,3 @@ const (
 	// All subscriptions
 	All Subscription = Entities | Components | Relations
 )
-
-// Subscription bits for an ecs.Listener
-type Subscription uint8
-
-// Contains checks whether all the argument's bits are contained in this Subscription.
-func (s Subscription) Contains(bits Subscription) bool {
-	return (bits & s) == bits
-}
-
-// ContainsAny checks whether any of the argument's bits are contained in this Subscription.
-func (s Subscription) ContainsAny(bits Subscription) bool {
-	return (bits & s) != 0
-}
diff --git a/ecs/relations.go b/ecs/relations.go
index f80c0e1..8d5cb9e 100644
--- a/ecs/relations.go
+++ b/ecs/relations.go
@@ -23,6 +23,8 @@ func (r *Relations) Get(entity Entity, comp ID) Entity {
 //
 // GetUnchecked is an optimized version of [Relations.Get].
 // Does not check if the entity is alive or that the component ID is applicable.
+//
+// Panics when called for a removed entity, but not for a recycled entity.
 func (r *Relations) GetUnchecked(entity Entity, comp ID) Entity {
 	return r.world.getRelationUnchecked(entity, comp)
 }
@@ -65,3 +67,60 @@ func (r *Relations) SetBatch(filter Filter, comp ID, target Entity) {
 func (r *Relations) SetBatchQ(filter Filter, comp ID, target Entity) Query {
 	return r.world.setRelationBatchQuery(filter, comp, target)
 }
+
+// Exchange adds and removes components in one pass.
+// This is more efficient than subsequent use of [World.Add] and [World.Remove].
+// In contrast to [World.Exchange], it allows to also set a relation target.
+//
+// When a [Relation] component is removed and another one is added,
+// the target entity of the relation is set to zero if no target is given.
+//
+// Panics:
+//   - when called for a removed (and potentially recycled) entity.
+//   - when called with components that can't be added or removed because they are already present/not present, respectively.
+//   - when called for a missing relation component.
+//   - when called for a component that is not a relation.
+//   - when called without any components to add or remove. Use [World.Relations] instead.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also the generic variants under [github.com/mlange-42/arche/generic.Exchange].
+func (r *Relations) Exchange(entity Entity, add []ID, rem []ID, relation ID, target Entity) {
+	r.world.exchange(entity, add, rem, relation, true, target)
+}
+
+// ExchangeBatch exchanges components for many entities, matching a filter.
+// In contrast to [Batch.Exchange], it allows to also set a relation target.
+//
+// When a [Relation] component is removed and another one is added,
+// the target entity of the relation is set to zero if no target is given.
+//
+// Panics:
+//   - when called with components that can't be added or removed because they are already present/not present, respectively.
+//   - when called for a missing relation component.
+//   - when called for a component that is not a relation.
+//   - when called without any components to add or remove. Use [Batch.SetRelation] instead.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Batch.Exchange], [Batch.ExchangeQ], [Relations.ExchangeBatch] and [World.Exchange].
+func (r *Relations) ExchangeBatch(filter Filter, add []ID, rem []ID, relation ID, target Entity) {
+	r.world.exchangeBatch(filter, add, rem, relation, true, target)
+}
+
+// ExchangeBatchQ exchanges components for many entities, matching a filter.
+// It returns a query over the affected entities.
+// In contrast to [Batch.ExchangeQ], it allows to also set a relation target.
+//
+// When a [Relation] component is removed and another one is added,
+// the target entity of the relation is set to zero if no target is given.
+//
+// Panics:
+//   - when called with components that can't be added or removed because they are already present/not present, respectively.
+//   - when called for a missing relation component.
+//   - when called for a component that is not a relation.
+//   - when called without any components to add or remove. Use [Batch.SetRelationQ] instead.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Batch.Exchange], [Batch.ExchangeQ], [Relations.ExchangeBatch] and [World.Exchange].
+func (r *Relations) ExchangeBatchQ(filter Filter, add []ID, rem []ID, relation ID, target Entity) Query {
+	return r.world.exchangeBatchQuery(filter, add, rem, relation, true, target)
+}
diff --git a/ecs/world.go b/ecs/world.go
index 1c3772f..2fb0a75 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -326,7 +326,7 @@ func (w *World) Remove(entity Entity, comps ...ID) {
 //   - when called with components that can't be added or removed because they are already present/not present, respectively.
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
-// See also the generic variants under [github.com/mlange-42/arche/generic.Exchange].
+// See also [Relations.Exchange] and the generic variants under [github.com/mlange-42/arche/generic.Exchange].
 func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.exchange(entity, add, rem, ID{}, false, Entity{})
 }
diff --git a/ecs/world_internal.go b/ecs/world_internal.go
index c461d26..1fa9cfe 100644
--- a/ecs/world_internal.go
+++ b/ecs/world_internal.go
@@ -198,6 +198,127 @@ func (w *World) newEntitiesWithQuery(count int, targetID ID, hasTarget bool, tar
 	return newBatchQuery(w, lock, &batches)
 }
 
+// Internal method to create new entities.
+func (w *World) newEntitiesNoNotify(count int, targetID ID, hasTarget bool, target Entity, comps ...ID) (*archetype, uint32) {
+	w.checkLocked()
+
+	if count < 1 {
+		panic("can only create a positive number of entities")
+	}
+
+	if !target.IsZero() && !w.entityPool.Alive(target) {
+		panic("can't make a dead entity a relation target")
+	}
+
+	arch := w.archetypes.Get(0)
+	if len(comps) > 0 {
+		arch = w.findOrCreateArchetype(arch, comps, nil, target)
+	}
+	if hasTarget {
+		w.checkRelation(arch, targetID)
+		if !target.IsZero() {
+			w.targetEntities.Set(target.id, true)
+		}
+	}
+
+	startIdx := arch.Len()
+	w.createEntities(arch, uint32(count))
+
+	return arch, startIdx
+}
+
+// Internal method to create new entities with component values.
+func (w *World) newEntitiesWithNoNotify(count int, targetID ID, hasTarget bool, target Entity, ids []ID, comps ...Component) (*archetype, uint32) {
+	w.checkLocked()
+
+	if count < 1 {
+		panic("can only create a positive number of entities")
+	}
+
+	if !target.IsZero() && !w.entityPool.Alive(target) {
+		panic("can't make a dead entity a relation target")
+	}
+
+	if len(comps) == 0 {
+		return w.newEntitiesNoNotify(count, targetID, hasTarget, target)
+	}
+
+	cnt := uint32(count)
+
+	arch := w.archetypes.Get(0)
+	if len(comps) > 0 {
+		arch = w.findOrCreateArchetype(arch, ids, nil, target)
+	}
+	if hasTarget {
+		w.checkRelation(arch, targetID)
+		if !target.IsZero() {
+			w.targetEntities.Set(target.id, true)
+		}
+	}
+
+	startIdx := arch.Len()
+	w.createEntities(arch, uint32(count))
+
+	var i uint32
+	for i = 0; i < cnt; i++ {
+		idx := startIdx + i
+		entity := arch.GetEntity(idx)
+		for _, c := range comps {
+			w.copyTo(entity, c.ID, c.Comp)
+		}
+	}
+
+	return arch, startIdx
+}
+
+// createEntity creates an Entity and adds it to the given archetype.
+func (w *World) createEntity(arch *archetype) Entity {
+	entity := w.entityPool.Get()
+	idx := arch.Alloc(entity)
+	len := len(w.entities)
+	if int(entity.id) == len {
+		if len == cap(w.entities) {
+			old := w.entities
+			w.entities = make([]entityIndex, len, len+w.config.CapacityIncrement)
+			copy(w.entities, old)
+
+		}
+		w.entities = append(w.entities, entityIndex{arch: arch, index: idx})
+		w.targetEntities.ExtendTo(len + w.config.CapacityIncrement)
+	} else {
+		w.entities[entity.id] = entityIndex{arch: arch, index: idx}
+		w.targetEntities.Set(entity.id, false)
+	}
+	return entity
+}
+
+// createEntity creates multiple Entities and adds them to the given archetype.
+func (w *World) createEntities(arch *archetype, count uint32) {
+	startIdx := arch.Len()
+	arch.AllocN(count)
+
+	len := len(w.entities)
+	required := len + int(count) - w.entityPool.Available()
+	capacity := capacity(required, w.config.CapacityIncrement)
+	if required > cap(w.entities) {
+		old := w.entities
+		w.entities = make([]entityIndex, required, capacity)
+		copy(w.entities, old)
+	} else if required > len {
+		w.entities = w.entities[:required]
+	}
+	w.targetEntities.ExtendTo(capacity)
+
+	var i uint32
+	for i = 0; i < count; i++ {
+		idx := startIdx + i
+		entity := w.entityPool.Get()
+		arch.SetEntity(idx, entity)
+		w.entities[entity.id] = entityIndex{arch: arch, index: idx}
+		w.targetEntities.Set(entity.id, false)
+	}
+}
+
 // RemoveEntities removes and recycles all entities matching a filter.
 //
 // Returns the number of removed entities.
@@ -295,6 +416,9 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation ID, hasRela
 	}
 
 	if len(add) == 0 && len(rem) == 0 {
+		if hasRelation {
+			panic("exchange operation has no effect, but a relation is specified. Use World.Relation instead")
+		}
 		return
 	}
 	index := &w.entities[entity.id]
@@ -351,6 +475,10 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation ID, hasRela
 	}
 	oldTarget := oldArch.RelationTarget
 
+	if !target.IsZero() {
+		w.targetEntities.Set(target.id, true)
+	}
+
 	w.cleanupArchetype(oldArch)
 
 	if w.listener != nil {
@@ -408,35 +536,38 @@ func (w *World) getExchangeMask(mask Mask, add []ID, rem []ID) Mask {
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
 // See also [World.Exchange].
-func (w *World) exchangeBatch(filter Filter, add []ID, rem []ID) {
+func (w *World) exchangeBatch(filter Filter, add []ID, rem []ID, relation ID, hasRelation bool, target Entity) {
 	batches := batchArchetypes{
 		Added:   add,
 		Removed: rem,
 	}
 
-	w.exchangeBatchNoNotify(filter, add, rem, &batches)
+	w.exchangeBatchNoNotify(filter, add, rem, relation, hasRelation, target, &batches)
 
 	if w.listener != nil {
 		w.notifyQuery(&batches)
 	}
 }
 
-func (w *World) exchangeBatchQuery(filter Filter, add []ID, rem []ID) Query {
+func (w *World) exchangeBatchQuery(filter Filter, add []ID, rem []ID, relation ID, hasRelation bool, target Entity) Query {
 	batches := batchArchetypes{
 		Added:   add,
 		Removed: rem,
 	}
 
-	w.exchangeBatchNoNotify(filter, add, rem, &batches)
+	w.exchangeBatchNoNotify(filter, add, rem, relation, hasRelation, target, &batches)
 
 	lock := w.lock()
 	return newBatchQuery(w, lock, &batches)
 }
 
-func (w *World) exchangeBatchNoNotify(filter Filter, add []ID, rem []ID, batches *batchArchetypes) {
+func (w *World) exchangeBatchNoNotify(filter Filter, add []ID, rem []ID, relation ID, hasRelation bool, target Entity, batches *batchArchetypes) {
 	w.checkLocked()
 
 	if len(add) == 0 && len(rem) == 0 {
+		if hasRelation {
+			panic("exchange operation has no effect, but a relation is specified. Use Batch.SetRelation instead")
+		}
 		return
 	}
 
@@ -453,22 +584,33 @@ func (w *World) exchangeBatchNoNotify(filter Filter, add []ID, rem []ID, batches
 			continue
 		}
 
-		newArch, start := w.exchangeArch(arch, archLen, add, rem)
+		newArch, start := w.exchangeArch(arch, archLen, add, rem, relation, hasRelation, target)
 		batches.Add(newArch, arch, start, newArch.Len())
 	}
 }
 
-func (w *World) exchangeArch(oldArch *archetype, oldArchLen uint32, add []ID, rem []ID) (*archetype, uint32) {
+func (w *World) exchangeArch(oldArch *archetype, oldArchLen uint32, add []ID, rem []ID, relation ID, hasRelation bool, target Entity) (*archetype, uint32) {
 	mask := w.getExchangeMask(oldArch.Mask, add, rem)
 	oldIDs := oldArch.Components()
 
-	target := oldArch.RelationTarget
-	if !target.IsZero() && oldArch.Mask.ContainsAny(&w.registry.IsRelation) {
-		for _, id := range rem {
-			// Removing a relation
-			if w.registry.IsRelation.Get(id) {
-				target = Entity{}
-				break
+	if hasRelation {
+		if !mask.Get(relation) {
+			tp, _ := w.registry.ComponentType(relation.id)
+			panic(fmt.Sprintf("can't add relation: resulting entity has no component %s", tp.Name()))
+		}
+		if !w.registry.IsRelation.Get(relation) {
+			tp, _ := w.registry.ComponentType(relation.id)
+			panic(fmt.Sprintf("can't add relation: %s is not a relation component", tp.Name()))
+		}
+	} else {
+		target = oldArch.RelationTarget
+		if !target.IsZero() && oldArch.Mask.ContainsAny(&w.registry.IsRelation) {
+			for _, id := range rem {
+				// Removing a relation
+				if w.registry.IsRelation.Get(id) {
+					target = Entity{}
+					break
+				}
 			}
 		}
 	}
@@ -496,6 +638,10 @@ func (w *World) exchangeArch(oldArch *archetype, oldArchLen uint32, add []ID, re
 		}
 	}
 
+	if !target.IsZero() {
+		w.targetEntities.Set(target.id, true)
+	}
+
 	// Theoretically, it could be oldArchLen < oldArch.Len(),
 	// which means we can't reset the archetype.
 	// However, this should not be possible as processing an entity twice
@@ -581,7 +727,10 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 		w.entities[swapEntity.id].index = index.index
 	}
 	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}
-	w.targetEntities.Set(target.id, true)
+
+	if !target.IsZero() {
+		w.targetEntities.Set(target.id, true)
+	}
 
 	oldTarget := oldArch.RelationTarget
 	w.cleanupArchetype(oldArch)
@@ -643,7 +792,7 @@ func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID,
 	w.checkRelation(oldArch, comp)
 
 	// Before, entities with unchanged target were included in the query,
-	// end events were emitted for them. Seems better to skip them completely,
+	// and events were emitted for them. Seems better to skip them completely,
 	// which is done in World.setRelationBatchNoNotify.
 	//if oldArch.RelationTarget == target {
 	//	return oldArch, 0, oldArchLen
@@ -675,6 +824,10 @@ func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID,
 		}
 	}
 
+	if !target.IsZero() {
+		w.targetEntities.Set(target.id, true)
+	}
+
 	// Theoretically, it could be oldArchLen < oldArch.Len(),
 	// which means we can't reset the archetype.
 	// However, this should not be possible as processing an entity twice
@@ -715,127 +868,6 @@ func (w *World) checkLocked() {
 	}
 }
 
-// Internal method to create new entities.
-func (w *World) newEntitiesNoNotify(count int, targetID ID, hasTarget bool, target Entity, comps ...ID) (*archetype, uint32) {
-	w.checkLocked()
-
-	if count < 1 {
-		panic("can only create a positive number of entities")
-	}
-
-	if !target.IsZero() && !w.entityPool.Alive(target) {
-		panic("can't make a dead entity a relation target")
-	}
-
-	arch := w.archetypes.Get(0)
-	if len(comps) > 0 {
-		arch = w.findOrCreateArchetype(arch, comps, nil, target)
-	}
-	if hasTarget {
-		w.checkRelation(arch, targetID)
-		if !target.IsZero() {
-			w.targetEntities.Set(target.id, true)
-		}
-	}
-
-	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count))
-
-	return arch, startIdx
-}
-
-// Internal method to create new entities with component values.
-func (w *World) newEntitiesWithNoNotify(count int, targetID ID, hasTarget bool, target Entity, ids []ID, comps ...Component) (*archetype, uint32) {
-	w.checkLocked()
-
-	if count < 1 {
-		panic("can only create a positive number of entities")
-	}
-
-	if !target.IsZero() && !w.entityPool.Alive(target) {
-		panic("can't make a dead entity a relation target")
-	}
-
-	if len(comps) == 0 {
-		return w.newEntitiesNoNotify(count, targetID, hasTarget, target)
-	}
-
-	cnt := uint32(count)
-
-	arch := w.archetypes.Get(0)
-	if len(comps) > 0 {
-		arch = w.findOrCreateArchetype(arch, ids, nil, target)
-	}
-	if hasTarget {
-		w.checkRelation(arch, targetID)
-		if !target.IsZero() {
-			w.targetEntities.Set(target.id, true)
-		}
-	}
-
-	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count))
-
-	var i uint32
-	for i = 0; i < cnt; i++ {
-		idx := startIdx + i
-		entity := arch.GetEntity(idx)
-		for _, c := range comps {
-			w.copyTo(entity, c.ID, c.Comp)
-		}
-	}
-
-	return arch, startIdx
-}
-
-// createEntity creates an Entity and adds it to the given archetype.
-func (w *World) createEntity(arch *archetype) Entity {
-	entity := w.entityPool.Get()
-	idx := arch.Alloc(entity)
-	len := len(w.entities)
-	if int(entity.id) == len {
-		if len == cap(w.entities) {
-			old := w.entities
-			w.entities = make([]entityIndex, len, len+w.config.CapacityIncrement)
-			copy(w.entities, old)
-
-		}
-		w.entities = append(w.entities, entityIndex{arch: arch, index: idx})
-		w.targetEntities.ExtendTo(len + w.config.CapacityIncrement)
-	} else {
-		w.entities[entity.id] = entityIndex{arch: arch, index: idx}
-		w.targetEntities.Set(entity.id, false)
-	}
-	return entity
-}
-
-// createEntity creates multiple Entities and adds them to the given archetype.
-func (w *World) createEntities(arch *archetype, count uint32) {
-	startIdx := arch.Len()
-	arch.AllocN(count)
-
-	len := len(w.entities)
-	required := len + int(count) - w.entityPool.Available()
-	capacity := capacity(required, w.config.CapacityIncrement)
-	if required > cap(w.entities) {
-		old := w.entities
-		w.entities = make([]entityIndex, required, capacity)
-		copy(w.entities, old)
-	} else if required > len {
-		w.entities = w.entities[:required]
-	}
-	w.targetEntities.ExtendTo(capacity)
-
-	var i uint32
-	for i = 0; i < count; i++ {
-		idx := startIdx + i
-		entity := w.entityPool.Get()
-		arch.SetEntity(idx, entity)
-		w.entities[entity.id] = entityIndex{arch: arch, index: idx}
-		w.targetEntities.Set(entity.id, false)
-	}
-}
-
 // Copies a component to an entity
 func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 	if !w.Has(entity, id) {
diff --git a/generic/doc.go b/generic/doc.go
index 8e9f10a..7244ff6 100644
--- a/generic/doc.go
+++ b/generic/doc.go
@@ -10,5 +10,6 @@
 //   - [Map] provides generic access to a single component using world access, like [Map.Get] and [Map.Set].
 //   - [Map1], [Map2], etc. provide generic access to multiple components using world access,
 //     like [Map1.Get], [Map1.Add], [Map1.Remove], etc.
+//   - [Exchange] allows to add, remove and exchange components, incl. as batch operations.
 //   - [Resource] provides generic access to a resource from [ecs.Resources].
 package generic
diff --git a/generic/exchange.go b/generic/exchange.go
index e65bae9..897ac93 100644
--- a/generic/exchange.go
+++ b/generic/exchange.go
@@ -12,9 +12,12 @@ import "github.com/mlange-42/arche/ecs"
 //
 // For only adding or removing components, see also [Map1], [Map2] etc.
 type Exchange struct {
-	add    []ecs.ID
-	remove []ecs.ID
-	world  *ecs.World
+	add         []ecs.ID
+	remove      []ecs.ID
+	hasRelation bool
+	relationID  ecs.ID
+	builder     ecs.Builder
+	world       *ecs.World
 }
 
 // NewExchange creates a new Exchange object.
@@ -24,11 +27,30 @@ func NewExchange(w *ecs.World) *Exchange {
 	}
 }
 
+// WithRelation sets the [Relation] component for the Exchange.
+//
+// Use in conjunction with the optional target argument of [Exchange.NewEntity], [Exchange.Add], [Exchange.Remove] and [Exchange.Exchange].
+//
+// See [Relation] for details and examples.
+func (m *Exchange) WithRelation(comp Comp) *Exchange {
+	m.hasRelation = true
+	m.relationID = ecs.TypeID(m.world, comp)
+
+	m.builder = *ecs.NewBuilder(m.world, m.add...).WithRelation(m.relationID)
+	return m
+}
+
 // Adds sets components to add in calls to [Exchange.Add] and [Exchange.Exchange].
 //
 // Create the required mask items with [T].
 func (m *Exchange) Adds(add ...Comp) *Exchange {
 	m.add = toIds(m.world, add)
+
+	b := ecs.NewBuilder(m.world, m.add...)
+	if m.hasRelation {
+		b = b.WithRelation(m.relationID)
+	}
+	m.builder = *b
 	return m
 }
 
@@ -42,35 +64,92 @@ func (m *Exchange) Removes(remove ...Comp) *Exchange {
 
 // NewEntity creates a new [ecs.Entity] with the components set via [Exchange.Adds].
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Exchange's [ecs.Relation].
+// See [Exchange.WithRelation].
+//
 // See also [ecs.World.NewEntity].
-func (m *Exchange) NewEntity() ecs.Entity {
-	entity := m.world.NewEntity(m.add...)
-	return entity
+func (m *Exchange) NewEntity(target ...ecs.Entity) ecs.Entity {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Exchange has no relation")
+		}
+		return m.builder.New(target[0])
+	}
+	return m.world.NewEntity(m.add...)
 }
 
 // Add the components set via [Exchange.Adds] to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Exchange's [ecs.Relation].
+// See [Exchange.WithRelation].
+//
 // See also [ecs.World.Add].
-func (m *Exchange) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.add...)
+func (m *Exchange) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Exchange has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.add, nil, m.relationID, target[0])
+	} else {
+		m.world.Add(entity, m.add...)
+	}
 }
 
 // Remove the components set via [Exchange.Removes] from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Exchange's [ecs.Relation].
+// See [Exchange.WithRelation].
+//
 // See also [ecs.World.Remove].
-func (m *Exchange) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.remove...)
+func (m *Exchange) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Exchange has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.remove, m.relationID, target[0])
+	} else {
+		m.world.Remove(entity, m.remove...)
+	}
 }
 
 // Exchange components on an entity.
 //
 // Removes the components set via [Exchange.Removes].
 // Adds the components set via [Exchange.Adds].
-//
-// When a [Relation] component is removed and another one is added,
-// the target entity of the relation remains unchanged.
+// The optional argument can be used to set the target [ecs.Entity] for the Exchange's [ecs.Relation].
+// See [Exchange.WithRelation].
+// When a [ecs.Relation] component is removed and another one is added,
+// the target entity of the relation is set to zero if no target is given.
 //
 // See also [ecs.World.Exchange].
-func (m *Exchange) Exchange(entity ecs.Entity) {
-	m.world.Exchange(entity, m.add, m.remove)
+func (m *Exchange) Exchange(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Exchange has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.add, m.remove, m.relationID, target[0])
+	} else {
+		m.world.Exchange(entity, m.add, m.remove)
+	}
+}
+
+// ExchangeBatch exchanges components on many entities, matching a filter.
+//
+// Removes the components set via [Exchange.Removes].
+// Adds the components set via [Exchange.Adds].
+// The optional argument can be used to set the target [ecs.Entity] for the Exchange's [ecs.Relation].
+// See [Exchange.WithRelation].
+// When a [ecs.Relation] component is removed and another one is added,
+// the target entity of the relation is set to zero if no target is given.
+//
+// See also [ecs.Batch.Exchange] and [ecs.Batch.ExchangeQ].
+func (m *Exchange) ExchangeBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Exchange has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.add, m.remove, m.relationID, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.add, m.remove)
+	}
 }
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index e1b5671..e2eef10 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -109,9 +109,56 @@ func (m *Map{{ .Index }}{{ .Types }}) NewWith({{ .Arguments }}, target ...ecs.En
 
 // Add the Map{{ .Index }}'s components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map{{ .Index }}{{ .Types }}) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map{{ .Index }}{{ .Types }}) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map{{ .Index }}'s components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map{{ .Index }}{{ .Types }}) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map{{ .Index }}'s components to multiple entities and returns a query over them
+// and the newly added Map{{ .Index }}'s components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map{{ .Index }}{{ .Types }}) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query{{ .Index }}{{ .Types }} {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query{{ .Index }}{{ .Types }}{
+		Query: query,
+		{{ .IDAssign2 }}
+	}
 }
 
 // Assign the Map{{ .Index }}'s components to the given entity, using the supplied values.
@@ -126,11 +173,56 @@ func (m *Map{{ .Index }}{{ .Types }}) Assign(entity ecs.Entity, {{ .Arguments }}
 
 // Remove the Map{{ .Index }}'s components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map{{ .Index }}{{ .Types }}) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map{{ .Index }}{{ .Types }}) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
 }
 
+// RemoveBatch removes the Map{{ .Index }}'s components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map{{ .Index }}{{ .Types }}) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map{{ .Index }}'s components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map{{ .Index }}'s [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map{{ .Index }}{{ .Types }}) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map{{ .Index }} has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
+}
 
 // RemoveEntities removes all entities from the world that match the Map{{ .Index }}'s components.
 //
@@ -139,7 +231,7 @@ func (m *Map{{ .Index }}{{ .Types }}) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map{{ .Index }}{{ .Types }}) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
diff --git a/generic/generate/query.go.txt b/generic/generate/query.go.txt
index 3610790..b778a6a 100644
--- a/generic/generate/query.go.txt
+++ b/generic/generate/query.go.txt
@@ -87,7 +87,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ...ecs.E
 	return q
 }
 
-// Query builds a [Query{{ .Index }}] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter{{ .Index }}.WithRelation] was not called
@@ -95,7 +95,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ...ecs.E
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World, target ...ecs.Entity) Query{{ .Index }}{{ .Types }} {
+func (q *Filter{{ .Index }}{{ .Types }}) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	
 	filter := q.compiled.filter
@@ -111,6 +111,19 @@ func (q *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World, target ...ecs.Entit
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query{{ .Index }}] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter{{ .Index }}.WithRelation] was not called
+//   - if the target was already set via [Filter{{ .Index }}.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World, target ...ecs.Entity) Query{{ .Index }}{{ .Types }} {
+	filter := q.Filter(w, target...)
 	return Query{{ .Index }}{{ .Types }}{
 		Query: w.Query(filter),
 		target: q.compiled.TargetComp,
diff --git a/generic/map_generated.go b/generic/map_generated.go
index feb4d24..ea42912 100644
--- a/generic/map_generated.go
+++ b/generic/map_generated.go
@@ -115,9 +115,56 @@ func (m *Map1[A]) NewWith(a *A, target ...ecs.Entity) ecs.Entity {
 
 // Add the Map1's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map1[A]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map1[A]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map1's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map1[A]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map1's components to multiple entities and returns a query over them
+// and the newly added Map1's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map1[A]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query1[A] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query1[A]{
+		Query: query,
+		id0:   m.id0,
+	}
 }
 
 // Assign the Map1's components to the given entity, using the supplied values.
@@ -131,9 +178,55 @@ func (m *Map1[A]) Assign(entity ecs.Entity, a *A) {
 
 // Remove the Map1's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map1[A]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map1[A]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map1's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map1[A]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map1's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map1's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map1[A]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map1 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map1's components.
@@ -143,7 +236,7 @@ func (m *Map1[A]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map1[A]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -270,9 +363,57 @@ func (m *Map2[A, B]) NewWith(a *A, b *B, target ...ecs.Entity) ecs.Entity {
 
 // Add the Map2's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map2[A, B]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map2[A, B]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map2's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map2[A, B]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map2's components to multiple entities and returns a query over them
+// and the newly added Map2's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map2[A, B]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query2[A, B] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query2[A, B]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+	}
 }
 
 // Assign the Map2's components to the given entity, using the supplied values.
@@ -287,9 +428,55 @@ func (m *Map2[A, B]) Assign(entity ecs.Entity, a *A, b *B) {
 
 // Remove the Map2's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map2[A, B]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map2[A, B]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map2's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map2[A, B]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map2's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map2's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map2[A, B]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map2 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map2's components.
@@ -299,7 +486,7 @@ func (m *Map2[A, B]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map2[A, B]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -433,9 +620,58 @@ func (m *Map3[A, B, C]) NewWith(a *A, b *B, c *C, target ...ecs.Entity) ecs.Enti
 
 // Add the Map3's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map3[A, B, C]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map3[A, B, C]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map3's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map3[A, B, C]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map3's components to multiple entities and returns a query over them
+// and the newly added Map3's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map3[A, B, C]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query3[A, B, C] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query3[A, B, C]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+	}
 }
 
 // Assign the Map3's components to the given entity, using the supplied values.
@@ -451,9 +687,55 @@ func (m *Map3[A, B, C]) Assign(entity ecs.Entity, a *A, b *B, c *C) {
 
 // Remove the Map3's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map3[A, B, C]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map3[A, B, C]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map3's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map3[A, B, C]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map3's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map3's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map3[A, B, C]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map3 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map3's components.
@@ -463,7 +745,7 @@ func (m *Map3[A, B, C]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map3[A, B, C]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -604,9 +886,59 @@ func (m *Map4[A, B, C, D]) NewWith(a *A, b *B, c *C, d *D, target ...ecs.Entity)
 
 // Add the Map4's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map4[A, B, C, D]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map4[A, B, C, D]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map4's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map4[A, B, C, D]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map4's components to multiple entities and returns a query over them
+// and the newly added Map4's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map4[A, B, C, D]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query4[A, B, C, D] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query4[A, B, C, D]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+	}
 }
 
 // Assign the Map4's components to the given entity, using the supplied values.
@@ -623,9 +955,55 @@ func (m *Map4[A, B, C, D]) Assign(entity ecs.Entity, a *A, b *B, c *C, d *D) {
 
 // Remove the Map4's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map4[A, B, C, D]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map4[A, B, C, D]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map4's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map4[A, B, C, D]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map4's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map4's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map4[A, B, C, D]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map4 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map4's components.
@@ -635,7 +1013,7 @@ func (m *Map4[A, B, C, D]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map4[A, B, C, D]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -783,9 +1161,60 @@ func (m *Map5[A, B, C, D, E]) NewWith(a *A, b *B, c *C, d *D, e *E, target ...ec
 
 // Add the Map5's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map5[A, B, C, D, E]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map5[A, B, C, D, E]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map5's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map5[A, B, C, D, E]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map5's components to multiple entities and returns a query over them
+// and the newly added Map5's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map5[A, B, C, D, E]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query5[A, B, C, D, E] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query5[A, B, C, D, E]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+	}
 }
 
 // Assign the Map5's components to the given entity, using the supplied values.
@@ -803,9 +1232,55 @@ func (m *Map5[A, B, C, D, E]) Assign(entity ecs.Entity, a *A, b *B, c *C, d *D,
 
 // Remove the Map5's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map5[A, B, C, D, E]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map5[A, B, C, D, E]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map5's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map5[A, B, C, D, E]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map5's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map5's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map5[A, B, C, D, E]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map5 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map5's components.
@@ -815,7 +1290,7 @@ func (m *Map5[A, B, C, D, E]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map5[A, B, C, D, E]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -970,9 +1445,61 @@ func (m *Map6[A, B, C, D, E, F]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F, tar
 
 // Add the Map6's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map6[A, B, C, D, E, F]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map6[A, B, C, D, E, F]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map6's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map6[A, B, C, D, E, F]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map6's components to multiple entities and returns a query over them
+// and the newly added Map6's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map6[A, B, C, D, E, F]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query6[A, B, C, D, E, F] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query6[A, B, C, D, E, F]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+	}
 }
 
 // Assign the Map6's components to the given entity, using the supplied values.
@@ -991,9 +1518,55 @@ func (m *Map6[A, B, C, D, E, F]) Assign(entity ecs.Entity, a *A, b *B, c *C, d *
 
 // Remove the Map6's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map6[A, B, C, D, E, F]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map6[A, B, C, D, E, F]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map6's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map6[A, B, C, D, E, F]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map6's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map6's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map6[A, B, C, D, E, F]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map6 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map6's components.
@@ -1003,7 +1576,7 @@ func (m *Map6[A, B, C, D, E, F]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map6[A, B, C, D, E, F]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -1165,9 +1738,62 @@ func (m *Map7[A, B, C, D, E, F, G]) NewWith(a *A, b *B, c *C, d *D, e *E, f *F,
 
 // Add the Map7's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map7[A, B, C, D, E, F, G]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map7[A, B, C, D, E, F, G]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map7's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map7[A, B, C, D, E, F, G]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map7's components to multiple entities and returns a query over them
+// and the newly added Map7's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map7[A, B, C, D, E, F, G]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query7[A, B, C, D, E, F, G] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query7[A, B, C, D, E, F, G]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+	}
 }
 
 // Assign the Map7's components to the given entity, using the supplied values.
@@ -1187,9 +1813,55 @@ func (m *Map7[A, B, C, D, E, F, G]) Assign(entity ecs.Entity, a *A, b *B, c *C,
 
 // Remove the Map7's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map7[A, B, C, D, E, F, G]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map7[A, B, C, D, E, F, G]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map7's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map7[A, B, C, D, E, F, G]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map7's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map7's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map7[A, B, C, D, E, F, G]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map7 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map7's components.
@@ -1199,7 +1871,7 @@ func (m *Map7[A, B, C, D, E, F, G]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map7[A, B, C, D, E, F, G]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -1368,9 +2040,63 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewWith(a *A, b *B, c *C, d *D, e *E, f *
 
 // Add the Map8's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map8[A, B, C, D, E, F, G, H]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map8[A, B, C, D, E, F, G, H]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map8's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map8[A, B, C, D, E, F, G, H]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map8's components to multiple entities and returns a query over them
+// and the newly added Map8's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map8[A, B, C, D, E, F, G, H]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query8[A, B, C, D, E, F, G, H] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query8[A, B, C, D, E, F, G, H]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+		id7:   m.id7,
+	}
 }
 
 // Assign the Map8's components to the given entity, using the supplied values.
@@ -1391,9 +2117,55 @@ func (m *Map8[A, B, C, D, E, F, G, H]) Assign(entity ecs.Entity, a *A, b *B, c *
 
 // Remove the Map8's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map8[A, B, C, D, E, F, G, H]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map8[A, B, C, D, E, F, G, H]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map8's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map8[A, B, C, D, E, F, G, H]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map8's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map8's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map8[A, B, C, D, E, F, G, H]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map8 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map8's components.
@@ -1403,7 +2175,7 @@ func (m *Map8[A, B, C, D, E, F, G, H]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map8[A, B, C, D, E, F, G, H]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -1579,9 +2351,64 @@ func (m *Map9[A, B, C, D, E, F, G, H, I]) NewWith(a *A, b *B, c *C, d *D, e *E,
 
 // Add the Map9's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map9[A, B, C, D, E, F, G, H, I]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map9[A, B, C, D, E, F, G, H, I]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map9's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map9[A, B, C, D, E, F, G, H, I]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map9's components to multiple entities and returns a query over them
+// and the newly added Map9's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map9[A, B, C, D, E, F, G, H, I]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query9[A, B, C, D, E, F, G, H, I] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query9[A, B, C, D, E, F, G, H, I]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+		id7:   m.id7,
+		id8:   m.id8,
+	}
 }
 
 // Assign the Map9's components to the given entity, using the supplied values.
@@ -1603,9 +2430,55 @@ func (m *Map9[A, B, C, D, E, F, G, H, I]) Assign(entity ecs.Entity, a *A, b *B,
 
 // Remove the Map9's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map9[A, B, C, D, E, F, G, H, I]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map9[A, B, C, D, E, F, G, H, I]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map9's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map9[A, B, C, D, E, F, G, H, I]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map9's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map9's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map9[A, B, C, D, E, F, G, H, I]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map9 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map9's components.
@@ -1615,7 +2488,7 @@ func (m *Map9[A, B, C, D, E, F, G, H, I]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map9[A, B, C, D, E, F, G, H, I]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -1798,9 +2671,65 @@ func (m *Map10[A, B, C, D, E, F, G, H, I, J]) NewWith(a *A, b *B, c *C, d *D, e
 
 // Add the Map10's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map10's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map10's components to multiple entities and returns a query over them
+// and the newly added Map10's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query10[A, B, C, D, E, F, G, H, I, J] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query10[A, B, C, D, E, F, G, H, I, J]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+		id7:   m.id7,
+		id8:   m.id8,
+		id9:   m.id9,
+	}
 }
 
 // Assign the Map10's components to the given entity, using the supplied values.
@@ -1823,9 +2752,55 @@ func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Assign(entity ecs.Entity, a *A, b
 
 // Remove the Map10's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map10's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map10's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map10's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map10[A, B, C, D, E, F, G, H, I, J]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map10 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map10's components.
@@ -1835,7 +2810,7 @@ func (m *Map10[A, B, C, D, E, F, G, H, I, J]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map10[A, B, C, D, E, F, G, H, I, J]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -2025,9 +3000,66 @@ func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) NewWith(a *A, b *B, c *C, d *D,
 
 // Add the Map11's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map11's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map11's components to multiple entities and returns a query over them
+// and the newly added Map11's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query11[A, B, C, D, E, F, G, H, I, J, K] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query11[A, B, C, D, E, F, G, H, I, J, K]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+		id7:   m.id7,
+		id8:   m.id8,
+		id9:   m.id9,
+		id10:  m.id10,
+	}
 }
 
 // Assign the Map11's components to the given entity, using the supplied values.
@@ -2051,9 +3083,55 @@ func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Assign(entity ecs.Entity, a *A,
 
 // Remove the Map11's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map11's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map11's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map11's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map11 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map11's components.
@@ -2063,7 +3141,7 @@ func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map11[A, B, C, D, E, F, G, H, I, J, K]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
@@ -2260,9 +3338,67 @@ func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) NewWith(a *A, b *B, c *C, d
 
 // Add the Map12's components to the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
 // See also [ecs.World.Add].
-func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Add(entity ecs.Entity) {
-	m.world.Add(entity, m.ids...)
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Add(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		m.world.Relations().Exchange(entity, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Add(entity, m.ids...)
+	}
+}
+
+// AddBatch adds the Map12's components to many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
+// See also [ecs.Batch.Add].
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) AddBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, m.ids, nil)
+	}
+}
+
+// AddBatchQ adds the Map12's components to multiple entities and returns a query over them
+// and the newly added Map12's components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
+// See also [ecs.Batch.AddQ].
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) AddBatchQ(filter ecs.Filter, target ...ecs.Entity) Query12[A, B, C, D, E, F, G, H, I, J, K, L] {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, m.ids, nil, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, m.ids, nil)
+	}
+	return Query12[A, B, C, D, E, F, G, H, I, J, K, L]{
+		Query: query,
+		id0:   m.id0,
+		id1:   m.id1,
+		id2:   m.id2,
+		id3:   m.id3,
+		id4:   m.id4,
+		id5:   m.id5,
+		id6:   m.id6,
+		id7:   m.id7,
+		id8:   m.id8,
+		id9:   m.id9,
+		id10:  m.id10,
+		id11:  m.id11,
+	}
 }
 
 // Assign the Map12's components to the given entity, using the supplied values.
@@ -2287,9 +3423,55 @@ func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Assign(entity ecs.Entity, a
 
 // Remove the Map12's components from the given entity.
 //
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
 // See also [ecs.World.Remove].
-func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Remove(entity ecs.Entity) {
-	m.world.Remove(entity, m.ids...)
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Remove(entity ecs.Entity, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		m.world.Relations().Exchange(entity, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Remove(entity, m.ids...)
+	}
+}
+
+// RemoveBatch removes the Map12's components from many entities, matching a filter.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
+// See also [ecs.Batch.Remove].
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) RemoveBatch(filter ecs.Filter, target ...ecs.Entity) {
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		m.world.Relations().ExchangeBatch(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		m.world.Batch().Exchange(filter, nil, m.ids)
+	}
+}
+
+// RemoveBatchQ adds the Map12's components from multiple entities and returns a query over them,
+// with no components.
+//
+// The optional argument can be used to set the target [ecs.Entity] for the Map12's [ecs.Relation].
+//
+// See also [ecs.Batch.RemoveQ].
+func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) RemoveBatchQ(filter ecs.Filter, target ...ecs.Entity) Query0 {
+	var query ecs.Query
+	if len(target) > 0 {
+		if !m.hasRelation {
+			panic("can't set target entity: Map12 has no relation")
+		}
+		query = m.world.Relations().ExchangeBatchQ(filter, nil, m.ids, m.relation, target[0])
+	} else {
+		query = m.world.Batch().ExchangeQ(filter, nil, m.ids)
+	}
+	return Query0{
+		Query: query,
+	}
 }
 
 // RemoveEntities removes all entities from the world that match the Map12's components.
@@ -2299,7 +3481,7 @@ func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) Remove(entity ecs.Entity) {
 //
 // Returns the number of removed entities.
 //
-// See also [ecs.World.NewEntityWith].
+// See also [ecs.Batch.RemoveEntities].
 func (m *Map12[A, B, C, D, E, F, G, H, I, J, K, L]) RemoveEntities(exclusive bool) int {
 	if exclusive {
 		filter := m.mask.Exclusive()
diff --git a/generic/query_generated.go b/generic/query_generated.go
index 7e39a9d..02100d5 100644
--- a/generic/query_generated.go
+++ b/generic/query_generated.go
@@ -75,7 +75,7 @@ func (q *Filter0) WithRelation(comp Comp, target ...ecs.Entity) *Filter0 {
 	return q
 }
 
-// Query builds a [Query0] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter0.WithRelation] was not called
@@ -83,7 +83,7 @@ func (q *Filter0) WithRelation(comp Comp, target ...ecs.Entity) *Filter0 {
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter0) Query(w *ecs.World, target ...ecs.Entity) Query0 {
+func (q *Filter0) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -99,6 +99,19 @@ func (q *Filter0) Query(w *ecs.World, target ...ecs.Entity) Query0 {
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query0] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter0.WithRelation] was not called
+//   - if the target was already set via [Filter0.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter0) Query(w *ecs.World, target ...ecs.Entity) Query0 {
+	filter := q.Filter(w, target...)
 	return Query0{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -241,7 +254,7 @@ func (q *Filter1[A]) WithRelation(comp Comp, target ...ecs.Entity) *Filter1[A] {
 	return q
 }
 
-// Query builds a [Query1] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter1.WithRelation] was not called
@@ -249,7 +262,7 @@ func (q *Filter1[A]) WithRelation(comp Comp, target ...ecs.Entity) *Filter1[A] {
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter1[A]) Query(w *ecs.World, target ...ecs.Entity) Query1[A] {
+func (q *Filter1[A]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -265,6 +278,19 @@ func (q *Filter1[A]) Query(w *ecs.World, target ...ecs.Entity) Query1[A] {
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query1] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter1.WithRelation] was not called
+//   - if the target was already set via [Filter1.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter1[A]) Query(w *ecs.World, target ...ecs.Entity) Query1[A] {
+	filter := q.Filter(w, target...)
 	return Query1[A]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -417,7 +443,7 @@ func (q *Filter2[A, B]) WithRelation(comp Comp, target ...ecs.Entity) *Filter2[A
 	return q
 }
 
-// Query builds a [Query2] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter2.WithRelation] was not called
@@ -425,7 +451,7 @@ func (q *Filter2[A, B]) WithRelation(comp Comp, target ...ecs.Entity) *Filter2[A
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter2[A, B]) Query(w *ecs.World, target ...ecs.Entity) Query2[A, B] {
+func (q *Filter2[A, B]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -441,6 +467,19 @@ func (q *Filter2[A, B]) Query(w *ecs.World, target ...ecs.Entity) Query2[A, B] {
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query2] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter2.WithRelation] was not called
+//   - if the target was already set via [Filter2.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter2[A, B]) Query(w *ecs.World, target ...ecs.Entity) Query2[A, B] {
+	filter := q.Filter(w, target...)
 	return Query2[A, B]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -597,7 +636,7 @@ func (q *Filter3[A, B, C]) WithRelation(comp Comp, target ...ecs.Entity) *Filter
 	return q
 }
 
-// Query builds a [Query3] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter3.WithRelation] was not called
@@ -605,7 +644,7 @@ func (q *Filter3[A, B, C]) WithRelation(comp Comp, target ...ecs.Entity) *Filter
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter3[A, B, C]) Query(w *ecs.World, target ...ecs.Entity) Query3[A, B, C] {
+func (q *Filter3[A, B, C]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -621,6 +660,19 @@ func (q *Filter3[A, B, C]) Query(w *ecs.World, target ...ecs.Entity) Query3[A, B
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query3] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter3.WithRelation] was not called
+//   - if the target was already set via [Filter3.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter3[A, B, C]) Query(w *ecs.World, target ...ecs.Entity) Query3[A, B, C] {
+	filter := q.Filter(w, target...)
 	return Query3[A, B, C]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -781,7 +833,7 @@ func (q *Filter4[A, B, C, D]) WithRelation(comp Comp, target ...ecs.Entity) *Fil
 	return q
 }
 
-// Query builds a [Query4] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter4.WithRelation] was not called
@@ -789,7 +841,7 @@ func (q *Filter4[A, B, C, D]) WithRelation(comp Comp, target ...ecs.Entity) *Fil
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter4[A, B, C, D]) Query(w *ecs.World, target ...ecs.Entity) Query4[A, B, C, D] {
+func (q *Filter4[A, B, C, D]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -805,6 +857,19 @@ func (q *Filter4[A, B, C, D]) Query(w *ecs.World, target ...ecs.Entity) Query4[A
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query4] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter4.WithRelation] was not called
+//   - if the target was already set via [Filter4.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter4[A, B, C, D]) Query(w *ecs.World, target ...ecs.Entity) Query4[A, B, C, D] {
+	filter := q.Filter(w, target...)
 	return Query4[A, B, C, D]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -969,7 +1034,7 @@ func (q *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ...ecs.Entity) *
 	return q
 }
 
-// Query builds a [Query5] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter5.WithRelation] was not called
@@ -977,7 +1042,7 @@ func (q *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ...ecs.Entity) *
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World, target ...ecs.Entity) Query5[A, B, C, D, E] {
+func (q *Filter5[A, B, C, D, E]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -993,6 +1058,19 @@ func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World, target ...ecs.Entity) Query
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query5] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter5.WithRelation] was not called
+//   - if the target was already set via [Filter5.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World, target ...ecs.Entity) Query5[A, B, C, D, E] {
+	filter := q.Filter(w, target...)
 	return Query5[A, B, C, D, E]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -1161,7 +1239,7 @@ func (q *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ...ecs.Entity
 	return q
 }
 
-// Query builds a [Query6] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter6.WithRelation] was not called
@@ -1169,7 +1247,7 @@ func (q *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ...ecs.Entity
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World, target ...ecs.Entity) Query6[A, B, C, D, E, F] {
+func (q *Filter6[A, B, C, D, E, F]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -1185,6 +1263,19 @@ func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World, target ...ecs.Entity) Qu
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query6] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter6.WithRelation] was not called
+//   - if the target was already set via [Filter6.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World, target ...ecs.Entity) Query6[A, B, C, D, E, F] {
+	filter := q.Filter(w, target...)
 	return Query6[A, B, C, D, E, F]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -1357,7 +1448,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ...ecs.Ent
 	return q
 }
 
-// Query builds a [Query7] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter7.WithRelation] was not called
@@ -1365,7 +1456,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ...ecs.Ent
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World, target ...ecs.Entity) Query7[A, B, C, D, E, F, G] {
+func (q *Filter7[A, B, C, D, E, F, G]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -1381,6 +1472,19 @@ func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World, target ...ecs.Entity)
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query7] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter7.WithRelation] was not called
+//   - if the target was already set via [Filter7.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World, target ...ecs.Entity) Query7[A, B, C, D, E, F, G] {
+	filter := q.Filter(w, target...)
 	return Query7[A, B, C, D, E, F, G]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -1557,7 +1661,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ...ecs.
 	return q
 }
 
-// Query builds a [Query8] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter8.WithRelation] was not called
@@ -1565,7 +1669,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ...ecs.
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World, target ...ecs.Entity) Query8[A, B, C, D, E, F, G, H] {
+func (q *Filter8[A, B, C, D, E, F, G, H]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -1581,6 +1685,19 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World, target ...ecs.Enti
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query8] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter8.WithRelation] was not called
+//   - if the target was already set via [Filter8.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World, target ...ecs.Entity) Query8[A, B, C, D, E, F, G, H] {
+	filter := q.Filter(w, target...)
 	return Query8[A, B, C, D, E, F, G, H]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -1761,7 +1878,7 @@ func (q *Filter9[A, B, C, D, E, F, G, H, I]) WithRelation(comp Comp, target ...e
 	return q
 }
 
-// Query builds a [Query9] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter9.WithRelation] was not called
@@ -1769,7 +1886,7 @@ func (q *Filter9[A, B, C, D, E, F, G, H, I]) WithRelation(comp Comp, target ...e
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter9[A, B, C, D, E, F, G, H, I]) Query(w *ecs.World, target ...ecs.Entity) Query9[A, B, C, D, E, F, G, H, I] {
+func (q *Filter9[A, B, C, D, E, F, G, H, I]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -1785,6 +1902,19 @@ func (q *Filter9[A, B, C, D, E, F, G, H, I]) Query(w *ecs.World, target ...ecs.E
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query9] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter9.WithRelation] was not called
+//   - if the target was already set via [Filter9.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter9[A, B, C, D, E, F, G, H, I]) Query(w *ecs.World, target ...ecs.Entity) Query9[A, B, C, D, E, F, G, H, I] {
+	filter := q.Filter(w, target...)
 	return Query9[A, B, C, D, E, F, G, H, I]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -1969,7 +2099,7 @@ func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) WithRelation(comp Comp, target
 	return q
 }
 
-// Query builds a [Query10] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter10.WithRelation] was not called
@@ -1977,7 +2107,7 @@ func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) WithRelation(comp Comp, target
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) Query(w *ecs.World, target ...ecs.Entity) Query10[A, B, C, D, E, F, G, H, I, J] {
+func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -1993,6 +2123,19 @@ func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) Query(w *ecs.World, target ...e
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query10] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter10.WithRelation] was not called
+//   - if the target was already set via [Filter10.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter10[A, B, C, D, E, F, G, H, I, J]) Query(w *ecs.World, target ...ecs.Entity) Query10[A, B, C, D, E, F, G, H, I, J] {
+	filter := q.Filter(w, target...)
 	return Query10[A, B, C, D, E, F, G, H, I, J]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -2181,7 +2324,7 @@ func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) WithRelation(comp Comp, targ
 	return q
 }
 
-// Query builds a [Query11] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter11.WithRelation] was not called
@@ -2189,7 +2332,7 @@ func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) WithRelation(comp Comp, targ
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Query(w *ecs.World, target ...ecs.Entity) Query11[A, B, C, D, E, F, G, H, I, J, K] {
+func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -2205,6 +2348,19 @@ func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Query(w *ecs.World, target .
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query11] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter11.WithRelation] was not called
+//   - if the target was already set via [Filter11.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Query(w *ecs.World, target ...ecs.Entity) Query11[A, B, C, D, E, F, G, H, I, J, K] {
+	filter := q.Filter(w, target...)
 	return Query11[A, B, C, D, E, F, G, H, I, J, K]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,
@@ -2397,7 +2553,7 @@ func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) WithRelation(comp Comp, t
 	return q
 }
 
-// Query builds a [Query12] query for iteration, with an optional relation target.
+// Filter builds an [ecs.Filter], with an optional relation target.
 //
 // A relation target can't be used:
 //   - if [Filter12.WithRelation] was not called
@@ -2405,7 +2561,7 @@ func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) WithRelation(comp Comp, t
 //   - if the filter is registered for caching
 //
 // Panics in these cases.
-func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Query(w *ecs.World, target ...ecs.Entity) Query12[A, B, C, D, E, F, G, H, I, J, K, L] {
+func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
 	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 
 	filter := q.compiled.filter
@@ -2421,6 +2577,19 @@ func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Query(w *ecs.World, targe
 		filter = &q.compiled.relationFilter
 	}
 
+	return filter
+}
+
+// Query builds a [Query12] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter12.WithRelation] was not called
+//   - if the target was already set via [Filter12.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Query(w *ecs.World, target ...ecs.Entity) Query12[A, B, C, D, E, F, G, H, I, J, K, L] {
+	filter := q.Filter(w, target...)
 	return Query12[A, B, C, D, E, F, G, H, I, J, K, L]{
 		Query:     w.Query(filter),
 		target:    q.compiled.TargetComp,


Test patch:
diff --git a/ecs/batch_examples_test.go b/ecs/batch_examples_test.go
new file mode 100644
index 0000000..0e2070d
--- /dev/null
+++ b/ecs/batch_examples_test.go
@@ -0,0 +1,174 @@
+package ecs_test
+
+import "github.com/mlange-42/arche/ecs"
+
+func ExampleBatch() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID, velID)
+	builder.NewBatch(100)
+
+	world.Batch().Remove(ecs.All(posID, velID), velID)
+	world.Batch().RemoveEntities(ecs.All(posID))
+	// Output:
+}
+
+func ExampleBatch_Add() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID)
+	world.Batch().Add(filter, velID)
+	// Output:
+}
+
+func ExampleBatch_AddQ() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID)
+	query := world.Batch().AddQ(filter, velID)
+
+	for query.Next() {
+		pos := (*Position)(query.Get(posID))
+		pos.X = 100
+	}
+	// Output:
+}
+
+func ExampleBatch_Remove() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID, velID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID, velID)
+	world.Batch().Remove(filter, velID)
+	// Output:
+}
+
+func ExampleBatch_RemoveQ() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID, velID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID, velID)
+	query := world.Batch().RemoveQ(filter, velID)
+
+	for query.Next() {
+		pos := (*Position)(query.Get(posID))
+		pos.X = 100
+	}
+	// Output:
+}
+
+func ExampleBatch_Exchange() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID)
+	world.Batch().Exchange(
+		filter,          // Filter
+		[]ecs.ID{velID}, // Add components
+		[]ecs.ID{posID}, // Remove components
+	)
+	// Output:
+}
+
+func ExampleBatch_ExchangeQ() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	velID := ecs.ComponentID[Velocity](&world)
+
+	builder := ecs.NewBuilder(&world, posID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID)
+	query := world.Batch().ExchangeQ(
+		filter,          // Filter
+		[]ecs.ID{velID}, // Add components
+		[]ecs.ID{posID}, // Remove components
+	)
+
+	for query.Next() {
+		vel := (*Velocity)(query.Get(velID))
+		vel.X = 100
+	}
+	// Output:
+}
+
+func ExampleBatch_RemoveEntities() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+
+	builder := ecs.NewBuilder(&world, posID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(posID)
+	world.Batch().RemoveEntities(filter)
+	// Output:
+}
+
+func ExampleBatch_SetRelation() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	childID := ecs.ComponentID[ChildOf](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, posID, childID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(childID)
+	world.Batch().SetRelation(filter, childID, target)
+	// Output:
+}
+
+func ExampleBatch_SetRelationQ() {
+	world := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&world)
+	childID := ecs.ComponentID[ChildOf](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, posID, childID)
+	builder.NewBatch(100)
+
+	filter := ecs.All(childID)
+	query := world.Batch().SetRelationQ(filter, childID, target)
+
+	for query.Next() {
+		pos := (*Position)(query.Get(posID))
+		pos.X = 100
+	}
+	// Output:
+}
diff --git a/ecs/batch_test.go b/ecs/batch_test.go
index 0e2070d..e85b45a 100644
--- a/ecs/batch_test.go
+++ b/ecs/batch_test.go
@@ -1,174 +1,1 @@
 package ecs_test
-
-import "github.com/mlange-42/arche/ecs"
-
-func ExampleBatch() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID, velID)
-	builder.NewBatch(100)
-
-	world.Batch().Remove(ecs.All(posID, velID), velID)
-	world.Batch().RemoveEntities(ecs.All(posID))
-	// Output:
-}
-
-func ExampleBatch_Add() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID)
-	world.Batch().Add(filter, velID)
-	// Output:
-}
-
-func ExampleBatch_AddQ() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID)
-	query := world.Batch().AddQ(filter, velID)
-
-	for query.Next() {
-		pos := (*Position)(query.Get(posID))
-		pos.X = 100
-	}
-	// Output:
-}
-
-func ExampleBatch_Remove() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID, velID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID, velID)
-	world.Batch().Remove(filter, velID)
-	// Output:
-}
-
-func ExampleBatch_RemoveQ() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID, velID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID, velID)
-	query := world.Batch().RemoveQ(filter, velID)
-
-	for query.Next() {
-		pos := (*Position)(query.Get(posID))
-		pos.X = 100
-	}
-	// Output:
-}
-
-func ExampleBatch_Exchange() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID)
-	world.Batch().Exchange(
-		filter,          // Filter
-		[]ecs.ID{velID}, // Add components
-		[]ecs.ID{posID}, // Remove components
-	)
-	// Output:
-}
-
-func ExampleBatch_ExchangeQ() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	velID := ecs.ComponentID[Velocity](&world)
-
-	builder := ecs.NewBuilder(&world, posID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID)
-	query := world.Batch().ExchangeQ(
-		filter,          // Filter
-		[]ecs.ID{velID}, // Add components
-		[]ecs.ID{posID}, // Remove components
-	)
-
-	for query.Next() {
-		vel := (*Velocity)(query.Get(velID))
-		vel.X = 100
-	}
-	// Output:
-}
-
-func ExampleBatch_RemoveEntities() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-
-	builder := ecs.NewBuilder(&world, posID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(posID)
-	world.Batch().RemoveEntities(filter)
-	// Output:
-}
-
-func ExampleBatch_SetRelation() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	childID := ecs.ComponentID[ChildOf](&world)
-
-	target := world.NewEntity()
-
-	builder := ecs.NewBuilder(&world, posID, childID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(childID)
-	world.Batch().SetRelation(filter, childID, target)
-	// Output:
-}
-
-func ExampleBatch_SetRelationQ() {
-	world := ecs.NewWorld()
-
-	posID := ecs.ComponentID[Position](&world)
-	childID := ecs.ComponentID[ChildOf](&world)
-
-	target := world.NewEntity()
-
-	builder := ecs.NewBuilder(&world, posID, childID)
-	builder.NewBatch(100)
-
-	filter := ecs.All(childID)
-	query := world.Batch().SetRelationQ(filter, childID, target)
-
-	for query.Next() {
-		pos := (*Position)(query.Get(posID))
-		pos.X = 100
-	}
-	// Output:
-}
diff --git a/ecs/relations_test.go b/ecs/relations_test.go
index 4461e9f..44756ce 100644
--- a/ecs/relations_test.go
+++ b/ecs/relations_test.go
@@ -2,10 +2,59 @@ package ecs_test
 
 import (
 	"fmt"
+	"testing"
 
 	"github.com/mlange-42/arche/ecs"
+	"github.com/stretchr/testify/assert"
 )
 
+func TestRelationsExchange(t *testing.T) {
+	w := ecs.NewWorld()
+	posID := ecs.ComponentID[Position](&w)
+	velID := ecs.ComponentID[Velocity](&w)
+	childID := ecs.ComponentID[ChildOf](&w)
+
+	parent1 := w.NewEntity(posID)
+	parent2 := w.NewEntity(posID)
+
+	builder := ecs.NewBuilder(&w, posID, childID).WithRelation(childID)
+	builder.NewBatch(10, parent1)
+
+	relFilter := ecs.NewRelationFilter(ecs.All(posID, childID), parent1)
+	query := w.Query(&relFilter)
+	assert.Equal(t, 10, query.Count())
+	query.Close()
+
+	filter := ecs.All(posID, childID)
+	assert.Panics(t, func() {
+		w.Relations().ExchangeBatch(filter, nil, []ecs.ID{posID}, velID, parent2)
+	})
+	assert.Panics(t, func() {
+		w.Relations().ExchangeBatch(filter, []ecs.ID{velID}, []ecs.ID{posID}, velID, parent2)
+	})
+	assert.Panics(t, func() {
+		w.Relations().ExchangeBatch(filter, nil, nil, childID, parent2)
+	})
+	assert.Panics(t, func() {
+		_ = w.Relations().ExchangeBatchQ(filter, nil, nil, childID, parent2)
+	})
+
+	w.Relations().ExchangeBatch(filter, []ecs.ID{velID}, nil, childID, parent2)
+
+	relFilter = ecs.NewRelationFilter(ecs.All(posID, velID, childID), parent2)
+	query = w.Query(&relFilter)
+	assert.Equal(t, 10, query.Count())
+	query.Close()
+
+	w.Relations().ExchangeBatch(filter, nil, []ecs.ID{velID}, childID, parent1)
+
+	relFilter = ecs.NewRelationFilter(ecs.All(posID, childID), parent1)
+	query = w.Query(&relFilter)
+	assert.Equal(t, 10, query.Count())
+	query.Close()
+
+}
+
 func ExampleRelations() {
 	world := ecs.NewWorld()
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 91a0a19..fbeded7 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -284,6 +284,31 @@ func TestWorldExchange(t *testing.T) {
 	assert.Panics(t, func() { w.exchange(e0, []ID{posID}, nil, rel1ID, true, target) })
 }
 
+func TestWorldExchangeRelation(t *testing.T) {
+	w := NewWorld()
+
+	posID := ComponentID[Position](&w)
+	rel1ID := ComponentID[testRelationA](&w)
+	rel2ID := ComponentID[testRelationB](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+
+	w.Relations().Exchange(e0, []ID{posID, rel1ID}, nil, rel1ID, e1)
+
+	assert.Equal(t, []ID{posID, rel1ID}, w.Ids(e0))
+	assert.Equal(t, e1, w.Relations().Get(e0, rel1ID))
+
+	assert.Panics(t, func() {
+		w.Relations().Exchange(e0, nil, nil, rel1ID, e2)
+	})
+
+	w.Relations().Exchange(e0, []ID{rel2ID}, []ID{rel1ID}, rel2ID, e2)
+	assert.Equal(t, []ID{posID, rel2ID}, w.Ids(e0))
+	assert.Equal(t, e2, w.Relations().Get(e0, rel2ID))
+}
+
 func TestWorldExchangeBatch(t *testing.T) {
 	w := NewWorld()
 
@@ -314,12 +339,19 @@ func TestWorldExchangeBatch(t *testing.T) {
 	}, events[201])
 
 	filter := All(posID, relID)
-	query := w.Batch().ExchangeQ(filter, []ID{velID}, []ID{posID})
+	query := w.Query(filter)
+	assert.Equal(t, 200, query.Count())
+	for query.Next() {
+		assert.False(t, query.Relation(relID).IsZero())
+	}
+
+	query = w.Batch().ExchangeQ(filter, []ID{velID}, []ID{posID})
 	assert.Equal(t, 200, query.Count())
 	for query.Next() {
 		assert.True(t, query.Has(velID))
 		assert.True(t, query.Has(relID))
 		assert.False(t, query.Has(posID))
+		assert.False(t, query.Relation(relID).IsZero())
 	}
 
 	query = w.Query(All(posID))
diff --git a/generic/exchange_test.go b/generic/exchange_test.go
index 0d95b71..97430a7 100644
--- a/generic/exchange_test.go
+++ b/generic/exchange_test.go
@@ -44,3 +44,115 @@ func TestExchange(t *testing.T) {
 	assert.NotNil(t, s2)
 	assert.NotNil(t, s3)
 }
+
+func TestExchangeRelation(t *testing.T) {
+	w := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&w)
+	velID := ecs.ComponentID[Velocity](&w)
+	rel1ID := ecs.ComponentID[testRelationA](&w)
+
+	ex1 := NewExchange(&w).
+		Adds(T3[Position, Velocity, testRelationA]()...).
+		WithRelation(T[testRelationA]())
+
+	ex2 := NewExchange(&w).
+		Removes(T[Position]()).
+		WithRelation(T[testRelationA]())
+
+	ex3 := NewExchange(&w).
+		WithRelation(T[testRelationA]()).
+		Adds(T[Position]()).
+		Removes(T[Velocity]())
+
+	relMap := NewMap[testRelationA](&w)
+
+	e0 := ex1.NewEntity()
+	e1 := ex1.NewEntity()
+	e2 := ex1.NewEntity(e0)
+
+	assert.Equal(t, []ecs.ID{posID, velID, rel1ID}, w.Ids(e0))
+	assert.Equal(t, []ecs.ID{posID, velID, rel1ID}, w.Ids(e1))
+	assert.Equal(t, []ecs.ID{posID, velID, rel1ID}, w.Ids(e2))
+
+	assert.Equal(t, ecs.Entity{}, relMap.GetRelation(e0))
+	assert.Equal(t, ecs.Entity{}, relMap.GetRelation(e1))
+	assert.Equal(t, e0, relMap.GetRelation(e2))
+
+	e3 := w.NewEntity()
+	e4 := w.NewEntity()
+	ex1.Add(e3)
+	ex1.Add(e4, e0)
+
+	assert.Equal(t, []ecs.ID{posID, velID, rel1ID}, w.Ids(e3))
+	assert.Equal(t, []ecs.ID{posID, velID, rel1ID}, w.Ids(e4))
+	assert.Equal(t, ecs.Entity{}, relMap.GetRelation(e3))
+	assert.Equal(t, e0, relMap.GetRelation(e4))
+
+	ex2.Remove(e3, e1)
+	ex2.Remove(e4)
+
+	assert.Equal(t, []ecs.ID{velID, rel1ID}, w.Ids(e3))
+	assert.Equal(t, []ecs.ID{velID, rel1ID}, w.Ids(e4))
+	assert.Equal(t, e1, relMap.GetRelation(e3))
+	assert.Equal(t, e0, relMap.GetRelation(e4))
+
+	ex3.Exchange(e3)
+	ex3.Exchange(e4, e2)
+
+	assert.Equal(t, []ecs.ID{posID, rel1ID}, w.Ids(e3))
+	assert.Equal(t, []ecs.ID{posID, rel1ID}, w.Ids(e4))
+	assert.Equal(t, e1, relMap.GetRelation(e3))
+	assert.Equal(t, e2, relMap.GetRelation(e4))
+}
+
+func TestExchangeRelationPanics(t *testing.T) {
+	w := ecs.NewWorld()
+
+	e0 := w.NewEntity()
+
+	ex1 := NewExchange(&w).
+		Adds(T2[Position, testRelationA]()...).
+		Removes(T[Velocity]())
+
+	assert.Panics(t, func() {
+		_ = ex1.NewEntity(e0)
+	})
+
+	assert.Panics(t, func() {
+		ex1.Add(e0, e0)
+	})
+
+	assert.Panics(t, func() {
+		ex1.Remove(e0, e0)
+	})
+
+	assert.Panics(t, func() {
+		ex1.Exchange(e0, e0)
+	})
+
+	assert.Panics(t, func() {
+		ex1.ExchangeBatch(ecs.All(), e0)
+	})
+}
+
+func TestExchangeBatch(t *testing.T) {
+	w := ecs.NewWorld()
+
+	posID := ecs.ComponentID[Position](&w)
+
+	ex1 := NewExchange(&w).
+		Adds(T3[Position, Velocity, testRelationA]()...).
+		WithRelation(T[testRelationA]())
+
+	parent1 := w.NewEntity(posID)
+
+	b := ecs.NewBuilder(&w)
+	b.NewBatch(10)
+
+	filter := ecs.All().Exclusive()
+	ex1.ExchangeBatch(&filter, parent1)
+
+	b.NewBatch(10)
+	ex1.ExchangeBatch(&filter)
+}
diff --git a/generic/map_generated_test.go b/generic/map_generated_test.go
index 1d129cc..0150fdf 100644
--- a/generic/map_generated_test.go
+++ b/generic/map_generated_test.go
@@ -74,6 +74,87 @@ func TestMap1Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap1[testRelationA](&w)
+	mut4 := NewMap1[testStruct0](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap2Generated(t *testing.T) {
@@ -152,6 +233,91 @@ func TestMap2Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap2[
+		testRelationA, testStruct1,
+	](&w)
+	mut4 := NewMap2[
+		testStruct0, testStruct1,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap3Generated(t *testing.T) {
@@ -234,6 +400,91 @@ func TestMap3Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap3[
+		testRelationA, testStruct1, testStruct2,
+	](&w)
+	mut4 := NewMap3[
+		testStruct0, testStruct1, testStruct2,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap4Generated(t *testing.T) {
@@ -319,6 +570,91 @@ func TestMap4Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap4[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+	](&w)
+	mut4 := NewMap4[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap5Generated(t *testing.T) {
@@ -416,6 +752,93 @@ func TestMap5Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap5[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4,
+	](&w)
+	mut4 := NewMap5[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap6Generated(t *testing.T) {
@@ -513,6 +936,93 @@ func TestMap6Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap6[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5,
+	](&w)
+	mut4 := NewMap6[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap7Generated(t *testing.T) {
@@ -610,6 +1120,93 @@ func TestMap7Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap7[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6,
+	](&w)
+	mut4 := NewMap7[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap8Generated(t *testing.T) {
@@ -707,6 +1304,93 @@ func TestMap8Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap8[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+	](&w)
+	mut4 := NewMap8[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap9Generated(t *testing.T) {
@@ -811,6 +1495,95 @@ func TestMap9Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap9[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8,
+	](&w)
+	mut4 := NewMap9[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap10Generated(t *testing.T) {
@@ -915,6 +1688,95 @@ func TestMap10Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap10[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9,
+	](&w)
+	mut4 := NewMap10[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap11Generated(t *testing.T) {
@@ -1019,6 +1881,95 @@ func TestMap11Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap11[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9, testStruct10,
+	](&w)
+	mut4 := NewMap11[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9, testStruct10,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }
 
 func TestMap12Generated(t *testing.T) {
@@ -1123,4 +2074,93 @@ func TestMap12Generated(t *testing.T) {
 	mapper := NewMap[testRelationA](&w)
 	assert.Equal(t, target, mapper.GetRelation(e))
 	assert.Equal(t, target, mapper.GetRelationUnchecked(e))
+
+	// === Batch operations ====
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	mut3 := NewMap12[
+		testRelationA, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9, testStruct10, testStruct11,
+	](&w)
+	mut4 := NewMap12[
+		testStruct0, testStruct1, testStruct2, testStruct3,
+		testStruct4, testStruct5, testStruct6, testStruct7,
+		testStruct8, testStruct9, testStruct10, testStruct11,
+	](&w, T[testRelationB]())
+	rel2ID := ecs.ComponentID[testRelationB](&w)
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.Add(e1, e2)
+	})
+	mut2.Add(e1, e2)
+
+	assert.Panics(t, func() {
+		mut3.Remove(e1, e2)
+	})
+
+	mut.Add(e3)
+	w.Add(e3, rel2ID)
+	mut4.Remove(e3, e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+
+	e1 = w.NewEntity()
+	e2 = w.NewEntity()
+	_ = w.NewEntity()
+	assert.Panics(t, func() {
+		mut3.AddBatch(ecs.All(), e2)
+	})
+	assert.Panics(t, func() {
+		_ = mut3.AddBatchQ(ecs.All(), e2)
+	})
+	mut2.AddBatch(ecs.All(), e2)
+	mut2.RemoveBatch(ecs.All())
+	query := mut2.AddBatchQ(ecs.All(), e2)
+	assert.Equal(t, 3, query.Count())
+	query.Close()
+
+	assert.Panics(t, func() {
+		mut3.RemoveBatch(ecs.All(), e2)
+	})
+
+	assert.Panics(t, func() {
+		_ = mut3.RemoveBatchQ(ecs.All(), e2)
+	})
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	mut.AddBatch(ecs.All())
+	w.Batch().Add(ecs.All(), rel2ID)
+	mut4.RemoveBatch(ecs.All(), e1)
+
+	w.Batch().RemoveEntities(ecs.All())
+	e1 = w.NewEntity()
+	_ = w.NewEntity()
+	_ = w.NewEntity()
+
+	query2 := mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	w.Batch().Add(ecs.All(), rel2ID)
+
+	query3 := mut4.RemoveBatchQ(ecs.All(), e1)
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
+
+	query2 = mut.AddBatchQ(ecs.All())
+	assert.Equal(t, 3, query2.Count())
+	query2.Close()
+
+	query3 = mut4.RemoveBatchQ(ecs.All())
+	assert.Equal(t, 3, query3.Count())
+	query2.Close()
 }