Repository: mlange-42/arche
Pull request #196 (2023-03-30 20:52:23)

Code cleanup, optimize archetype creation, event docs
- diverse code cleanups
- optimize archetype creation a bit, by faster extraction of IDs from mask
- extend docs on events


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index ead6338..328a9b8 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -13,41 +13,21 @@ var layoutSize = unsafe.Sizeof(layout{})
 
 // archetypeNode is a node in the archetype graph.
 type archetypeNode struct {
-	mask      Mask                  // Mask of the archetype
-	archetype *archetype            // The archetype
-	toAdd     idMap[*archetypeNode] // Mapping from component ID to add to the resulting archetype
-	toRemove  idMap[*archetypeNode] // Mapping from component ID to remove to the resulting archetype
+	mask             Mask                  // Mask of the archetype
+	archetype        *archetype            // The archetype
+	TransitionAdd    idMap[*archetypeNode] // Mapping from component ID to add to the resulting archetype
+	TransitionRemove idMap[*archetypeNode] // Mapping from component ID to remove to the resulting archetype
 }
 
 // Creates a new archetypeNode
 func newArchetypeNode(mask Mask) archetypeNode {
 	return archetypeNode{
-		mask:     mask,
-		toAdd:    newIDMap[*archetypeNode](),
-		toRemove: newIDMap[*archetypeNode](),
+		mask:             mask,
+		TransitionAdd:    newIDMap[*archetypeNode](),
+		TransitionRemove: newIDMap[*archetypeNode](),
 	}
 }
 
-// GetTransitionAdd returns the archetypeNode resulting from adding a component
-func (a *archetypeNode) GetTransitionAdd(id ID) (*archetypeNode, bool) {
-	return a.toAdd.Get(id)
-}
-
-// GetTransitionRemove returns the archetypeNode resulting from removing a component
-func (a *archetypeNode) GetTransitionRemove(id ID) (*archetypeNode, bool) {
-	return a.toRemove.Get(id)
-}
-
-// SetTransitionAdd sets the archetypeNode resulting from adding a component
-func (a *archetypeNode) SetTransitionAdd(id ID, to *archetypeNode) {
-	a.toAdd.Set(id, to)
-}
-
-// SetTransitionRemove sets the archetypeNode resulting from removing a component
-func (a *archetypeNode) SetTransitionRemove(id ID, to *archetypeNode) {
-	a.toRemove.Set(id, to)
-}
-
 // Helper for accessing data from an archetype
 type archetypeAccess struct {
 	basePointer   unsafe.Pointer // Pointer to the first component column layout.
@@ -190,11 +170,11 @@ func (a *archetype) Add(entity Entity, components ...Component) uintptr {
 	a.addEntity(idx, &entity)
 	for _, c := range components {
 		lay := a.getLayout(c.ID)
-		dst := a.Get(uintptr(idx), c.ID)
 		if lay.itemSize == 0 {
 			continue
 		}
 		src := reflect.ValueOf(c.Comp).UnsafePointer()
+		dst := a.Get(uintptr(idx), c.ID)
 		a.copy(src, dst, lay.itemSize)
 	}
 	a.len++
diff --git a/ecs/cache.go b/ecs/cache.go
index 01248fc..7958c92 100644
--- a/ecs/cache.go
+++ b/ecs/cache.go
@@ -46,6 +46,9 @@ func newCache() Cache {
 //	cached := world.Cache().Register(&filter)
 //	query := world.Query(&cached)
 func (c *Cache) Register(f Filter) CachedFilter {
+	if _, ok := f.(*CachedFilter); ok {
+		panic("filter is already cached")
+	}
 	id := c.bitPool.Get()
 	c.filters = append(c.filters,
 		cacheEntry{
diff --git a/ecs/event.go b/ecs/event.go
index b9ccdac..b6c3139 100644
--- a/ecs/event.go
+++ b/ecs/event.go
@@ -2,7 +2,7 @@ package ecs
 
 // EntityEvent contains information about component changes to an [Entity].
 //
-// To receive change events, register a function func(e EntityEvent) with [World.SetListener].
+// 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.
@@ -15,6 +15,9 @@ package ecs
 // 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.
+//
+// Note that the event pointer received by the listener function should not be stored,
+// as the instance behind the pointer might be reused for further notifications.
 type EntityEvent struct {
 	Entity                  Entity // The entity that was changed.
 	OldMask, NewMask        Mask   // The old and new component masks.
diff --git a/ecs/world.go b/ecs/world.go
index b07bd93..60113db 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -711,23 +711,23 @@ func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID) *arc
 	mask := start.Mask
 	for _, id := range rem {
 		mask.Set(id, false)
-		if next, ok := curr.GetTransitionRemove(id); ok {
+		if next, ok := curr.TransitionRemove.Get(id); ok {
 			curr = next
 		} else {
 			next, _ := w.findOrCreateArchetypeSlow(mask)
-			next.SetTransitionAdd(id, curr)
-			curr.SetTransitionRemove(id, next)
+			next.TransitionAdd.Set(id, curr)
+			curr.TransitionRemove.Set(id, next)
 			curr = next
 		}
 	}
 	for _, id := range add {
 		mask.Set(id, true)
-		if next, ok := curr.GetTransitionAdd(id); ok {
+		if next, ok := curr.TransitionAdd.Get(id); ok {
 			curr = next
 		} else {
 			next, _ := w.findOrCreateArchetypeSlow(mask)
-			next.SetTransitionRemove(id, curr)
-			curr.SetTransitionAdd(id, next)
+			next.TransitionRemove.Set(id, curr)
+			curr.TransitionAdd.Set(id, next)
 			curr = next
 		}
 	}
@@ -773,8 +773,17 @@ func (w *World) createArchetype(node *archetypeNode, forStorage bool) *archetype
 	count := int(mask.TotalBitsSet())
 	types := make([]componentType, count)
 
+	start := 0
+	end := MaskTotalBits
+	if mask.Lo == 0 {
+		start = wordSize
+	}
+	if mask.Hi == 0 {
+		end = wordSize
+	}
+
 	idx := 0
-	for i := 0; i < MaskTotalBits; i++ {
+	for i := start; i < end; i++ {
 		id := ID(i)
 		if mask.Get(id) {
 			types[idx] = componentType{ID: id, Type: w.registry.Types[id]}


Test patch:
diff --git a/ecs/cache_test.go b/ecs/cache_test.go
index cd41c7b..22f28f8 100644
--- a/ecs/cache_test.go
+++ b/ecs/cache_test.go
@@ -18,6 +18,8 @@ func TestFilterCache(t *testing.T) {
 	assert.Equal(t, 0, int(f1.id))
 	assert.Equal(t, 1, int(f2.id))
 
+	assert.Panics(t, func() { cache.Register(&f2) })
+
 	e1 := cache.get(&f1)
 	e2 := cache.get(&f2)
 
diff --git a/ecs/event_test.go b/ecs/event_test.go
index ab7dae7..8bbb030 100644
--- a/ecs/event_test.go
+++ b/ecs/event_test.go
@@ -1,6 +1,7 @@
 package ecs
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -68,3 +69,15 @@ func BenchmarkEntityEventPointerReuse(b *testing.B) {
 		handler.ListenPointer(&event)
 	}
 }
+
+func ExampleEntityEvent() {
+	world := NewWorld()
+
+	listener := func(evt *EntityEvent) {
+		fmt.Println(evt)
+	}
+	world.SetListener(listener)
+
+	world.NewEntity()
+	// Output: &{{1 0} {0 0} {0 0} [] [] [] 1}
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index dc69ad2..2c48111 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -1294,11 +1294,10 @@ func ExampleWorld_Cache() {
 func ExampleWorld_SetListener() {
 	world := NewWorld()
 
-	world.SetListener(
-		func(evt *EntityEvent) {
-			fmt.Println(evt)
-		},
-	)
+	listener := func(evt *EntityEvent) {
+		fmt.Println(evt)
+	}
+	world.SetListener(listener)
 
 	world.NewEntity()
 	// Output: &{{1 0} {0 0} {0 0} [] [] [] 1}