Repository: mlange-42/arche
Pull request #162 (2023-03-25 16:39:24)

EntityEvent pointer for listener
World listener takes a pointer instead of a copied event struct.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59e0d7a..1acd33c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 * 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)
 * Removes methods `filter.ALL.Not()` and `filter.ANY.Not()`, use `NoneOf()` and `AnyNot()` instead (#160)
+* World listener function takes a pointer to the `EntityEvent` instead of a copy as argument (#162)
 
 ### Features
 
diff --git a/ecs/event.go b/ecs/event.go
index e9d986c..b9ccdac 100644
--- a/ecs/event.go
+++ b/ecs/event.go
@@ -18,7 +18,7 @@ package ecs
 type EntityEvent struct {
 	Entity                  Entity // The entity that was changed.
 	OldMask, NewMask        Mask   // The old and new component masks.
-	Added, Removed, Current []ID   // Components added, removed, and after the change.
+	Added, Removed, Current []ID   // Components added, removed, and after the change. DO NOT MODIFY!
 	AddedRemoved            int    // Whether the entity itself was added (> 0), removed (< 0), or only changed (= 0).
 }
 
diff --git a/ecs/world.go b/ecs/world.go
index 7b52641..aa316ac 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -48,7 +48,7 @@ func AddResource[T any](w *World, res *T) {
 // World is the central type holding [Entity] and component data, as well as resources.
 type World struct {
 	config     Config                    // World configuration.
-	listener   func(e EntityEvent)       // Component change listener.
+	listener   func(e *EntityEvent)      // Component change listener.
 	resources  Resources                 // World resources.
 	entities   []entityIndex             // Mapping from entities to archetype and index.
 	entityPool entityPool                // Pool for entities.
@@ -113,7 +113,7 @@ func (w *World) NewEntity(comps ...ID) Entity {
 	entity := w.createEntity(arch, true)
 
 	if w.listener != nil {
-		w.listener(EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
 	}
 	return entity
 }
@@ -150,7 +150,7 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 	}
 
 	if w.listener != nil {
-		w.listener(EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.Ids, 1})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.Ids, 1})
 	}
 	return entity
 }
@@ -166,7 +166,7 @@ func (w *World) newEntities(count int, comps ...ID) (*archetype, 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.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.Ids, 1})
 		}
 	}
 
@@ -197,7 +197,7 @@ func (w *World) newEntitiesWith(count int, comps ...Component) (*archetype, uint
 		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.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.Ids, 1})
 		}
 	}
 
@@ -229,7 +229,7 @@ func (w *World) RemoveEntity(entity Entity) {
 
 	if w.listener != nil {
 		lock := w.lock()
-		w.listener(EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.Ids, nil, -1})
+		w.listener(&EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.Ids, nil, -1})
 		w.unlock(lock)
 	}
 
@@ -265,7 +265,7 @@ func (w *World) removeEntities(filter Filter) {
 		for j = 0; j < len; j++ {
 			entity := arch.GetEntity(j)
 			if w.listener != nil {
-				w.listener(EntityEvent{entity, arch.Mask, Mask{}, nil, arch.Ids, nil, -1})
+				w.listener(&EntityEvent{entity, arch.Mask, Mask{}, nil, arch.Ids, nil, -1})
 			}
 			w.entities[entity.id].arch = nil
 			w.entityPool.Recycle(entity)
@@ -425,7 +425,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}
 
 	if w.listener != nil {
-		w.listener(EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.Ids, 0})
+		w.listener(&EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.Ids, 0})
 	}
 }
 
@@ -504,7 +504,7 @@ func (w *World) Mask(entity Entity) Mask {
 // Replaces the current listener. Call with `nil` to remove a listener.
 //
 // For details, see [EntityEvent].
-func (w *World) SetListener(listener func(e EntityEvent)) {
+func (w *World) SetListener(listener func(e *EntityEvent)) {
 	w.listener = listener
 }
 
@@ -776,6 +776,6 @@ func (w *World) notifyQuery(batchArch *batchArchetype) {
 	for i = uintptr(batchArch.StartIndex); i < len; i++ {
 		entity := arch.GetEntity(i)
 		event.Entity = entity
-		w.listener(event)
+		w.listener(&event)
 	}
 }
diff --git a/examples/change_listener/main.go b/examples/change_listener/main.go
index 028f3b0..4776a68 100644
--- a/examples/change_listener/main.go
+++ b/examples/change_listener/main.go
@@ -26,7 +26,7 @@ type Rotation struct {
 }
 
 // Listen is called on entity changes.
-func Listen(evt ecs.EntityEvent) {
+func Listen(evt *ecs.EntityEvent) {
 	// Just prints out what the event is about.
 	// This could also be a method of a type that manages events.
 	// Could use e.g. filters to distribute events to interested/registered systems.


Test patch:
diff --git a/ecs/batch_test.go b/ecs/batch_test.go
index 064bbc4..da1f422 100644
--- a/ecs/batch_test.go
+++ b/ecs/batch_test.go
@@ -8,7 +8,7 @@ import (
 
 func TestBatch(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
-	world.SetListener(func(e EntityEvent) {})
+	world.SetListener(func(e *EntityEvent) {})
 
 	posID := ComponentID[position](&world)
 	rotID := ComponentID[rotation](&world)
@@ -36,7 +36,7 @@ func TestBatch(t *testing.T) {
 
 func TestBatchQuery(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
-	world.SetListener(func(e EntityEvent) {})
+	world.SetListener(func(e *EntityEvent) {})
 
 	posID := ComponentID[position](&world)
 	rotID := ComponentID[rotation](&world)
diff --git a/ecs/event_test.go b/ecs/event_test.go
index a4d9766..ab7dae7 100644
--- a/ecs/event_test.go
+++ b/ecs/event_test.go
@@ -22,3 +22,49 @@ func TestEntityEvent(t *testing.T) {
 	assert.False(t, e.EntityAdded())
 	assert.True(t, e.EntityRemoved())
 }
+
+type eventHandler struct {
+	LastEntity Entity
+}
+
+func (h *eventHandler) ListenCopy(e EntityEvent) {
+	h.LastEntity = e.Entity
+}
+
+func (h *eventHandler) ListenPointer(e *EntityEvent) {
+	h.LastEntity = e.Entity
+}
+
+func BenchmarkEntityEventCopy(b *testing.B) {
+	handler := eventHandler{}
+
+	for i := 0; i < b.N; i++ {
+		handler.ListenCopy(EntityEvent{Entity: Entity{}, OldMask: Mask{}, NewMask: Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0})
+	}
+}
+
+func BenchmarkEntityEventCopyReuse(b *testing.B) {
+	handler := eventHandler{}
+	event := EntityEvent{Entity: Entity{}, OldMask: Mask{}, NewMask: Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0}
+
+	for i := 0; i < b.N; i++ {
+		handler.ListenCopy(event)
+	}
+}
+
+func BenchmarkEntityEventPointer(b *testing.B) {
+	handler := eventHandler{}
+
+	for i := 0; i < b.N; i++ {
+		handler.ListenPointer(&EntityEvent{Entity: Entity{}, OldMask: Mask{}, NewMask: Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0})
+	}
+}
+
+func BenchmarkEntityEventPointerReuse(b *testing.B) {
+	handler := eventHandler{}
+	event := EntityEvent{Entity: Entity{}, OldMask: Mask{}, NewMask: Mask{}, Added: nil, Removed: nil, Current: nil, AddedRemoved: 0}
+
+	for i := 0; i < b.N; i++ {
+		handler.ListenPointer(&event)
+	}
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 78f6ee7..f6136f2 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -363,9 +363,9 @@ func TestWorldNewEntities(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
 
 	events := []EntityEvent{}
-	world.SetListener(func(e EntityEvent) {
+	world.SetListener(func(e *EntityEvent) {
 		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
-		events = append(events, e)
+		events = append(events, *e)
 	})
 
 	posID := ComponentID[position](&world)
@@ -438,9 +438,9 @@ func TestWorldNewEntitiesWith(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
 
 	events := []EntityEvent{}
-	world.SetListener(func(e EntityEvent) {
+	world.SetListener(func(e *EntityEvent) {
 		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
-		events = append(events, e)
+		events = append(events, *e)
 	})
 
 	posID := ComponentID[position](&world)
@@ -512,9 +512,9 @@ func TestWorldRemoveEntities(t *testing.T) {
 	world := NewWorld(NewConfig().WithCapacityIncrement(16))
 
 	events := []EntityEvent{}
-	world.SetListener(func(e EntityEvent) {
+	world.SetListener(func(e *EntityEvent) {
 		assert.Equal(t, world.IsLocked(), e.EntityRemoved())
-		events = append(events, e)
+		events = append(events, *e)
 	})
 
 	posID := ComponentID[position](&world)
@@ -634,7 +634,7 @@ func TestRegisterComponents(t *testing.T) {
 func TestWorldReset(t *testing.T) {
 	world := NewWorld()
 
-	world.SetListener(func(e EntityEvent) {})
+	world.SetListener(func(e *EntityEvent) {})
 
 	posID := ComponentID[position](&world)
 	velID := ComponentID[velocity](&world)
@@ -695,8 +695,8 @@ func TestArchetypeGraph(t *testing.T) {
 
 func TestWorldListener(t *testing.T) {
 	events := []EntityEvent{}
-	listen := func(e EntityEvent) {
-		events = append(events, e)
+	listen := func(e *EntityEvent) {
+		events = append(events, *e)
 	}
 
 	w := NewWorld()