Repository: mlange-42/arche
Pull request #28 (2023-02-07 17:24:26)

World configuration
Add `Config` to allow for configuration of the world

Current only setting is storage capacity increment


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bd9bbc..4700ec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,9 @@
 
 ## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.1.2...main)
 
-Nothing
+## Features
+
+* Add `Config` to allow for configuration of the world (currently only storage capacity increment) (#28)
 
 ## [[v0.1.2]](https://github.com/mlange-42/arche/compare/v0.1.1...v0.1.2)
 
diff --git a/ecs/archetype.go b/ecs/archetype.go
index c1fc8e8..09066f4 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -17,7 +17,7 @@ var entityType = reflect.TypeOf(Entity{})
 
 // newArchetype creates a new Archetype for the given components
 // Component arguments must be sorted by ID!
-func newArchetype(components ...componentType) archetype {
+func newArchetype(capacityIncrement int, components ...componentType) archetype {
 	var mask Mask
 	indices := make([]ID, len(components))
 	comps := [MaskTotalBits]storage{}
@@ -31,13 +31,13 @@ func newArchetype(components ...componentType) archetype {
 
 		mask.Set(c.ID, true)
 		indices[i] = c.ID
-		comps[c.ID] = newStorage(c.Type, 32)
+		comps[c.ID] = newStorage(c.Type, capacityIncrement)
 	}
 
 	return archetype{
 		mask:       mask,
 		indices:    indices,
-		entities:   newStorage(entityType, 32),
+		entities:   newStorage(entityType, capacityIncrement),
 		components: comps,
 	}
 }
diff --git a/ecs/config.go b/ecs/config.go
new file mode 100644
index 0000000..a9da0e8
--- /dev/null
+++ b/ecs/config.go
@@ -0,0 +1,25 @@
+package ecs
+
+// Config provides configuration for en ECS World
+type Config struct {
+	CapacityIncrement int
+}
+
+// NewConfig creates a new default config
+func NewConfig() Config {
+	return Config{
+		CapacityIncrement: 128,
+	}
+}
+
+// WithCapacityIncrement return a new Config with the given setting.
+// Use with method chaining.
+func (c Config) WithCapacityIncrement(inc int) Config {
+	c.CapacityIncrement = inc
+	return c
+}
+
+// Build creates a World from this Config
+func (c Config) Build() World {
+	return FromConfig(c)
+}
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index 03b33a0..770bba7 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -3,26 +3,33 @@ package ecs
 import "math"
 
 // newEntityPool creates a new, initialized Entity pool
-func newEntityPool() entityPool {
+func newEntityPool(capacityIncrement int) entityPool {
 	return entityPool{
-		entities:  []Entity{{0, math.MaxUint16}},
-		next:      0,
-		available: 0,
+		entities:          []Entity{{0, math.MaxUint16}},
+		next:              0,
+		available:         0,
+		capacityIncrement: capacityIncrement,
 	}
 }
 
 // entityPool is an implementation using implicit linked lists.
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
 type entityPool struct {
-	entities  []Entity
-	next      ID
-	available uint32
+	entities          []Entity
+	next              ID
+	available         uint32
+	capacityIncrement int
 }
 
 // Get returns a fresh or recycled entity
 func (p *entityPool) Get() Entity {
 	if p.available == 0 {
 		e := newEntity(len(p.entities))
+		if len(p.entities) == cap(p.entities) {
+			old := p.entities
+			p.entities = make([]Entity, len(p.entities), len(p.entities)+p.capacityIncrement)
+			copy(p.entities, old)
+		}
 		p.entities = append(p.entities, e)
 		return e
 	}
diff --git a/ecs/world.go b/ecs/world.go
index 1007fa2..2773944 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -8,18 +8,25 @@ import (
 
 // NewWorld creates a new World
 func NewWorld() World {
+	return FromConfig(NewConfig())
+}
+
+// FromConfig creates a new World from a Config
+func FromConfig(conf Config) World {
 	return World{
+		config:     conf,
 		entities:   []entityIndex{{-1, 0}},
-		entityPool: newEntityPool(),
+		entityPool: newEntityPool(conf.CapacityIncrement),
 		bitPool:    newBitPool(),
 		registry:   newComponentRegistry(),
-		archetypes: []archetype{newArchetype()},
+		archetypes: []archetype{newArchetype(conf.CapacityIncrement)},
 		locks:      Mask(0),
 	}
 }
 
 // World holds all ECS data
 type World struct {
+	config     Config
 	entities   []entityIndex
 	archetypes []archetype
 	entityPool entityPool
@@ -37,6 +44,11 @@ func (w *World) NewEntity() Entity {
 	entity := w.entityPool.Get()
 	idx := w.archetypes[0].Add(entity)
 	if int(entity.id) == len(w.entities) {
+		if len(w.entities) == cap(w.entities) {
+			old := w.entities
+			w.entities = make([]entityIndex, len(w.entities), len(w.entities)+w.config.CapacityIncrement)
+			copy(w.entities, old)
+		}
 		w.entities = append(w.entities, entityIndex{0, idx})
 	} else {
 		w.entities[entity.id] = entityIndex{0, idx}
@@ -193,7 +205,7 @@ func (w *World) createArchetype(comps ...ID) int {
 	for i, id := range comps {
 		types[i] = componentType{id, w.registry.types[id]}
 	}
-	w.archetypes = append(w.archetypes, newArchetype(types...))
+	w.archetypes = append(w.archetypes, newArchetype(w.config.CapacityIncrement, types...))
 	return len(w.archetypes) - 1
 }
 
diff --git a/profile/main/profile.go b/profile/main/profile.go
index 4141c97..dcde45a 100644
--- a/profile/main/profile.go
+++ b/profile/main/profile.go
@@ -37,7 +37,9 @@ func main() {
 
 func run(rounds, iters, entities int) {
 	for i := 0; i < rounds; i++ {
-		world := ecs.NewWorld()
+		world := ecs.NewConfig().
+			WithCapacityIncrement(1024).
+			Build()
 
 		posID := ecs.ComponentID[position](&world)
 		rotID := ecs.ComponentID[rotation](&world)


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index e872a90..8519f2b 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -13,7 +13,7 @@ func TestArchetype(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	arch := newArchetype(comps...)
+	arch := newArchetype(32, comps...)
 
 	arch.Add(
 		newEntity(0),
@@ -76,14 +76,14 @@ func TestNewArchetype(t *testing.T) {
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
-	_ = newArchetype(comps...)
+	_ = newArchetype(32, comps...)
 
 	comps = []componentType{
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 		{ID: 0, Type: reflect.TypeOf(position{})},
 	}
 	assert.Panics(t, func() {
-		_ = newArchetype(comps...)
+		_ = newArchetype(32, comps...)
 	})
 }
 
@@ -94,7 +94,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	arch := newArchetype(comps...)
+	arch := newArchetype(32, comps...)
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(
diff --git a/ecs/config_test.go b/ecs/config_test.go
new file mode 100644
index 0000000..d23eac9
--- /dev/null
+++ b/ecs/config_test.go
@@ -0,0 +1,14 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConfig(t *testing.T) {
+	c := NewConfig()
+	c = c.WithCapacityIncrement(16)
+	assert.Equal(t, 16, c.CapacityIncrement)
+	_ = c.Build()
+}
diff --git a/ecs/entity_pool_test.go b/ecs/entity_pool_test.go
index a90ce7f..84f2875 100644
--- a/ecs/entity_pool_test.go
+++ b/ecs/entity_pool_test.go
@@ -9,11 +9,11 @@ import (
 )
 
 func TestEntityPoolConstructor(t *testing.T) {
-	_ = newEntityPool()
+	_ = newEntityPool(128)
 }
 
 func TestEntityPool(t *testing.T) {
-	p := newEntityPool()
+	p := newEntityPool(128)
 
 	expectedAll := []Entity{newEntity(0), newEntity(1), newEntity(2), newEntity(3), newEntity(4), newEntity(5)}
 	expectedAll[0].gen = math.MaxUint16
@@ -54,7 +54,7 @@ func TestEntityPool(t *testing.T) {
 
 func TestEntityPoolStochastic(t *testing.T) {
 	for i := 0; i < 10; i++ {
-		p := newEntityPool()
+		p := newEntityPool(128)
 
 		alive := map[Entity]bool{}
 		for i := 0; i < 10; i++ {