Repository: mlange-42/arche
Pull request #297 (2023-05-13 23:21:25)

Return a single query from batch operations
Before this change, batchoperations take a callback to provide multiple queries: one for each archetypes.

This PR adds support for multiple batch archetypes in a single query. Batch functions return a query instead of taking a callback.

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

* **130** of **132**   **(98.48%)**  changed or added relevant lines in **5** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage decreased (**-0.08%**) to **99.922%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [ecs/relations.go](https://coveralls.io/builds/59922968/source?filename=ecs%2Frelations.go#L65) | 3 | 5 | 60.0%
<!-- | **Total:** | **130** | **132** | **98.48%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [ecs/relations.go](https://coveralls.io/builds/59922968/source?filename=ecs%2Frelations.go#L67) | 1 | 80.0% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/59922968/badge)](https://coveralls.io/builds/59922968) |
| :-- | --: |
| Change from base [Build 4969347592](https://coveralls.io/builds/59922962): |  -0.08% |
| Covered Lines: | 3831 |
| Relevant Lines: | 3834 |

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17877a4..54fce46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@
 * Re-introduce `World.Batch` for batch-processing of entities (add/remove/exchange) (#264)
 * New method `Builder.Add` for adding components with a target to entities (#264)
 * New method `Batch.SetRelation` for batch-setting entity relations (#265)
+* New methods `Builder.AddQuery`, `Builder.RemoveQuery` etc. to get a query over batch-processed entities (#297)
 * Sends an `EntityEvent` to the world listener on relation target changes (#265)
 
 ### Performance
diff --git a/ecs/archetype_node.go b/ecs/archetype_node.go
index 00ba388..ac6f79a 100644
--- a/ecs/archetype_node.go
+++ b/ecs/archetype_node.go
@@ -142,6 +142,8 @@ func (a *archNode) RemoveArchetype(arch *archetype) {
 	a.archetypes.Get(idx).Deactivate()
 }
 
+// Reset resets the archetypes in this node.
+// Relation archetypes with non-zero target are de-activated for re-use.
 func (a *archNode) Reset(cache *Cache) {
 	if !a.IsActive {
 		return
diff --git a/ecs/archetypes.go b/ecs/archetypes.go
index 0ac192f..1c35639 100644
--- a/ecs/archetypes.go
+++ b/ecs/archetypes.go
@@ -26,23 +26,30 @@ func (s singleArchetype) Len() int32 {
 // Implements [archetypes].
 //
 // Used for the [Query] returned by entity batch creation methods.
-type batchArchetype struct {
-	Archetype    *archetype
-	StartIndex   uint32
-	EndIndex     uint32
-	OldArchetype *archetype
+type batchArchetypes struct {
+	Archetype    []*archetype
+	StartIndex   []uint32
+	EndIndex     []uint32
+	OldArchetype []*archetype
 	Added        []ID
 	Removed      []ID
 }
 
 // Get returns the value at the given index.
-func (s *batchArchetype) Get(index int32) *archetype {
-	return s.Archetype
+func (s *batchArchetypes) Get(index int32) *archetype {
+	return s.Archetype[index]
 }
 
 // Len returns the current number of items in the paged array.
-func (s *batchArchetype) Len() int32 {
-	return 1
+func (s *batchArchetypes) Len() int32 {
+	return int32(len(s.Archetype))
+}
+
+func (s *batchArchetypes) Add(arch, oldArch *archetype, start, end uint32) {
+	s.Archetype = append(s.Archetype, arch)
+	s.OldArchetype = append(s.OldArchetype, oldArch)
+	s.StartIndex = append(s.StartIndex, start)
+	s.EndIndex = append(s.EndIndex, end)
 }
 
 // Implementation of an archetype iterator for pointers.
diff --git a/ecs/batch.go b/ecs/batch.go
index a5e07bb..d14bc7f 100644
--- a/ecs/batch.go
+++ b/ecs/batch.go
@@ -9,36 +9,64 @@ type Batch struct {
 
 // Add adds components to many entities, matching a filter.
 //
-// If the callback argument is given, it is called with a [Query] over the affected entities,
-// one Query for each affected archetype.
+// Panics:
+//   - when called with components that can't be added because they are already present.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Batch.AddQuery] and [World.Add].
+func (b *Batch) Add(filter Filter, comps ...ID) {
+	b.world.exchangeBatch(filter, comps, nil)
+}
+
+// AddQuery adds components to many entities, matching a filter.
+// It returns a query over the affected entities.
 //
 // Panics:
 //   - when called with components that can't be added because they are already present.
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
-// See also [World.Add].
-func (b *Batch) Add(filter Filter, callback func(Query), comps ...ID) {
-	b.world.exchangeBatch(filter, comps, nil, callback)
+// See also [Batch.Add] and [World.Add].
+func (b *Batch) AddQuery(filter Filter, comps ...ID) Query {
+	return b.world.exchangeBatchQuery(filter, comps, nil)
 }
 
 // Remove removes components from many entities, matching a filter.
 //
-// If the callback argument is given, it is called with a [Query] over the affected entities,
-// one Query for each affected archetype.
+// Panics:
+//   - when called with components that can't be removed because they are not present.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Batch.RemoveQuery] and [World.Remove].
+func (b *Batch) Remove(filter Filter, comps ...ID) {
+	b.world.exchangeBatch(filter, nil, comps)
+}
+
+// RemoveQuery removes components from many entities, matching a filter.
+// It returns a query over the affected entities.
 //
 // Panics:
 //   - when called with components that can't be removed because they are not present.
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
-// See also [World.Remove].
-func (b *Batch) Remove(filter Filter, callback func(Query), comps ...ID) {
-	b.world.exchangeBatch(filter, nil, comps, callback)
+// See also [Batch.Remove] and [World.Remove].
+func (b *Batch) RemoveQuery(filter Filter, comps ...ID) Query {
+	return b.world.exchangeBatchQuery(filter, nil, comps)
 }
 
 // SetRelation sets the [Relation] target for many entities, matching a filter.
 //
-// If the callback argument is given, it is called with a [Query] over the affected entities,
-// one Query for each affected archetype.
+// Panics:
+//   - when called for a missing component.
+//   - when called for a component that is not a relation.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Relations.Set] and [Relations.SetBatch].
+func (b *Batch) SetRelation(filter Filter, comp ID, target Entity) {
+	b.world.setRelationBatch(filter, comp, target)
+}
+
+// SetRelationQuery sets the [Relation] target for many entities, matching a filter.
+// It returns a query over the affected entities.
 //
 // Panics:
 //   - when called for a missing component.
@@ -46,22 +74,31 @@ func (b *Batch) Remove(filter Filter, callback func(Query), comps ...ID) {
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
 // See also [Relations.Set] and [Relations.SetBatch].
-func (b *Batch) SetRelation(filter Filter, comp ID, target Entity, callback func(Query)) {
-	b.world.setRelationBatch(filter, comp, target, callback)
+func (b *Batch) SetRelationQuery(filter Filter, comp ID, target Entity) Query {
+	return b.world.setRelationBatchQuery(filter, comp, target)
 }
 
 // Exchange exchanges components for many entities, matching a filter.
 //
-// If the callback argument is given, it is called with a [Query] over the affected entities,
-// one Query for each affected archetype.
+// Panics:
+//   - when called 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 [Batch.ExchangeQuery] and [World.Exchange].
+func (b *Batch) Exchange(filter Filter, add []ID, rem []ID) {
+	b.world.exchangeBatch(filter, add, rem)
+}
+
+// ExchangeQuery exchanges components for many entities, matching a filter.
+// It returns a query over the affected entities.
 //
 // Panics:
 //   - 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 [World.Exchange].
-func (b *Batch) Exchange(filter Filter, add []ID, rem []ID, callback func(Query)) {
-	b.world.exchangeBatch(filter, add, rem, callback)
+// See also [Batch.Exchange] and [World.Exchange].
+func (b *Batch) ExchangeQuery(filter Filter, add []ID, rem []ID) Query {
+	return b.world.exchangeBatchQuery(filter, add, rem)
 }
 
 // RemoveEntities removes and recycles all entities matching a filter.
diff --git a/ecs/query.go b/ecs/query.go
index 346c86d..b0324fc 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -60,23 +60,7 @@ func newCachedQuery(world *World, filter Filter, lockBit uint8, archetypes []*ar
 }
 
 // newQuery creates a query on a single archetype
-func newBatchQuery(world *World, lockBit uint8, archetype *batchArchetype) Query {
-	arch := archetype.Archetype
-	if archetype.StartIndex > 0 {
-		return Query{
-			filter:         nil,
-			isFiltered:     false,
-			isBatch:        true,
-			world:          world,
-			nodeArchetypes: archetype,
-			access:         &arch.archetypeAccess,
-			archIndex:      0,
-			lockBit:        lockBit,
-			count:          int32(archetype.EndIndex - archetype.StartIndex),
-			entityIndex:    archetype.StartIndex - 1,
-			entityIndexMax: archetype.EndIndex - 1,
-		}
-	}
+func newBatchQuery(world *World, lockBit uint8, archetype *batchArchetypes) Query {
 	return Query{
 		filter:         nil,
 		isFiltered:     false,
@@ -85,7 +69,7 @@ func newBatchQuery(world *World, lockBit uint8, archetype *batchArchetype) Query
 		nodeArchetypes: archetype,
 		archIndex:      -1,
 		lockBit:        lockBit,
-		count:          int32(archetype.EndIndex),
+		count:          -1,
 	}
 }
 
@@ -206,16 +190,20 @@ func (q *Query) nextBatch() bool {
 }
 
 func (q *Query) nextArchetypeBatch() bool {
-	if q.archIndex >= 0 {
-		return false
+	len := int32(q.nodeArchetypes.Len()) - 1
+	for q.archIndex < len {
+		q.archIndex++
+		a := q.nodeArchetypes.Get(q.archIndex)
+		aLen := a.Len()
+		if aLen > 0 {
+			q.access = &a.archetypeAccess
+			batch := q.nodeArchetypes.(*batchArchetypes)
+			q.entityIndex = batch.StartIndex[q.archIndex]
+			q.entityIndexMax = batch.EndIndex[q.archIndex] - 1
+			return true
+		}
 	}
-	q.archIndex++
-	a := q.nodeArchetypes.Get(q.archIndex)
-	q.access = &a.archetypeAccess
-	q.entityIndex = 0
-	batch := q.nodeArchetypes.(*batchArchetype)
-	q.entityIndexMax = batch.EndIndex - 1
-	return true
+	return false
 }
 
 func (q *Query) nextArchetypeSimple() bool {
@@ -325,6 +313,15 @@ func (q *Query) stepArchetype(step uint32) (int, bool) {
 func (q *Query) countEntities() int {
 	var count uint32 = 0
 
+	if q.isBatch {
+		batch := q.nodeArchetypes.(*batchArchetypes)
+		nArch := batch.Len()
+		var j int32
+		for j = 0; j < nArch; j++ {
+			count += batch.EndIndex[j] - batch.StartIndex[j]
+		}
+	}
+
 	if q.isFiltered {
 		ln := int32(len(q.archetypes))
 		var i int32
diff --git a/ecs/relations.go b/ecs/relations.go
index abfa278..bcc4e5e 100644
--- a/ecs/relations.go
+++ b/ecs/relations.go
@@ -43,15 +43,25 @@ func (r *Relations) Set(entity Entity, comp ID, target Entity) {
 
 // SetBatch sets the [Relation] target for many entities, matching a filter.
 //
-// If the callback argument is given, it is called with a [Query] over the affected entities,
-// one Query for each affected archetype.
+// Panics:
+//   - when called for a missing component.
+//   - when called for a component that is not a relation.
+//   - when called on a locked world. Do not use during [Query] iteration!
+//
+// See also [Relations.Set], [Relations.SetBatchQuery] and [Batch.SetRelation].
+func (r *Relations) SetBatch(filter Filter, comp ID, target Entity) {
+	r.world.setRelationBatch(filter, comp, target)
+}
+
+// SetBatchQuery sets the [Relation] target for many entities, matching a filter.
+// Returns a query over all affected entities.
 //
 // Panics:
 //   - when called for a missing component.
 //   - when called for a component that is not a relation.
 //   - when called on a locked world. Do not use during [Query] iteration!
 //
-// See also [Relations.Set] and [Batch.SetRelation].
-func (r *Relations) SetBatch(filter Filter, comp ID, target Entity, callback func(Query)) {
-	r.world.setRelationBatch(filter, comp, target, callback)
+// See also [Relations.Set], [Relations.SetBatch] and [Batch.SetRelation].
+func (r *Relations) SetBatchQuery(filter Filter, comp ID, target Entity) Query {
+	return r.world.setRelationBatchQuery(filter, comp, target)
 }
diff --git a/ecs/world.go b/ecs/world.go
index db6044c..2cf84ec 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -278,7 +278,13 @@ func (w *World) newEntities(count int, targetID int8, target Entity, comps ...ID
 func (w *World) newEntitiesQuery(count int, targetID int8, target Entity, comps ...ID) Query {
 	arch, startIdx := w.newEntitiesNoNotify(count, targetID, target, comps...)
 	lock := w.lock()
-	return newBatchQuery(w, lock, &batchArchetype{arch, startIdx, arch.Len(), nil, arch.Components(), nil})
+
+	batches := batchArchetypes{
+		Added:   arch.Components(),
+		Removed: nil,
+	}
+	batches.Add(arch, nil, startIdx, arch.Len())
+	return newBatchQuery(w, lock, &batches)
 }
 
 // Creates new entities with component values without returning a query over them.
@@ -314,7 +320,12 @@ func (w *World) newEntitiesWithQuery(count int, targetID int8, target Entity, co
 
 	arch, startIdx := w.newEntitiesWithNoNotify(count, targetID, target, ids, comps...)
 	lock := w.lock()
-	return newBatchQuery(w, lock, &batchArchetype{arch, startIdx, arch.Len(), nil, arch.Components(), nil})
+	batches := batchArchetypes{
+		Added:   arch.Components(),
+		Removed: nil,
+	}
+	batches.Add(arch, nil, startIdx, arch.Len())
+	return newBatchQuery(w, lock, &batches)
 }
 
 // RemoveEntity removes and recycles an [Entity].
@@ -645,7 +656,32 @@ 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, callback func(Query)) {
+func (w *World) exchangeBatch(filter Filter, add []ID, rem []ID) {
+	batches := batchArchetypes{
+		Added:   add,
+		Removed: rem,
+	}
+
+	w.exchangeBatchNoNotify(filter, add, rem, &batches)
+
+	if w.listener != nil {
+		w.notifyQuery(&batches)
+	}
+}
+
+func (w *World) exchangeBatchQuery(filter Filter, add []ID, rem []ID) Query {
+	batches := batchArchetypes{
+		Added:   add,
+		Removed: rem,
+	}
+
+	w.exchangeBatchNoNotify(filter, add, rem, &batches)
+
+	lock := w.lock()
+	return newBatchQuery(w, lock, &batches)
+}
+
+func (w *World) exchangeBatchNoNotify(filter Filter, add []ID, rem []ID, batches *batchArchetypes) {
 	w.checkLocked()
 
 	if len(add) == 0 && len(rem) == 0 {
@@ -666,16 +702,7 @@ func (w *World) exchangeBatch(filter Filter, add []ID, rem []ID, callback func(Q
 		}
 
 		newArch, start := w.exchangeArch(arch, archLen, add, rem)
-		if callback == nil {
-			if w.listener != nil {
-				w.notifyQuery(&batchArchetype{newArch, start, newArch.Len(), arch, add, rem})
-			}
-		} else {
-			lock := w.lock()
-			query := newBatchQuery(w, lock, &batchArchetype{newArch, start, newArch.Len(), arch, add, rem})
-			callback(query)
-			w.checkLocked()
-		}
+		batches.Add(newArch, arch, start, newArch.Len())
 	}
 }
 
@@ -800,7 +827,22 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 }
 
 // set relation target in batches.
-func (w *World) setRelationBatch(filter Filter, comp ID, target Entity, callback func(Query)) {
+func (w *World) setRelationBatch(filter Filter, comp ID, target Entity) {
+	batches := batchArchetypes{}
+	w.setRelationBatchNoNotify(filter, comp, target, &batches)
+	if w.listener != nil {
+		w.notifyQuery(&batches)
+	}
+}
+
+func (w *World) setRelationBatchQuery(filter Filter, comp ID, target Entity) Query {
+	batches := batchArchetypes{}
+	w.setRelationBatchNoNotify(filter, comp, target, &batches)
+	lock := w.lock()
+	return newBatchQuery(w, lock, &batches)
+}
+
+func (w *World) setRelationBatchNoNotify(filter Filter, comp ID, target Entity, batches *batchArchetypes) {
 	w.checkLocked()
 
 	if !target.IsZero() && !w.entityPool.Alive(target) {
@@ -821,16 +863,7 @@ func (w *World) setRelationBatch(filter Filter, comp ID, target Entity, callback
 		}
 
 		newArch, start, end := w.setRelationArch(arch, archLen, comp, target)
-		if callback == nil {
-			if w.listener != nil {
-				w.notifyQuery(&batchArchetype{newArch, start, end, arch, nil, nil})
-			}
-		} else {
-			lock := w.lock()
-			query := newBatchQuery(w, lock, &batchArchetype{newArch, start, end, arch, nil, nil})
-			callback(query)
-			w.checkLocked()
-		}
+		batches.Add(newArch, arch, start, end)
 	}
 }
 
@@ -1372,34 +1405,37 @@ func (w *World) closeQuery(query *Query) {
 	w.unlock(query.lockBit)
 
 	if w.listener != nil {
-		if arch, ok := query.nodeArchetypes.(*batchArchetype); ok {
+		if arch, ok := query.nodeArchetypes.(*batchArchetypes); ok {
 			w.notifyQuery(arch)
 		}
 	}
 }
 
 // notifies the listener for all entities on a batch query.
-func (w *World) notifyQuery(batchArch *batchArchetype) {
-	arch := batchArch.Archetype
-	var i uint32
-
-	event := EntityEvent{
-		Entity{}, Mask{}, arch.Mask, batchArch.Added, batchArch.Removed, arch.node.Ids, 1,
-		Entity{}, arch.RelationTarget, false,
-	}
+func (w *World) notifyQuery(batchArch *batchArchetypes) {
+	count := batchArch.Len()
+	var i int32
+	for i = 0; i < count; i++ {
+		arch := batchArch.Get(i)
+		event := EntityEvent{
+			Entity{}, Mask{}, arch.Mask, batchArch.Added, batchArch.Removed, arch.node.Ids, 1,
+			Entity{}, arch.RelationTarget, false,
+		}
 
-	oldArch := batchArch.OldArchetype
-	if oldArch != nil {
-		event.OldMask = oldArch.node.Mask
-		event.AddedRemoved = 0
-		event.OldTarget = oldArch.RelationTarget
-		event.TargetChanged = event.OldMask == event.NewMask
-	}
+		oldArch := batchArch.OldArchetype[i]
+		if oldArch != nil {
+			event.OldMask = oldArch.node.Mask
+			event.AddedRemoved = 0
+			event.OldTarget = oldArch.RelationTarget
+			event.TargetChanged = event.OldMask == event.NewMask
+		}
 
-	start, end := batchArch.StartIndex, batchArch.EndIndex
-	for i = start; i < end; i++ {
-		entity := arch.GetEntity(i)
-		event.Entity = entity
-		w.listener(&event)
+		start, end := batchArch.StartIndex[i], batchArch.EndIndex[i]
+		var e uint32
+		for e = start; e < end; e++ {
+			entity := arch.GetEntity(e)
+			event.Entity = entity
+			w.listener(&event)
+		}
 	}
 }
diff --git a/examples/batch_ops/main.go b/examples/batch_ops/main.go
index 542749a..8b267eb 100644
--- a/examples/batch_ops/main.go
+++ b/examples/batch_ops/main.go
@@ -51,9 +51,9 @@ func run() {
 	}
 
 	// Batch-remove components.
-	world.Batch().Remove(ecs.All(posID, velID), nil, velID)
+	world.Batch().Remove(ecs.All(posID, velID), velID)
 	// Batch-add components.
-	world.Batch().Add(ecs.All(posID), nil, velID)
+	world.Batch().Add(ecs.All(posID), velID)
 
 	// Batch-remove all entities with exactly the given components.
 	filterExcl := ecs.All(posID, velID).Exclusive()


Test patch:
diff --git a/ecs/archetypes_test.go b/ecs/archetypes_test.go
index e7f53cc..1d9c2e2 100644
--- a/ecs/archetypes_test.go
+++ b/ecs/archetypes_test.go
@@ -40,7 +40,8 @@ func TestArchetypePointers(t *testing.T) {
 
 func TestBatchArchetype(t *testing.T) {
 	arch := archetype{}
-	batch := batchArchetype{Archetype: &arch}
+	batch := batchArchetypes{}
+	batch.Add(&arch, nil, 0, 1)
 
 	assert.Equal(t, &arch, batch.Get(0))
 	assert.Equal(t, int32(1), batch.Len())
diff --git a/ecs/batch_test.go b/ecs/batch_test.go
index e051671..cef9bab 100644
--- a/ecs/batch_test.go
+++ b/ecs/batch_test.go
@@ -11,7 +11,7 @@ func ExampleBatch() {
 	builder := ecs.NewBuilder(&world, posID, velID)
 	builder.NewBatch(100)
 
-	world.Batch().Remove(ecs.All(posID, velID), nil, velID)
+	world.Batch().Remove(ecs.All(posID, velID), velID)
 	world.Batch().RemoveEntities(ecs.All(posID))
 }
 
@@ -25,7 +25,7 @@ func ExampleBatch_Add() {
 	builder.NewBatch(100)
 
 	filter := ecs.All(posID)
-	world.Batch().Add(filter, nil, velID)
+	world.Batch().Add(filter, velID)
 }
 
 func ExampleBatch_Remove() {
@@ -38,7 +38,7 @@ func ExampleBatch_Remove() {
 	builder.NewBatch(100)
 
 	filter := ecs.All(posID, velID)
-	world.Batch().Remove(filter, nil, velID)
+	world.Batch().Remove(filter, velID)
 }
 
 func ExampleBatch_Exchange() {
@@ -55,7 +55,6 @@ func ExampleBatch_Exchange() {
 		filter,          // Filter
 		[]ecs.ID{velID}, // Add components
 		[]ecs.ID{posID}, // Remove components
-		nil,             // Optional callback
 	)
 }
 
@@ -83,5 +82,5 @@ func ExampleBatch_SetRelation() {
 	builder.NewBatch(100)
 
 	filter := ecs.All(childID)
-	world.Batch().SetRelation(filter, childID, target, nil)
+	world.Batch().SetRelation(filter, childID, target)
 }
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 4573b32..9c67797 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -284,46 +284,38 @@ func TestWorldExchangeBatch(t *testing.T) {
 	builder.NewBatch(100, target1)
 	builder.NewBatch(100, target2)
 
-	cnt := 0
 	filter := All(posID, relID)
-	w.Batch().Exchange(filter, []ID{velID}, []ID{posID}, func(q Query) {
-		assert.Equal(t, 100, q.Count())
-		for q.Next() {
-			assert.True(t, q.Has(velID))
-			assert.True(t, q.Has(relID))
-			assert.False(t, q.Has(posID))
-		}
-		cnt++
-	})
-	assert.Equal(t, 2, cnt)
+	query := w.Batch().ExchangeQuery(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))
+	}
 
-	query := w.Query(All(posID))
+	query = w.Query(All(posID))
 	assert.Equal(t, 0, query.Count())
 	query.Close()
 
-	cnt = 0
 	filter2 := NewRelationFilter(All(relID), target1)
-	w.Batch().Exchange(&filter2, []ID{posID}, []ID{velID}, func(q Query) {
-		assert.Equal(t, 100, q.Count())
-		for q.Next() {
-			assert.True(t, q.Has(posID))
-			assert.True(t, q.Has(relID))
-			assert.False(t, q.Has(velID))
-			assert.Equal(t, target1, q.Relation(relID))
-		}
-		cnt++
-	})
-	assert.Equal(t, 1, cnt)
+	query = w.Batch().ExchangeQuery(&filter2, []ID{posID}, []ID{velID})
+	assert.Equal(t, 100, query.Count())
+	for query.Next() {
+		assert.True(t, query.Has(posID))
+		assert.True(t, query.Has(relID))
+		assert.False(t, query.Has(velID))
+		assert.Equal(t, target1, query.Relation(relID))
+	}
 
 	query = w.Query(All(posID))
 	assert.Equal(t, 100, query.Count())
 	query.Close()
 
-	w.Batch().Exchange(All(posID), nil, nil, nil)
+	w.Batch().Exchange(All(posID), nil, nil)
 
 	relFilter := NewRelationFilter(All(relID), target2)
-	w.Batch().Exchange(&relFilter, nil, []ID{relID}, nil)
-	w.Batch().Exchange(All(relID), nil, []ID{relID}, nil)
+	w.Batch().Exchange(&relFilter, nil, []ID{relID})
+	w.Batch().Exchange(All(relID), nil, []ID{relID})
 
 	w.Batch().RemoveEntities(All(posID))
 
@@ -338,7 +330,7 @@ func TestWorldExchangeBatch(t *testing.T) {
 	assert.Equal(t, []ID{posID}, events[202].Removed)
 
 	filter = All(velID)
-	w.Batch().Remove(filter, nil, velID)
+	w.Batch().Remove(filter, velID)
 
 	filter = All(velID)
 	q := w.Query(filter)
@@ -346,9 +338,23 @@ func TestWorldExchangeBatch(t *testing.T) {
 	q.Close()
 
 	filter = All()
-	w.Batch().Add(filter, nil, velID)
+	w.Batch().Add(filter, velID)
 
-	assert.Panics(t, func() { w.Batch().Remove(All(velID), func(q Query) {}, velID) })
+	w.Reset()
+
+	target1 = w.NewEntity(velID)
+	builder = NewBuilder(&w, posID, relID).WithRelation(relID)
+	builder.NewBatch(100, target1)
+
+	filter = All(velID)
+	q = w.Batch().RemoveQuery(filter, velID)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
+	filter = All()
+	q = w.Batch().AddQuery(filter, velID)
+	assert.Equal(t, 101, q.Count())
+	q.Close()
 }
 
 func TestWorldAssignSet(t *testing.T) {
@@ -773,47 +779,53 @@ func TestWorldRelationSetBatch(t *testing.T) {
 	builder.NewBatch(100, targ3)
 
 	relFilter := NewRelationFilter(All(relID), targ2)
-	world.Batch().SetRelation(&relFilter, relID, targ1, func(q Query) {
-		assert.Equal(t, 100, q.Count())
-		for q.Next() {
-			assert.Equal(t, targ1, q.Relation(relID))
-		}
-	})
+	q := world.Batch().SetRelationQuery(&relFilter, relID, targ1)
+	assert.Equal(t, 100, q.Count())
+	cnt := 0
+	for q.Next() {
+		assert.Equal(t, targ1, q.Relation(relID))
+		cnt++
+	}
+	assert.Equal(t, 100, cnt)
 
-	total := 0
-	world.Batch().SetRelation(All(relID), relID, targ3, func(q Query) {
-		total += q.Count()
-		for q.Next() {
-			assert.Equal(t, targ3, q.Relation(relID))
-		}
-	})
-	assert.Equal(t, 300, total)
+	q = world.Batch().SetRelationQuery(All(relID), relID, targ3)
+	assert.Equal(t, 300, q.Count())
+	cnt = 0
+	for q.Next() {
+		assert.Equal(t, targ3, q.Relation(relID))
+		cnt++
+	}
+	assert.Equal(t, 300, cnt)
 
 	relFilter = NewRelationFilter(All(relID), targ3)
-	world.Batch().SetRelation(&relFilter, relID, Entity{}, func(q Query) {
-		assert.Equal(t, 300, q.Count())
-		for q.Next() {
-			assert.True(t, q.Relation(relID).IsZero())
-		}
-	})
+	q = world.Batch().SetRelationQuery(&relFilter, relID, Entity{})
+	assert.Equal(t, 300, q.Count())
+	cnt = 0
+	for q.Next() {
+		assert.True(t, q.Relation(relID).IsZero())
+		cnt++
+	}
+	assert.Equal(t, 300, cnt)
 
 	relFilter = NewRelationFilter(All(relID), Entity{})
-	world.Batch().SetRelation(&relFilter, relID, targ1, nil)
+	world.Batch().SetRelation(&relFilter, relID, targ1)
 
 	world.RemoveEntity(targ3)
 	assert.Panics(t, func() {
-		world.Batch().SetRelation(All(relID), relID, targ3, nil)
+		world.Batch().SetRelation(All(relID), relID, targ3)
 	})
 
 	assert.Equal(t, 1304, len(events))
 
-	world.Relations().SetBatch(All(relID), relID, targ1, nil)
+	world.Relations().SetBatch(All(relID), relID, targ1)
 
-	assert.Panics(t, func() {
-		world.Batch().SetRelation(All(relID), relID, targ2, func(q Query) {})
-	})
+	q = world.Relations().SetBatchQuery(All(relID), relID, targ2)
+	assert.Equal(t, 300, q.Count())
+	q.Close()
 
 	fmt.Println(debugPrintWorld(&world))
+
+	world.Reset()
 }
 
 func TestWorldRelationRemove(t *testing.T) {