Repository: mlange-42/arche
Pull request #157 (2023-03-24 16:57:02)

Notify after batch-creation query
Instead of notifying before returning the query created by batch-creation, notification is delayed until the query is closed. This brings two advantages:

* Notification occurs after the user changed/initialized the newly created entities
* The engine does not need to be locked during notifications

Further changes:

* `World.Reset()` does no longer remove the listener


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0f518c..3c4f5d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 * Generic mappers do no longer return all components when creating entities or components (#145)
 * Resources API moved out of the world, to a helper to get by `World.Resources()` (#150)
+* `World.Reset()` does no longer remove the component change listener (#157)
 
 ### Features
 
@@ -14,6 +15,7 @@
 * Adds method `Mask.Exact()` to create a filter matching an exact component composition (#149)
 * Generic mappers (`Map1`, ...) have methods `NewEntities`, `NewEntitiesWith` and `RemoveEntities` for batch operations (#151)
 * Batch-creation methods (ID-based and generic) have variants like `NewEntitiesQuery` that return a query over the created entities (#152)
+* Notification during batch-creation is delayed until the resp. query is closed (#157)
 
 ### Performance
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 821d02b..4c77298 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -64,17 +64,18 @@ type archetypes interface {
 // Implements [archetypes].
 //
 // Used for the [Query] returned by entity batch creation methods.
-type singleArchetype struct {
-	archetype *archetype
+type batchArchetype struct {
+	Archetype  *archetype
+	StartIndex uint32
 }
 
 // Get returns the value at the given index.
-func (s singleArchetype) Get(index int) *archetype {
-	return s.archetype
+func (s batchArchetype) Get(index int) *archetype {
+	return s.Archetype
 }
 
 // Len returns the current number of items in the paged array.
-func (s singleArchetype) Len() int {
+func (s batchArchetype) Len() int {
 	return 1
 }
 
diff --git a/ecs/batch.go b/ecs/batch.go
index 03dfd74..c9cd335 100644
--- a/ecs/batch.go
+++ b/ecs/batch.go
@@ -25,6 +25,7 @@ func (b *Batch) NewEntities(count int, comps ...ID) {
 //
 // Returns a [Query] for iterating the created entities.
 // The [Query] must be closed if it is not used!
+// Listener notification is delayed until the query is closed of fully iterated.
 // See also [Batch.NewEntities].
 //
 // Panics when called on a locked world.
@@ -53,6 +54,7 @@ func (b *Batch) NewEntitiesWith(count int, comps ...Component) {
 //
 // Returns a [Query] for iterating the created entities.
 // The [Query] must be closed if it is not used!
+// Listener notification is delayed until the query is closed of fully iterated.
 // See also [Batch.NewEntitiesWith].
 //
 // Panics when called on a locked world.
diff --git a/ecs/event.go b/ecs/event.go
index 590ed3a..abfb3b1 100644
--- a/ecs/event.go
+++ b/ecs/event.go
@@ -1,8 +1,20 @@
 package ecs
 
-// EntityEvent contains information about component changes.
+// EntityEvent contains information about component changes to an [Entity].
 //
 // To receive change events, register a function func(e EntityEvent) with [World.SetListener].
+//
+// Events notified are entity creation, removal and changes to the component composition.
+// Events are emitted immediately after the change is applied.
+//
+// Except for removed entities, events are always fired when the [World] is in an unlocked state.
+// Events for removed entities are fired right before removal of the entity,
+// to allow for inspection of it's components.
+// Therefore, the [World] is in a locked state during entity removal events.
+//
+// Events for batch-creation of entities using [World.Batch] are fired after all entities are created.
+// For batch methods that return a [Query], events are fired after the [Query] is closed (or fully iterated).
+// This allows the [World] to be in an unlocked state, and notifies after potential entity initialization.
 type EntityEvent struct {
 	// The entity that was changed.
 	Entity Entity
diff --git a/ecs/query.go b/ecs/query.go
index 1b0267b..ca7881a 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -74,7 +74,7 @@ func newArchQuery(world *World, lockBit uint8, archetype *archetype, start uint3
 		return Query{
 			filter:        dummyFilter{true},
 			world:         world,
-			archetypes:    singleArchetype{archetype},
+			archetypes:    batchArchetype{archetype, start},
 			index:         0,
 			lockBit:       lockBit,
 			count:         int(archetype.Len() - start),
@@ -84,7 +84,7 @@ func newArchQuery(world *World, lockBit uint8, archetype *archetype, start uint3
 	return Query{
 		filter:     dummyFilter{true},
 		world:      world,
-		archetypes: singleArchetype{archetype},
+		archetypes: batchArchetype{archetype, start},
 		index:      -1,
 		lockBit:    lockBit,
 		count:      int(archetype.Len()),
diff --git a/ecs/world.go b/ecs/world.go
index 276b6a4..cdf4dbb 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -121,18 +121,33 @@ func (w *World) NewEntity(comps ...ID) Entity {
 }
 
 func (w *World) newEntitiesQuery(count int, comps ...ID) Query {
-	arch, startIdx := w.newEntities(count, comps...)
+	arch, startIdx := w.newEntitiesNoNotify(count, comps...)
 	lock := w.lock()
 	return newArchQuery(w, lock, arch, startIdx)
 }
 
 func (w *World) newEntities(count int, comps ...ID) (*archetype, uint32) {
+	arch, startIdx := w.newEntitiesNoNotify(count, comps...)
+
+	if w.listener != nil {
+		cnt := uint32(count)
+		var i uint32
+		for i = 0; i < cnt; i++ {
+			idx := startIdx + i
+			entity := arch.GetEntity(uintptr(idx))
+			w.listener(EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
+		}
+	}
+
+	return arch, startIdx
+}
+
+func (w *World) newEntitiesNoNotify(count int, comps ...ID) (*archetype, uint32) {
 	w.checkLocked()
 
 	if count < 1 {
 		panic("can only create a positive number of entities")
 	}
-	cnt := uint32(count)
 
 	arch := w.archetypes.Get(0)
 	if len(comps) > 0 {
@@ -141,17 +156,6 @@ func (w *World) newEntities(count int, comps ...ID) (*archetype, uint32) {
 	startIdx := arch.Len()
 	w.createEntities(arch, uint32(count), true)
 
-	if w.listener != nil {
-		lock := w.lock()
-		var i uint32
-		for i = 0; i < cnt; i++ {
-			idx := startIdx + i
-			entity := arch.GetEntity(uintptr(idx))
-			w.listener(EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
-		}
-		w.unlock(lock)
-	}
-
 	return arch, startIdx
 }
 
@@ -193,24 +197,45 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 }
 
 func (w *World) newEntitiesWithQuery(count int, comps ...Component) Query {
-	arch, startIdx := w.newEntitiesWith(count, comps...)
+	ids := make([]ID, len(comps))
+	for i, c := range comps {
+		ids[i] = c.ID
+	}
+
+	arch, startIdx := w.newEntitiesWithNoNotify(count, ids, comps...)
 	lock := w.lock()
 	return newArchQuery(w, lock, arch, startIdx)
 }
 
 func (w *World) newEntitiesWith(count int, comps ...Component) (*archetype, uint32) {
+	ids := make([]ID, len(comps))
+	for i, c := range comps {
+		ids[i] = c.ID
+	}
+
+	arch, startIdx := w.newEntitiesWithNoNotify(count, ids, comps...)
+
+	if w.listener != nil {
+		var i uint32
+		cnt := uint32(count)
+		for i = 0; i < cnt; i++ {
+			idx := startIdx + i
+			entity := arch.GetEntity(uintptr(idx))
+			w.listener(EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.Ids, 1})
+		}
+	}
+
+	return arch, startIdx
+}
+
+func (w *World) newEntitiesWithNoNotify(count int, ids []ID, comps ...Component) (*archetype, uint32) {
 	w.checkLocked()
 
 	if count < 1 {
 		panic("can only create a positive number of entities")
 	}
 	if len(comps) == 0 {
-		return w.newEntities(count)
-	}
-
-	ids := make([]ID, len(comps))
-	for i, c := range comps {
-		ids[i] = c.ID
+		return w.newEntitiesNoNotify(count)
 	}
 
 	cnt := uint32(count)
@@ -231,17 +256,6 @@ func (w *World) newEntitiesWith(count int, comps ...Component) (*archetype, uint
 		}
 	}
 
-	if w.listener != nil {
-		lock := w.lock()
-		var i uint32
-		for i = 0; i < cnt; i++ {
-			idx := startIdx + i
-			entity := arch.GetEntity(uintptr(idx))
-			w.listener(EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.Ids, 1})
-		}
-		w.unlock(lock)
-	}
-
 	return arch, startIdx
 }
 
@@ -464,7 +478,7 @@ func (w *World) Resources() *Resources {
 	return &w.resources
 }
 
-// Reset removes all entities and resources as well as the listener from the world.
+// Reset removes all entities and resources from the world.
 //
 // Does NOT free reserved memory, remove archetypes, clear the registry etc.
 //
@@ -478,8 +492,6 @@ func (w *World) Reset() {
 	w.bitPool.Reset()
 	w.resources.reset()
 
-	w.listener = nil
-
 	len := w.archetypes.Len()
 	for i := 0; i < len; i++ {
 		w.archetypes.Get(i).Reset()
@@ -549,10 +561,7 @@ func (w *World) Mask(entity Entity) Mask {
 // The listener is immediately called on every [ecs.Entity] change.
 // Replaces the current listener. Call with `nil` to remove a listener.
 //
-// Events notified are entity creation, removal and changes to the component composition.
-// Events are emitted immediately after the change is applied.
-// Except for removal of an entity, where the event is emitted before removal.
-// This allows for inspection of the to-be-removed Entity.
+// For details, see [EntityEvent].
 func (w *World) SetListener(listener func(e EntityEvent)) {
 	w.listener = listener
 }
@@ -738,6 +747,12 @@ func (w *World) resourceID(tp reflect.Type) ResID {
 func (w *World) closeQuery(query *Query) {
 	query.index = -2
 	w.unlock(query.lockBit)
+
+	if w.listener != nil {
+		if arch, ok := query.archetypes.(batchArchetype); ok {
+			w.notifyQuery(&arch)
+		}
+	}
 }
 
 // checkLocked checks if the world is locked, and panics if so.
@@ -746,3 +761,16 @@ func (w *World) checkLocked() {
 		panic("attempt to modify a locked world")
 	}
 }
+
+func (w *World) notifyQuery(batchArch *batchArchetype) {
+	arch := batchArch.Archetype
+	var i uintptr
+	len := uintptr(arch.Len())
+	event := EntityEvent{Entity{}, Mask{}, arch.Mask, arch.Ids, nil, arch.Ids, 1}
+
+	for i = uintptr(batchArch.StartIndex); i < len; i++ {
+		entity := arch.GetEntity(i)
+		event.Entity = entity
+		w.listener(event)
+	}
+}
diff --git a/generic/generate/map.go.txt b/generic/generate/map.go.txt
index 163a6d3..60bbceb 100644
--- a/generic/generate/map.go.txt
+++ b/generic/generate/map.go.txt
@@ -43,6 +43,8 @@ func (m *Map{{ .Index }}{{ .Types }}) NewEntities(count int) {
 // NewEntities creates entities with the Map{{ .Index }}'s components.
 // It returns a [Query{{ .Index }}] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map{{ .Index }}.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map{{ .Index }}{{ .Types }}) NewEntitiesQuery(count int) Query{{ .Index }}{{ .Types }} {
 	query := m.world.Batch().NewEntitiesQuery(count, {{ .IDList }})
@@ -73,6 +75,8 @@ func (m *Map{{ .Index }}{{ .Types }}) NewEntitiesWith(count int, {{ .Arguments }
 // NewEntitiesWithQuery creates entities with the Map{{ .Index }}'s components, using the supplied values.
 // It returns a [Query{{ .Index }}] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map{{ .Index }}.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map{{ .Index }}{{ .Types }}) NewEntitiesWithQuery(count int, {{ .Arguments }}) Query{{ .Index }}{{ .Types }} {
 	query := m.world.Batch().NewEntitiesWithQuery(count, {{ .Components }})
diff --git a/generic/map_generated.go b/generic/map_generated.go
index 9175d36..cfee65f 100644
--- a/generic/map_generated.go
+++ b/generic/map_generated.go
@@ -50,6 +50,8 @@ func (m *Map1[A]) NewEntities(count int) {
 // NewEntities creates entities with the Map1's components.
 // It returns a [Query1] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map1.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map1[A]) NewEntitiesQuery(count int) Query1[A] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0)
@@ -79,6 +81,8 @@ func (m *Map1[A]) NewEntitiesWith(count int, a *A) {
 // NewEntitiesWithQuery creates entities with the Map1's components, using the supplied values.
 // It returns a [Query1] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map1.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map1[A]) NewEntitiesWithQuery(count int, a *A) Query1[A] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a})
@@ -170,6 +174,8 @@ func (m *Map2[A, B]) NewEntities(count int) {
 // NewEntities creates entities with the Map2's components.
 // It returns a [Query2] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map2.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map2[A, B]) NewEntitiesQuery(count int) Query2[A, B] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1)
@@ -203,6 +209,8 @@ func (m *Map2[A, B]) NewEntitiesWith(count int, a *A, b *B) {
 // NewEntitiesWithQuery creates entities with the Map2's components, using the supplied values.
 // It returns a [Query2] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map2.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map2[A, B]) NewEntitiesWithQuery(count int, a *A, b *B) Query2[A, B] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -301,6 +309,8 @@ func (m *Map3[A, B, C]) NewEntities(count int) {
 // NewEntities creates entities with the Map3's components.
 // It returns a [Query3] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map3.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map3[A, B, C]) NewEntitiesQuery(count int) Query3[A, B, C] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2)
@@ -337,6 +347,8 @@ func (m *Map3[A, B, C]) NewEntitiesWith(count int, a *A, b *B, c *C) {
 // NewEntitiesWithQuery creates entities with the Map3's components, using the supplied values.
 // It returns a [Query3] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map3.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map3[A, B, C]) NewEntitiesWithQuery(count int, a *A, b *B, c *C) Query3[A, B, C] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -441,6 +453,8 @@ func (m *Map4[A, B, C, D]) NewEntities(count int) {
 // NewEntities creates entities with the Map4's components.
 // It returns a [Query4] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map4.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map4[A, B, C, D]) NewEntitiesQuery(count int) Query4[A, B, C, D] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2, m.id3)
@@ -480,6 +494,8 @@ func (m *Map4[A, B, C, D]) NewEntitiesWith(count int, a *A, b *B, c *C, d *D) {
 // NewEntitiesWithQuery creates entities with the Map4's components, using the supplied values.
 // It returns a [Query4] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map4.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map4[A, B, C, D]) NewEntitiesWithQuery(count int, a *A, b *B, c *C, d *D) Query4[A, B, C, D] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -590,6 +606,8 @@ func (m *Map5[A, B, C, D, E]) NewEntities(count int) {
 // NewEntities creates entities with the Map5's components.
 // It returns a [Query5] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map5.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map5[A, B, C, D, E]) NewEntitiesQuery(count int) Query5[A, B, C, D, E] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2, m.id3, m.id4)
@@ -632,6 +650,8 @@ func (m *Map5[A, B, C, D, E]) NewEntitiesWith(count int, a *A, b *B, c *C, d *D,
 // NewEntitiesWithQuery creates entities with the Map5's components, using the supplied values.
 // It returns a [Query5] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map5.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map5[A, B, C, D, E]) NewEntitiesWithQuery(count int, a *A, b *B, c *C, d *D, e *E) Query5[A, B, C, D, E] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -748,6 +768,8 @@ func (m *Map6[A, B, C, D, E, F]) NewEntities(count int) {
 // NewEntities creates entities with the Map6's components.
 // It returns a [Query6] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map6.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map6[A, B, C, D, E, F]) NewEntitiesQuery(count int) Query6[A, B, C, D, E, F] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2, m.id3, m.id4, m.id5)
@@ -793,6 +815,8 @@ func (m *Map6[A, B, C, D, E, F]) NewEntitiesWith(count int, a *A, b *B, c *C, d
 // NewEntitiesWithQuery creates entities with the Map6's components, using the supplied values.
 // It returns a [Query6] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map6.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map6[A, B, C, D, E, F]) NewEntitiesWithQuery(count int, a *A, b *B, c *C, d *D, e *E, f *F) Query6[A, B, C, D, E, F] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -915,6 +939,8 @@ func (m *Map7[A, B, C, D, E, F, G]) NewEntities(count int) {
 // NewEntities creates entities with the Map7's components.
 // It returns a [Query7] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map7.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map7[A, B, C, D, E, F, G]) NewEntitiesQuery(count int) Query7[A, B, C, D, E, F, G] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2, m.id3, m.id4, m.id5, m.id6)
@@ -963,6 +989,8 @@ func (m *Map7[A, B, C, D, E, F, G]) NewEntitiesWith(count int, a *A, b *B, c *C,
 // NewEntitiesWithQuery creates entities with the Map7's components, using the supplied values.
 // It returns a [Query7] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map7.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map7[A, B, C, D, E, F, G]) NewEntitiesWithQuery(count int, a *A, b *B, c *C, d *D, e *E, f *F, g *G) Query7[A, B, C, D, E, F, G] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},
@@ -1091,6 +1119,8 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewEntities(count int) {
 // NewEntities creates entities with the Map8's components.
 // It returns a [Query8] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map8.NewEntities] and [ecs.Batch.NewEntitiesQuery].
 func (m *Map8[A, B, C, D, E, F, G, H]) NewEntitiesQuery(count int) Query8[A, B, C, D, E, F, G, H] {
 	query := m.world.Batch().NewEntitiesQuery(count, m.id0, m.id1, m.id2, m.id3, m.id4, m.id5, m.id6, m.id7)
@@ -1142,6 +1172,8 @@ func (m *Map8[A, B, C, D, E, F, G, H]) NewEntitiesWith(count int, a *A, b *B, c
 // NewEntitiesWithQuery creates entities with the Map8's components, using the supplied values.
 // It returns a [Query8] over the new entities.
 //
+// Listener notification is delayed until the query is closed of fully iterated.
+//
 // See also [Map8.NewEntitiesWith] and [ecs.Batch.NewEntitiesWithQuery].
 func (m *Map8[A, B, C, D, E, F, G, H]) NewEntitiesWithQuery(count int, a *A, b *B, c *C, d *D, e *E, f *F, g *G, h *H) Query8[A, B, C, D, E, F, G, H] {
 	query := m.world.Batch().NewEntitiesWithQuery(count, ecs.Component{ID: m.id0, Comp: a},


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 22edc7b..954dcb1 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -94,6 +94,7 @@ func TestArchetypeExtend(t *testing.T) {
 	}
 	arch := archetype{}
 	arch.Init(nil, 8, true, comps...)
+
 	assert.Equal(t, 8, int(arch.Cap()))
 	assert.Equal(t, 0, int(arch.Len()))
 
@@ -107,6 +108,29 @@ func TestArchetypeExtend(t *testing.T) {
 	assert.Equal(t, 24, int(arch.Cap()))
 }
 
+func TestArchetypeAlloc(t *testing.T) {
+	comps := []componentType{
+		{ID: 0, Type: reflect.TypeOf(position{})},
+		{ID: 1, Type: reflect.TypeOf(rotation{})},
+	}
+	arch := archetype{}
+	arch.Init(nil, 8, true, comps...)
+
+	assert.Equal(t, 8, int(arch.Cap()))
+	assert.Equal(t, 0, int(arch.Len()))
+
+	arch.AllocN(1, true)
+	assert.Equal(t, 1, int(arch.Len()))
+
+	arch.AllocN(7, true)
+	assert.Equal(t, 8, int(arch.Len()))
+	assert.Equal(t, 8, int(arch.Cap()))
+
+	arch.AllocN(1, true)
+	assert.Equal(t, 9, int(arch.Len()))
+	assert.Equal(t, 16, int(arch.Cap()))
+}
+
 func TestArchetypeAddGetSet(t *testing.T) {
 	a := archetype{}
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 87e70d9..8a349c1 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -361,7 +361,12 @@ func TestWorldIter(t *testing.T) {
 
 func TestWorldNewEntities(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
-	world.SetListener(func(e EntityEvent) {})
+
+	events := []EntityEvent{}
+	world.SetListener(func(e EntityEvent) {
+		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
+		events = append(events, e)
+	})
 
 	posID := ComponentID[position](&world)
 	rotID := ComponentID[rotation](&world)
@@ -372,6 +377,7 @@ func TestWorldNewEntities(t *testing.T) {
 
 	query := world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 100, query.Count())
+	assert.Equal(t, 1, len(events))
 
 	cnt := 0
 	for query.Next() {
@@ -381,6 +387,7 @@ func TestWorldNewEntities(t *testing.T) {
 		cnt++
 	}
 	assert.Equal(t, 100, cnt)
+	assert.Equal(t, 101, len(events))
 
 	query = world.Query(All(posID, rotID))
 	assert.Equal(t, 101, query.Count())
@@ -396,6 +403,7 @@ func TestWorldNewEntities(t *testing.T) {
 
 	query = world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 100, query.Count())
+	assert.Equal(t, 101, len(events))
 
 	entities := make([]Entity, query.Count())
 	cnt = 0
@@ -404,17 +412,30 @@ func TestWorldNewEntities(t *testing.T) {
 		cnt++
 	}
 	assert.Equal(t, 100, cnt)
+	assert.Equal(t, 201, len(events))
 
 	for _, e := range entities {
 		world.RemoveEntity(e)
 	}
+	assert.Equal(t, 301, len(events))
+
 	query = world.newEntitiesQuery(100, posID, rotID)
+	assert.Equal(t, 301, len(events))
 	query.Close()
+	assert.Equal(t, 401, len(events))
+
+	world.newEntities(100, posID, rotID)
+	assert.Equal(t, 501, len(events))
 }
 
 func TestWorldNewEntitiesWith(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
-	world.SetListener(func(e EntityEvent) {})
+
+	events := []EntityEvent{}
+	world.SetListener(func(e EntityEvent) {
+		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
+		events = append(events, e)
+	})
 
 	posID := ComponentID[position](&world)
 	rotID := ComponentID[rotation](&world)
@@ -425,13 +446,19 @@ func TestWorldNewEntitiesWith(t *testing.T) {
 	}
 
 	world.NewEntity(posID, rotID)
+	assert.Equal(t, 1, len(events))
 
 	assert.Panics(t, func() { world.newEntitiesWithQuery(0, comps...) })
+	assert.Equal(t, 1, len(events))
+
 	query := world.newEntitiesWithQuery(1)
+	assert.Equal(t, 1, len(events))
 	query.Close()
+	assert.Equal(t, 2, len(events))
 
 	query = world.newEntitiesWithQuery(100, comps...)
 	assert.Equal(t, 100, query.Count())
+	assert.Equal(t, 2, len(events))
 
 	cnt := 0
 	for query.Next() {
@@ -443,6 +470,7 @@ func TestWorldNewEntitiesWith(t *testing.T) {
 		cnt++
 	}
 	assert.Equal(t, 100, cnt)
+	assert.Equal(t, 102, len(events))
 
 	query = world.Query(All(posID, rotID))
 	assert.Equal(t, 101, query.Count())
@@ -461,17 +489,27 @@ func TestWorldNewEntitiesWith(t *testing.T) {
 		Component{ID: rotID, Comp: &rotation{300}},
 	)
 	assert.Equal(t, 100, query.Count())
+	assert.Equal(t, 102, len(events))
 
 	cnt = 0
 	for query.Next() {
 		cnt++
 	}
 	assert.Equal(t, 100, cnt)
+	assert.Equal(t, 202, len(events))
+
+	world.newEntitiesWith(100, comps...)
+	assert.Equal(t, 302, len(events))
 }
 
 func TestWorldRemoveEntities(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
-	world.SetListener(func(e EntityEvent) {})
+
+	events := []EntityEvent{}
+	world.SetListener(func(e EntityEvent) {
+		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
+		events = append(events, e)
+	})
 
 	posID := ComponentID[position](&world)
 	rotID := ComponentID[rotation](&world)
@@ -479,10 +517,12 @@ func TestWorldRemoveEntities(t *testing.T) {
 	query := world.newEntitiesQuery(100, posID)
 	assert.Equal(t, 100, query.Count())
 	query.Close()
+	assert.Equal(t, 100, len(events))
 
 	query = world.newEntitiesQuery(100, posID, rotID)
 	assert.Equal(t, 100, query.Count())
 	query.Close()
+	assert.Equal(t, 200, len(events))
 
 	query = world.Query(All())
 	assert.Equal(t, 200, query.Count())
@@ -490,6 +530,7 @@ func TestWorldRemoveEntities(t *testing.T) {
 
 	filter := All(posID).Exact()
 	world.removeEntities(&filter)
+	assert.Equal(t, 300, len(events))
 
 	query = world.Query(All())
 	assert.Equal(t, 100, query.Count())
@@ -604,7 +645,6 @@ func TestWorldReset(t *testing.T) {
 	assert.Equal(t, 0, int(world.archetypes.Get(0).Len()))
 	assert.Equal(t, 0, world.entityPool.Len())
 	assert.Equal(t, 1, len(world.entities))
-	assert.Nil(t, world.listener)
 
 	query := world.Query(All())
 	assert.Equal(t, 0, query.Count())