Repository: mlange-42/arche
Pull request #161 (2023-03-25 15:57:12)

Cleanup docs and code
* Bundle locks and bit pool in a separate struct
* Document more struct fields


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 19313bb..f899ea0 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -50,10 +50,6 @@ func (a *archetypeNode) SetTransitionRemove(id ID, to *archetypeNode) {
 	a.toRemove[id] = to
 }
 
-// Synonym for pagedArr32[archetype].
-// Implements [archetypes].
-type archetypeArr = pagedArr32[archetype]
-
 // Interface for an iterator over archetypes
 type archetypes interface {
 	Get(index int) *archetype
@@ -122,16 +118,16 @@ func (l *layout) Get(index uintptr) unsafe.Pointer {
 
 // archetype represents an ECS archetype
 type archetype struct {
-	archetypeAccess
-	Ids               []ID
-	layouts           []layout
-	indices           []uint32
-	buffers           []reflect.Value
-	entityBuffer      reflect.Value
-	len               uint32
-	cap               uint32
-	capacityIncrement uint32
-	graphNode         *archetypeNode
+	archetypeAccess                   // Access helper, passed to queries.
+	graphNode         *archetypeNode  // Node in the archetype graph.
+	Ids               []ID            // List of component IDs.
+	layouts           []layout        // Column layouts by ID.
+	indices           []uint32        // Mapping from IDs to buffer indices.
+	buffers           []reflect.Value // Reflection arrays containing component data.
+	entityBuffer      reflect.Value   // Reflection array containing entity data.
+	len               uint32          // Current number of entities
+	cap               uint32          // Current capacity
+	capacityIncrement uint32          // Capacity increment
 }
 
 // Init initializes an archetype
diff --git a/ecs/bit_pool.go b/ecs/bit_pool.go
index f22cf85..b971d07 100644
--- a/ecs/bit_pool.go
+++ b/ecs/bit_pool.go
@@ -8,16 +8,6 @@ type bitPool struct {
 	available uint8
 }
 
-// newBitPool creates a new, initialized bit pool.
-func newBitPool() bitPool {
-	return bitPool{
-		bits:      [MaskTotalBits]uint8{},
-		next:      0,
-		length:    0,
-		available: 0,
-	}
-}
-
 // Get returns a fresh or recycled bit.
 func (p *bitPool) Get() uint8 {
 	if p.available == 0 {
diff --git a/ecs/bitmask.go b/ecs/bitmask.go
index 956fed5..d4be7a0 100644
--- a/ecs/bitmask.go
+++ b/ecs/bitmask.go
@@ -11,8 +11,8 @@ const wordSize = 64
 // Mask is a 128 bit bitmask.
 // It is also a [Filter] for including certain components (see [All] and [Mask.Without]).
 type Mask struct {
-	Lo uint64
-	Hi uint64
+	Lo uint64 // First 64 bits of the mask
+	Hi uint64 // Second 64 bits of the mask
 }
 
 // All creates a new Mask from a list of IDs.
diff --git a/ecs/config.go b/ecs/config.go
index 07ec7c8..a4a9426 100644
--- a/ecs/config.go
+++ b/ecs/config.go
@@ -2,6 +2,8 @@ package ecs
 
 // Config provides configuration for an ECS [World].
 type Config struct {
+	// Capacity increment for archetypes and the entity index.
+	// The default value is 128.
 	CapacityIncrement int
 }
 
diff --git a/ecs/event.go b/ecs/event.go
index abfb3b1..e9d986c 100644
--- a/ecs/event.go
+++ b/ecs/event.go
@@ -16,14 +16,10 @@ package ecs
 // 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
-	// The old and new component masks.
-	OldMask, NewMask Mask
-	// Components added, removed, and after the change.
-	Added, Removed, Current []ID
-	// Whether the entity itself was added (> 0), removed (< 0), or only changed (= 0).
-	AddedRemoved int
+	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.
+	AddedRemoved            int    // Whether the entity itself was added (> 0), removed (< 0), or only changed (= 0).
 }
 
 // EntityAdded reports whether the entity was newly added.
diff --git a/ecs/query.go b/ecs/query.go
index ca7881a..dec73c3 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -18,8 +18,8 @@ type Filter interface {
 // MaskFilter is a [Filter] for including and excluding certain components.
 // See [All] and [Mask.Without].
 type MaskFilter struct {
-	Include Mask
-	Exclude Mask
+	Include Mask // Components to include.
+	Exclude Mask // Components to exclude.
 }
 
 // Matches matches a filter against a mask.
diff --git a/ecs/types.go b/ecs/types.go
index c7d0fde..e17cbb6 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -16,8 +16,8 @@ type ResID = uint8
 // It is a helper for [World.Assign] and [World.NewEntityWith].
 // It is not related to how components are implemented in Arche.
 type Component struct {
-	ID   ID
-	Comp interface{}
+	ID   ID          // Component ID.
+	Comp interface{} // The component, as a pointer to a struct.
 }
 
 // componentType is a component ID with a data type
diff --git a/ecs/util.go b/ecs/util.go
index 8d9cdc6..e2f2978 100644
--- a/ecs/util.go
+++ b/ecs/util.go
@@ -1,5 +1,6 @@
 package ecs
 
+// Calculates the capacity required for size, given an increment.
 func capacity(size, increment int) int {
 	cap := increment * (size / increment)
 	if size%increment != 0 {
@@ -8,6 +9,7 @@ func capacity(size, increment int) int {
 	return cap
 }
 
+// Calculates the capacity required for size, given an increment.
 func capacityU32(size, increment uint32) uint32 {
 	cap := increment * (size / increment)
 	if size%increment != 0 {
@@ -15,3 +17,36 @@ func capacityU32(size, increment uint32) uint32 {
 	}
 	return cap
 }
+
+// Manages locks by mask bits.
+type lockMask struct {
+	locks   Mask    // The actual locks.
+	bitPool bitPool // The bit pool for getting and recycling bits.
+}
+
+// Lock the world and get the Lock bit for later unlocking.
+func (m *lockMask) Lock() uint8 {
+	lock := m.bitPool.Get()
+	m.locks.Set(ID(lock), true)
+	return lock
+}
+
+// Unlock unlocks the given lock bit.
+func (m *lockMask) Unlock(l uint8) {
+	if !m.locks.Get(ID(l)) {
+		panic("unbalanced unlock")
+	}
+	m.locks.Set(ID(l), false)
+	m.bitPool.Recycle(l)
+}
+
+// IsLocked returns whether the world is locked by any queries.
+func (m *lockMask) IsLocked() bool {
+	return !m.locks.IsZero()
+}
+
+// Reset the locks and the pool.
+func (m *lockMask) Reset() {
+	m.locks = Mask{}
+	m.bitPool.Reset()
+}
diff --git a/ecs/world.go b/ecs/world.go
index 4d4a272..7b52641 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -24,11 +24,11 @@ func ResourceID[T any](w *World) ResID {
 	return w.resourceID(tp)
 }
 
-// GetResource returns a pointer to the given resource type.
+// GetResource returns a pointer to the given resource type in world.
 //
 // Returns nil if there is no such resource.
 //
-// Uses reflection. For more efficient access, see [World.GetResource],
+// Uses reflection. For more efficient access, see [World.Resources],
 // and [github.com/mlange-42/arche/generic.Resource.Get] for a generic variant.
 // These methods are more than 20 times faster than the GetResource function.
 func GetResource[T any](w *World) *T {
@@ -47,16 +47,15 @@ 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
-	entities   []entityIndex
-	archetypes archetypeArr
-	graph      pagedArr32[archetypeNode]
-	entityPool entityPool
-	bitPool    bitPool
-	registry   componentRegistry[ID]
-	locks      Mask
-	listener   func(e EntityEvent)
-	resources  Resources
+	config     Config                    // World configuration.
+	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.
+	archetypes pagedArr32[archetype]     // The archetypes.
+	graph      pagedArr32[archetypeNode] // The archetype graph.
+	locks      lockMask                  // World locks.
+	registry   componentRegistry[ID]     // Component registry.
 }
 
 // NewWorld creates a new [World] from an optional [Config].
@@ -84,11 +83,10 @@ func fromConfig(conf Config) World {
 		config:     conf,
 		entities:   entities,
 		entityPool: newEntityPool(conf.CapacityIncrement),
-		bitPool:    newBitPool(),
 		registry:   newComponentRegistry(),
-		archetypes: archetypeArr{},
+		archetypes: pagedArr32[archetype]{},
 		graph:      pagedArr32[archetypeNode]{},
-		locks:      Mask{},
+		locks:      lockMask{},
 		listener:   nil,
 		resources:  newResources(),
 	}
@@ -120,45 +118,6 @@ func (w *World) NewEntity(comps ...ID) Entity {
 	return entity
 }
 
-func (w *World) newEntitiesQuery(count int, comps ...ID) Query {
-	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")
-	}
-
-	arch := w.archetypes.Get(0)
-	if len(comps) > 0 {
-		arch = w.findOrCreateArchetype(arch, comps, nil)
-	}
-	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count), true)
-
-	return arch, startIdx
-}
-
 // NewEntityWith returns a new or recycled [Entity].
 // The given component values are assigned to the entity.
 //
@@ -196,17 +155,34 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 	return entity
 }
 
-func (w *World) newEntitiesWithQuery(count int, comps ...Component) Query {
-	ids := make([]ID, len(comps))
-	for i, c := range comps {
-		ids[i] = c.ID
+// Creates new entities without returning a query over them.
+// Used via [World.Batch].
+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})
+		}
 	}
 
-	arch, startIdx := w.newEntitiesWithNoNotify(count, ids, comps...)
+	return arch, startIdx
+}
+
+// Creates new entities and returns a query over them.
+// Used via [World.Batch].
+func (w *World) newEntitiesQuery(count int, comps ...ID) Query {
+	arch, startIdx := w.newEntitiesNoNotify(count, comps...)
 	lock := w.lock()
 	return newArchQuery(w, lock, arch, startIdx)
 }
 
+// Creates new entities with component values without returning a query over them.
+// Used via [World.Batch].
 func (w *World) newEntitiesWith(count int, comps ...Component) (*archetype, uint32) {
 	ids := make([]ID, len(comps))
 	for i, c := range comps {
@@ -228,35 +204,17 @@ func (w *World) newEntitiesWith(count int, comps ...Component) (*archetype, uint
 	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.newEntitiesNoNotify(count)
-	}
-
-	cnt := uint32(count)
-
-	arch := w.archetypes.Get(0)
-	if len(comps) > 0 {
-		arch = w.findOrCreateArchetype(arch, ids, nil)
-	}
-	startIdx := arch.Len()
-	w.createEntities(arch, uint32(count), true)
-
-	var i uint32
-	for i = 0; i < cnt; i++ {
-		idx := startIdx + i
-		entity := arch.GetEntity(uintptr(idx))
-		for _, c := range comps {
-			w.copyTo(entity, c.ID, c.Comp)
-		}
+// Creates new entities with component values and returns a query over them.
+// Used via [World.Batch].
+func (w *World) newEntitiesWithQuery(count int, comps ...Component) Query {
+	ids := make([]ID, len(comps))
+	for i, c := range comps {
+		ids[i] = c.ID
 	}
 
-	return arch, startIdx
+	arch, startIdx := w.newEntitiesWithNoNotify(count, ids, comps...)
+	lock := w.lock()
+	return newArchQuery(w, lock, arch, startIdx)
 }
 
 // RemoveEntity removes and recycles an [Entity].
@@ -489,7 +447,7 @@ func (w *World) Reset() {
 
 	w.entities = w.entities[:1]
 	w.entityPool.Reset()
-	w.bitPool.Reset()
+	w.locks.Reset()
 	w.resources.reset()
 
 	len := w.archetypes.Len()
@@ -529,25 +487,9 @@ func (w *World) Batch() *Batch {
 	return &Batch{w}
 }
 
-// lock the world and get the lock bit for later unlocking.
-func (w *World) lock() uint8 {
-	lock := w.bitPool.Get()
-	w.locks.Set(ID(lock), true)
-	return lock
-}
-
-// unlock unlocks the given lock bit.
-func (w *World) unlock(l uint8) {
-	if !w.locks.Get(ID(l)) {
-		panic("unbalanced query unlock")
-	}
-	w.locks.Set(ID(l), false)
-	w.bitPool.Recycle(l)
-}
-
 // IsLocked returns whether the world is locked by any queries.
 func (w *World) IsLocked() bool {
-	return !w.locks.IsZero()
+	return w.locks.IsLocked()
 }
 
 // Mask returns the archetype [BitMask] for the given [Entity].
@@ -595,6 +537,73 @@ func (w *World) Stats() *stats.WorldStats {
 	}
 }
 
+// lock the world and get the lock bit for later unlocking.
+func (w *World) lock() uint8 {
+	return w.locks.Lock()
+}
+
+// unlock unlocks the given lock bit.
+func (w *World) unlock(l uint8) {
+	w.locks.Unlock(l)
+}
+
+// checkLocked checks if the world is locked, and panics if so.
+func (w *World) checkLocked() {
+	if w.IsLocked() {
+		panic("attempt to modify a locked world")
+	}
+}
+
+// Internal method to create new entities.
+func (w *World) newEntitiesNoNotify(count int, comps ...ID) (*archetype, uint32) {
+	w.checkLocked()
+
+	if count < 1 {
+		panic("can only create a positive number of entities")
+	}
+
+	arch := w.archetypes.Get(0)
+	if len(comps) > 0 {
+		arch = w.findOrCreateArchetype(arch, comps, nil)
+	}
+	startIdx := arch.Len()
+	w.createEntities(arch, uint32(count), true)
+
+	return arch, startIdx
+}
+
+// Internal method to create new entities with component values.
+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.newEntitiesNoNotify(count)
+	}
+
+	cnt := uint32(count)
+
+	arch := w.archetypes.Get(0)
+	if len(comps) > 0 {
+		arch = w.findOrCreateArchetype(arch, ids, nil)
+	}
+	startIdx := arch.Len()
+	w.createEntities(arch, uint32(count), true)
+
+	var i uint32
+	for i = 0; i < cnt; i++ {
+		idx := startIdx + i
+		entity := arch.GetEntity(uintptr(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, zero bool) Entity {
 	entity := w.entityPool.Get()
@@ -613,6 +622,7 @@ func (w *World) createEntity(arch *archetype, zero bool) Entity {
 	return entity
 }
 
+// createEntity creates multiple Entities and adds them to the given archetype.
 func (w *World) createEntities(arch *archetype, count uint32, zero bool) {
 	startIdx := arch.Len()
 	arch.AllocN(uint32(count), zero)
@@ -744,7 +754,7 @@ func (w *World) resourceID(tp reflect.Type) ResID {
 	return w.resources.registry.ComponentID(tp)
 }
 
-// closeQuery closes a query and unlocks the world
+// closeQuery closes a query and unlocks the world.
 func (w *World) closeQuery(query *Query) {
 	query.index = -2
 	w.unlock(query.lockBit)
@@ -756,13 +766,7 @@ func (w *World) closeQuery(query *Query) {
 	}
 }
 
-// checkLocked checks if the world is locked, and panics if so.
-func (w *World) checkLocked() {
-	if !w.locks.IsZero() {
-		panic("attempt to modify a locked world")
-	}
-}
-
+// notifies the listener for all entities on a batch query.
 func (w *World) notifyQuery(batchArch *batchArchetype) {
 	arch := batchArch.Archetype
 	var i uintptr


Test patch:
diff --git a/ecs/bit_pool_test.go b/ecs/bit_pool_test.go
index fe540cc..e67adf4 100644
--- a/ecs/bit_pool_test.go
+++ b/ecs/bit_pool_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func TestBitPool(t *testing.T) {
-	p := newBitPool()
+	p := bitPool{}
 
 	for i := 0; i < MaskTotalBits; i++ {
 		assert.Equal(t, i, int(p.Get()))
diff --git a/ecs/util_test.go b/ecs/util_test.go
index 1e1073f..2eeab3f 100644
--- a/ecs/util_test.go
+++ b/ecs/util_test.go
@@ -19,3 +19,25 @@ func TestCapacityU32(t *testing.T) {
 	assert.Equal(t, 8, int(capacityU32(8, 8)))
 	assert.Equal(t, 16, int(capacityU32(9, 8)))
 }
+
+func TestLockMask(t *testing.T) {
+	locks := lockMask{}
+
+	assert.False(t, locks.IsLocked())
+
+	l1 := locks.Lock()
+	assert.True(t, locks.IsLocked())
+	assert.Equal(t, 0, int(l1))
+
+	l2 := locks.Lock()
+	assert.True(t, locks.IsLocked())
+	assert.Equal(t, 1, int(l2))
+
+	locks.Unlock(l1)
+	assert.True(t, locks.IsLocked())
+
+	assert.Panics(t, func() { locks.Unlock(l1) })
+
+	locks.Unlock(l2)
+	assert.False(t, locks.IsLocked())
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index f62930b..78f6ee7 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -918,6 +918,7 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSize[archetypeIter]()
 	printTypeSize[Resources]()
 	printTypeSizeName[reflect.Value]("reflect.Value")
+	printTypeSize[EntityEvent]()
 }
 
 func printTypeSize[T any]() {