Repository: mlange-42/arche
Pull request #11 (2023-02-06 20:54:28)

Rework project structure, access, README


Patch:
diff --git a/README.md b/README.md
index 7a66d91..c98eda0 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,120 @@
 # Arche
 
-*Arche* is (to be) an archetype-based Entity Component System.
+[![Test status](https://github.com/mlange-42/arche/actions/workflows/tests.yml/badge.svg)](https://github.com/mlange-42/arche/actions/workflows/tests.yml)
+[![GitHub](https://img.shields.io/badge/github-repo-blue?logo=github)](https://github.com/mlange-42/arche)
+[![MIT license](https://img.shields.io/github/license/mlange-42/arche)](https://github.com/mlange-42/arche/blob/main/LICENSE)
+
+*Arche* is an archetype-based Entity Component System for Go.
+
+*Arche* is designed for the use in simulation models of the
+[Department for Ecological Modelling](https://www.ufz.de/index.php?en=34213) at the
+[Helmholtz Centre for Environmental Research](https://www.ufz.de).
+
+:warning: This project is in an early stage of development! :warning:
+
+## Installations
+
+```shell
+go get github.com/mlange-42/arche
+```
+
+## Features
+
+* Minimal API.
+* Very fast iteration and component access via `Query` (benchmarks for comparison in progress).
+* Fast random access for components of arbitrary entities. Useful for hierarchies.
+* No systems. Use your own structure.
+* Not thread-safe. On purpose.
+
+## Usage example
+
+Here is a minimal usage example.
+You will likely create systems with a method that takes a pointer to the `World` as argument.
+
+```go
+package main
+
+import (
+	"math/rand"
+
+	"github.com/mlange-42/arche/ecs"
+)
+
+// Position component
+type Position struct {
+	X float64
+	Y float64
+}
+
+// Velocity component
+type Velocity struct {
+	X float64
+	Y float64
+}
+
+func main() {
+	// Create a World.
+	world := ecs.NewWorld()
+
+	// Get component IDs.
+	// Registers component type if not already registered.
+	positionID := ecs.ComponentID[Position](&world)
+	velocityID := ecs.ComponentID[Velocity](&world)
+
+	// Create entities
+	for i := 0; i < 1000; i++ {
+		// Create a new Entity.
+		entity := world.NewEntity()
+		// Add components to it.
+		world.Add(entity, positionID, velocityID)
+
+		// Component access through the World.
+		// See below for faster access in queries.
+		pos := (*Position)(world.Get(entity, positionID))
+		vel := (*Velocity)(world.Get(entity, velocityID))
+
+		// Initialize component fields.
+		pos.X = rand.Float64() * 100
+		pos.Y = rand.Float64() * 100
+
+		vel.X = rand.NormFloat64()
+		vel.Y = rand.NormFloat64()
+	}
+
+	// Time loop.
+	for t := 0; t < 1000; t++ {
+		// Get a fresh query.
+		query := world.Query(positionID, velocityID)
+		// Iterate it
+		for query.Next() {
+			// Component access through a Query.
+			// About twice as fast as access through the World.
+			// Can also fetch components not in the query.
+			pos := (*Position)(query.Get(positionID))
+			vel := (*Velocity)(query.Get(velocityID))
+
+			// Update component fields.
+			pos.X += vel.X
+			pos.Y += vel.Y
+		}
+	}
+}
+```
+
+## Benchmarks
+
+[TODO]
+
+For now, see the latest [Benchmarks CI run](https://github.com/mlange-42/arche/actions/workflows/benchmarks.yml).
+
+## References
+
+Information and inspiration taken from:
+
+* [Sander Mertens](https://ajmmertens.medium.com/)' blog
+* Michele "skypjack" Caini's blog [skypjack on software](https://skypjack.github.io/)
+* [marioolofo/go-gameengine-ecs](https://github.com/marioolofo/go-gameengine-ecs) "Fast Entity Component System in Golang"
+
+## License
+
+This project is distributed under the [MIT licence](./LICENSE).
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 94d2643..c1fc8e8 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -5,22 +5,22 @@ import (
 	"unsafe"
 )
 
-// Archetype represents an ECS archetype
-type Archetype struct {
+// archetype represents an ECS archetype
+type archetype struct {
 	mask       Mask
 	indices    []ID
-	entities   Storage
-	components [MaskTotalBits]Storage
+	entities   storage
+	components [MaskTotalBits]storage
 }
 
 var entityType = reflect.TypeOf(Entity{})
 
-// NewArchetype creates a new Archetype for the given components
+// newArchetype creates a new Archetype for the given components
 // Component arguments must be sorted by ID!
-func NewArchetype(components ...ComponentType) Archetype {
+func newArchetype(components ...componentType) archetype {
 	var mask Mask
 	indices := make([]ID, len(components))
-	comps := [MaskTotalBits]Storage{}
+	comps := [MaskTotalBits]storage{}
 
 	prev := -1
 	for i, c := range components {
@@ -31,29 +31,29 @@ func NewArchetype(components ...ComponentType) Archetype {
 
 		mask.Set(c.ID, true)
 		indices[i] = c.ID
-		comps[c.ID] = NewReflectStorage(c.Type, 32)
+		comps[c.ID] = newStorage(c.Type, 32)
 	}
 
-	return Archetype{
+	return archetype{
 		mask:       mask,
 		indices:    indices,
-		entities:   NewReflectStorage(entityType, 32),
+		entities:   newStorage(entityType, 32),
 		components: comps,
 	}
 }
 
 // GetEntity returns the entity at the given index
-func (a *Archetype) GetEntity(index int) Entity {
+func (a *archetype) GetEntity(index int) Entity {
 	return *(*Entity)(a.entities.Get(uint32(index)))
 }
 
 // Get returns the component with the given ID at the given index
-func (a *Archetype) Get(index int, id ID) unsafe.Pointer {
+func (a *archetype) Get(index int, id ID) unsafe.Pointer {
 	return a.components[id].Get(uint32(index))
 }
 
 // Add adds an entity with components to the archetype
-func (a *Archetype) Add(entity Entity, components ...Component) uint32 {
+func (a *archetype) Add(entity Entity, components ...component) uint32 {
 	if len(components) != len(a.indices) {
 		panic("Invalid number of components")
 	}
@@ -65,7 +65,7 @@ func (a *Archetype) Add(entity Entity, components ...Component) uint32 {
 }
 
 // AddPointer adds an entity with components to the archetype, using pointers
-func (a *Archetype) AddPointer(entity Entity, components ...ComponentPointer) uint32 {
+func (a *archetype) AddPointer(entity Entity, components ...componentPointer) uint32 {
 	if len(components) != len(a.indices) {
 		panic("Invalid number of components")
 	}
@@ -81,7 +81,7 @@ func (a *Archetype) AddPointer(entity Entity, components ...ComponentPointer) ui
 }
 
 // Remove removes an entity from the archetype
-func (a *Archetype) Remove(index int) bool {
+func (a *archetype) Remove(index int) bool {
 	swapped := a.entities.Remove(uint32(index))
 	for _, c := range a.indices {
 		a.components[c].Remove(uint32(index))
@@ -90,16 +90,16 @@ func (a *Archetype) Remove(index int) bool {
 }
 
 // Components returns the component IDs for this archetype
-func (a *Archetype) Components() []ID {
+func (a *archetype) Components() []ID {
 	return a.indices
 }
 
 // HasComponent returns whether the archetype contains the given component ID
-func (a *Archetype) HasComponent(id ID) bool {
+func (a *archetype) HasComponent(id ID) bool {
 	return a.mask.Get(id)
 }
 
 // Len reports the number of entities in the archetype
-func (a *Archetype) Len() uint32 {
+func (a *archetype) Len() uint32 {
 	return a.entities.Len()
 }
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index d342250..091b792 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -1,44 +1,24 @@
 package ecs
 
-// EntityPool is the interface for entity recycling
-// TODO: Add a way to shrink/compact the pool?
-type EntityPool interface {
-	// Get returns a fresh or recycled entity
-	Get() Entity
-	// Recycle hands an entity back for recycling
-	Recycle(Entity)
-	// Alive return whether an entity is currently alive
-	Alive(Entity) bool
-}
-
-// NewEntityPool creates a new, initialized EntityPool
-func NewEntityPool() EntityPool {
-	return &implicitListEntityPool{
-		entities:  []Entity{},
-		next:      0,
-		available: 0,
-	}
-}
-
-// Creates a pool with proper initialization of next
-func newImplicitListEntityPool() *implicitListEntityPool {
-	return &implicitListEntityPool{
+// newEntityPool creates a new, initialized EntityPool
+func newEntityPool() entityPool {
+	return entityPool{
 		entities:  []Entity{},
 		next:      0,
 		available: 0,
 	}
 }
 
-// implicitListEntityPool is an EntityPool implementation using implicit linked lists.
+// entityPool is an entityPool implementation using implicit linked lists.
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
-type implicitListEntityPool struct {
+type entityPool struct {
 	entities  []Entity
 	next      ID
 	available uint32
 }
 
 // Get returns a fresh or recycled entity
-func (p *implicitListEntityPool) Get() Entity {
+func (p *entityPool) Get() Entity {
 	if p.available == 0 {
 		e := newEntity(len(p.entities))
 		p.entities = append(p.entities, e)
@@ -51,13 +31,13 @@ func (p *implicitListEntityPool) Get() Entity {
 }
 
 // Recycle hands an entity back for recycling
-func (p *implicitListEntityPool) Recycle(e Entity) {
+func (p *entityPool) Recycle(e Entity) {
 	p.entities[e.id].gen++
 	p.next, p.entities[e.id].id = e.id, p.next
 	p.available++
 }
 
 // Alive return whether an entity is still alive, based on the entity's generations
-func (p *implicitListEntityPool) Alive(e Entity) bool {
+func (p *entityPool) Alive(e Entity) bool {
 	return e.gen == p.entities[e.id].gen
 }
diff --git a/ecs/query.go b/ecs/query.go
index b1f80f4..2590e96 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -11,8 +11,8 @@ type Query struct {
 	done       bool
 }
 
-// NewQuery creates a new QueryIter
-func NewQuery(arches []archetypeIter) Query {
+// newQuery creates a new QueryIter
+func newQuery(arches []archetypeIter) Query {
 	return Query{
 		archetypes: arches,
 		index:      0,
@@ -53,12 +53,12 @@ func (q *Query) Entity() Entity {
 }
 
 type archetypeIter struct {
-	Archetype *Archetype
+	Archetype *archetype
 	Length    int
 	Index     int
 }
 
-func newArchetypeIter(arch *Archetype) archetypeIter {
+func newArchetypeIter(arch *archetype) archetypeIter {
 	return archetypeIter{
 		Archetype: arch,
 		Length:    int(arch.Len()),
diff --git a/ecs/registry.go b/ecs/registry.go
index 131a1df..e646824 100644
--- a/ecs/registry.go
+++ b/ecs/registry.go
@@ -2,21 +2,21 @@ package ecs
 
 import "reflect"
 
-// ComponentRegistry keeps track of component IDs
-type ComponentRegistry struct {
+// componentRegistry keeps track of component IDs
+type componentRegistry struct {
 	components map[reflect.Type]ID
 	types      [MaskTotalBits]reflect.Type
 }
 
-// NewComponentRegistry creates a new ComponentRegistry
-func NewComponentRegistry() ComponentRegistry {
-	return ComponentRegistry{
+// newComponentRegistry creates a new ComponentRegistry
+func newComponentRegistry() componentRegistry {
+	return componentRegistry{
 		components: map[reflect.Type]ID{},
 	}
 }
 
 // RegisterComponent registers a components and assigns an ID for it
-func (r *ComponentRegistry) RegisterComponent(tp reflect.Type) ID {
+func (r *componentRegistry) RegisterComponent(tp reflect.Type) ID {
 	id := ID(len(r.components))
 	r.components[tp] = id
 	r.types[id] = tp
@@ -24,7 +24,7 @@ func (r *ComponentRegistry) RegisterComponent(tp reflect.Type) ID {
 }
 
 // ComponentID returns the ID for a component type, and registers it if not already registered
-func (r *ComponentRegistry) ComponentID(tp reflect.Type) ID {
+func (r *componentRegistry) ComponentID(tp reflect.Type) ID {
 	if id, ok := r.components[tp]; ok {
 		return id
 	}
@@ -32,6 +32,6 @@ func (r *ComponentRegistry) ComponentID(tp reflect.Type) ID {
 }
 
 // ComponentType returns the type of a component by ID
-func (r *ComponentRegistry) ComponentType(id ID) reflect.Type {
+func (r *componentRegistry) ComponentType(id ID) reflect.Type {
 	return r.types[id]
 }
diff --git a/ecs/storage.go b/ecs/storage.go
index 785b350..c2ba7ce 100644
--- a/ecs/storage.go
+++ b/ecs/storage.go
@@ -6,8 +6,8 @@ import (
 	"unsafe"
 )
 
-// Storage is a storage implementation that works with reflection
-type Storage struct {
+// storage is a storage implementation that works with reflection
+type storage struct {
 	buffer            reflect.Value
 	bufferAddress     unsafe.Pointer
 	typeOf            reflect.Type
@@ -17,12 +17,12 @@ type Storage struct {
 	capacityIncrement uint32
 }
 
-// NewReflectStorage creates a new ReflectStorage
-func NewReflectStorage(tp reflect.Type, increment int) Storage {
+// newStorage creates a new ReflectStorage
+func newStorage(tp reflect.Type, increment int) storage {
 	size := tp.Size()
 
 	buffer := reflect.New(reflect.ArrayOf(increment, tp)).Elem()
-	return Storage{
+	return storage{
 		buffer:            buffer,
 		bufferAddress:     buffer.Addr().UnsafePointer(),
 		typeOf:            tp,
@@ -33,20 +33,14 @@ func NewReflectStorage(tp reflect.Type, increment int) Storage {
 	}
 }
 
-// NewReflectStorageFromTemplate creates a new ReflectStorage from a template object
-func NewReflectStorageFromTemplate(obj interface{}, increment int) Storage {
-	tp := reflect.TypeOf(obj)
-	return NewReflectStorage(tp, increment)
-}
-
 // Get retrieves an unsafe pointer to an element
-func (s *Storage) Get(index uint32) unsafe.Pointer {
+func (s *storage) Get(index uint32) unsafe.Pointer {
 	ptr := unsafe.Add(s.bufferAddress, uintptr(index)*s.itemSize)
 	return unsafe.Pointer(ptr)
 }
 
 // Add adds an element to the end of the storage
-func (s *Storage) Add(value interface{}) (index uint32) {
+func (s *storage) Add(value interface{}) (index uint32) {
 	s.extend()
 	s.len++
 	s.set(s.len-1, value)
@@ -54,7 +48,7 @@ func (s *Storage) Add(value interface{}) (index uint32) {
 }
 
 // AddPointer adds an element to the end of the storage, based on a pointer
-func (s *Storage) AddPointer(value unsafe.Pointer) (index uint32) {
+func (s *storage) AddPointer(value unsafe.Pointer) (index uint32) {
 	s.extend()
 	s.len++
 	s.setPointer(s.len-1, value)
@@ -62,14 +56,14 @@ func (s *Storage) AddPointer(value unsafe.Pointer) (index uint32) {
 }
 
 // Alloc adds an empty element to the end of the storage
-func (s *Storage) Alloc() (index uint32) {
+func (s *storage) Alloc() (index uint32) {
 	s.extend()
 	s.len++
 	s.Zero(s.len - 1)
 	return s.len - 1
 }
 
-func (s *Storage) extend() {
+func (s *storage) extend() {
 	if s.cap < s.len+1 {
 		old := s.buffer
 		s.cap = s.cap + s.capacityIncrement
@@ -80,7 +74,7 @@ func (s *Storage) extend() {
 }
 
 // Remove swap-removes an element
-func (s *Storage) Remove(index uint32) bool {
+func (s *storage) Remove(index uint32) bool {
 	o := s.len - 1
 	n := index
 
@@ -104,7 +98,7 @@ func (s *Storage) Remove(index uint32) bool {
 	return false
 }
 
-func (s *Storage) set(index uint32, value interface{}) {
+func (s *storage) set(index uint32, value interface{}) {
 	rValue := reflect.ValueOf(value)
 	dst := s.Get(index)
 	var src unsafe.Pointer
@@ -118,7 +112,7 @@ func (s *Storage) set(index uint32, value interface{}) {
 	copy(dstSlice, srcSlice)
 }
 
-func (s *Storage) setPointer(index uint32, value unsafe.Pointer) {
+func (s *storage) setPointer(index uint32, value unsafe.Pointer) {
 	dst := s.Get(index)
 	size := s.itemSize
 
@@ -129,7 +123,7 @@ func (s *Storage) setPointer(index uint32, value unsafe.Pointer) {
 }
 
 // Zero resets a block of storage
-func (s *Storage) Zero(index uint32) {
+func (s *storage) Zero(index uint32) {
 	dst := s.Get(index)
 
 	for i := uintptr(0); i < s.itemSize; i++ {
@@ -139,12 +133,12 @@ func (s *Storage) Zero(index uint32) {
 }
 
 // Len returns the number of items in the storage
-func (s *Storage) Len() uint32 {
+func (s *storage) Len() uint32 {
 	return s.len
 }
 
 // ToSlice converts the content of a storage to a slice of structs
-func ToSlice[T any](s Storage) []T {
+func ToSlice[T any](s storage) []T {
 	res := make([]T, s.Len())
 	for i := 0; i < int(s.Len()); i++ {
 		ptr := (*T)(s.Get(uint32(i)))
diff --git a/ecs/types.go b/ecs/types.go
index 7b59623..c778cd9 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -8,20 +8,20 @@ import (
 // ID defines the format for the components identifier
 type ID uint32
 
-// Component is a component ID with a reference struct
-type Component struct {
+// component is a component ID with a reference struct
+type component struct {
 	ID
 	Component interface{}
 }
 
-// ComponentType is a component ID with a data type
-type ComponentType struct {
+// componentType is a component ID with a data type
+type componentType struct {
 	ID
 	Type reflect.Type
 }
 
-// ComponentPointer is a component ID with a pointer in a storage
-type ComponentPointer struct {
+// componentPointer is a component ID with a pointer in a storage
+type componentPointer struct {
 	ID
 	Pointer unsafe.Pointer
 }
diff --git a/ecs/world.go b/ecs/world.go
index 1d022e5..037859f 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -10,21 +10,23 @@ import (
 func NewWorld() World {
 	return World{
 		entities:   []entityIndex{},
-		entityPool: NewEntityPool(),
-		registry:   NewComponentRegistry(),
-		archetypes: []Archetype{NewArchetype()},
+		entityPool: newEntityPool(),
+		registry:   newComponentRegistry(),
+		archetypes: []archetype{newArchetype()},
 	}
 }
 
 // World holds all ECS data
 type World struct {
 	entities   []entityIndex
-	archetypes []Archetype
-	entityPool EntityPool
-	registry   ComponentRegistry
+	archetypes []archetype
+	entityPool entityPool
+	registry   componentRegistry
 }
 
-// NewEntity creates a new or recycled entity
+// NewEntity creates a new or recycled entity.
+//
+// Do not use during Query iteration!
 func (w *World) NewEntity() Entity {
 	entity := w.entityPool.Get()
 	idx := w.archetypes[0].Add(entity)
@@ -36,7 +38,9 @@ func (w *World) NewEntity() Entity {
 	return entity
 }
 
-// RemEntity recycles an entity
+// RemEntity recycles an entity.
+//
+// Do not use during Query iteration!
 func (w *World) RemEntity(entity Entity) bool {
 	if !w.entityPool.Alive(entity) {
 		return false
@@ -60,7 +64,7 @@ func (w *World) RemEntity(entity Entity) bool {
 // Get returns a component for an entity
 func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	index := w.entities[entity.id]
-	arch := w.archetypes[index.arch]
+	arch := &w.archetypes[index.arch]
 
 	if !arch.HasComponent(comp) {
 		return nil
@@ -76,7 +80,9 @@ func (w *World) Has(entity Entity, comp ID) bool {
 	return arch.HasComponent(comp)
 }
 
-// Add adds components to an entity
+// Add adds components to an entity.
+//
+// Do not use during Query iteration!
 func (w *World) Add(entity Entity, comps ...ID) {
 	if len(comps) == 0 {
 		return
@@ -98,13 +104,13 @@ func (w *World) Add(entity Entity, comps ...ID) {
 	arch := &w.archetypes[archIdx]
 	oldArch = &w.archetypes[index.arch]
 
-	allComps := make([]ComponentPointer, 0, len(oldIDs)+len(comps))
+	allComps := make([]componentPointer, 0, len(oldIDs)+len(comps))
 	for _, id := range oldIDs {
 		comp := oldArch.Get(int(index.index), id)
-		allComps = append(allComps, ComponentPointer{id, comp})
+		allComps = append(allComps, componentPointer{id, comp})
 	}
 	for _, id := range comps {
-		allComps = append(allComps, ComponentPointer{id, nil})
+		allComps = append(allComps, componentPointer{id, nil})
 	}
 
 	newIndex := arch.AddPointer(entity, allComps...)
@@ -118,7 +124,9 @@ func (w *World) Add(entity Entity, comps ...ID) {
 	w.entities[entity.id] = entityIndex{archIdx, newIndex}
 }
 
-// Remove removes components from an entity
+// Remove removes components from an entity.
+//
+// Do not use during Query iteration!
 func (w *World) Remove(entity Entity, comps ...ID) {
 	if len(comps) == 0 {
 		return
@@ -145,10 +153,10 @@ func (w *World) Remove(entity Entity, comps ...ID) {
 	arch := &w.archetypes[archIdx]
 	oldArch = &w.archetypes[index.arch]
 
-	allComps := make([]ComponentPointer, 0, len(newIDs))
+	allComps := make([]componentPointer, 0, len(newIDs))
 	for _, id := range newIDs {
 		comp := oldArch.Get(int(index.index), id)
-		allComps = append(allComps, ComponentPointer{id, comp})
+		allComps = append(allComps, componentPointer{id, comp})
 	}
 
 	newIndex := arch.AddPointer(entity, allComps...)
@@ -174,11 +182,11 @@ func (w *World) findArchetype(mask Mask) (int, bool) {
 
 func (w *World) createArchetype(comps ...ID) int {
 	sort.Slice(comps, func(i, j int) bool { return comps[i] < comps[j] })
-	types := make([]ComponentType, len(comps))
+	types := make([]componentType, len(comps))
 	for i, id := range comps {
-		types[i] = ComponentType{id, w.registry.types[id]}
+		types[i] = componentType{id, w.registry.types[id]}
 	}
-	a := NewArchetype(types...)
+	a := newArchetype(types...)
 	w.archetypes = append(w.archetypes, a)
 	return len(w.archetypes) - 1
 }
@@ -188,9 +196,14 @@ func (w *World) Alive(entity Entity) bool {
 	return w.entityPool.Alive(entity)
 }
 
-// Registry returns the world's ComponentRegistry
-func (w *World) Registry() *ComponentRegistry {
-	return &w.registry
+// RegisterComponent registers a components and assigns an ID for it
+func (w *World) RegisterComponent(tp reflect.Type) ID {
+	return w.registry.RegisterComponent(tp)
+}
+
+// ComponentID returns the ID for a component type, and registers it if not already registered
+func (w *World) ComponentID(tp reflect.Type) ID {
+	return w.registry.ComponentID(tp)
 }
 
 // Query creates a query iterator for the given components
@@ -204,17 +217,17 @@ func (w *World) Query(comps ...ID) Query {
 			arches = append(arches, newArchetypeIter(arch))
 		}
 	}
-	return NewQuery(arches)
+	return newQuery(arches)
 }
 
 // RegisterComponent provides a way to register components via generics
 func RegisterComponent[T any](w *World) ID {
 	tp := reflect.TypeOf((*T)(nil)).Elem()
-	return w.Registry().RegisterComponent(tp)
+	return w.RegisterComponent(tp)
 }
 
 // ComponentID provides a way to get a component's ID via generics
 func ComponentID[T any](w *World) ID {
 	tp := reflect.TypeOf((*T)(nil)).Elem()
-	return w.Registry().ComponentID(tp)
+	return w.ComponentID(tp)
 }


Test patch:
diff --git a/benchmark/add/arche_test.go b/benchmark/add/arche_test.go
index 8ad6148..906d555 100644
--- a/benchmark/add/arche_test.go
+++ b/benchmark/add/arche_test.go
@@ -22,14 +22,14 @@ func addArcheWorld(b *testing.B, count int) {
 	}
 }
 
-func BenchmarkAddArche100(b *testing.B) {
+func BenchmarkArcheAdd_100(b *testing.B) {
 	addArcheWorld(b, 100)
 }
 
-func BenchmarkAddArche1000(b *testing.B) {
+func BenchmarkArcheAdd_1000(b *testing.B) {
 	addArcheWorld(b, 1000)
 }
 
-func BenchmarkAddArche10000(b *testing.B) {
+func BenchmarkArcheAdd_10000(b *testing.B) {
 	addArcheWorld(b, 10000)
 }
diff --git a/benchmark/add/geecs_test.go b/benchmark/add/ggecs_test.go
similarity index 81%
rename from benchmark/add/geecs_test.go
rename to benchmark/add/ggecs_test.go
index 2d8b149..fde6aa7 100644
--- a/benchmark/add/geecs_test.go
+++ b/benchmark/add/ggecs_test.go
@@ -26,14 +26,14 @@ func addGameEngineEcs(b *testing.B, count int) {
 	}
 }
 
-func BenchmarkAddGEEcs100(b *testing.B) {
+func BenchmarkGGEcsAdd_100(b *testing.B) {
 	addGameEngineEcs(b, 100)
 }
 
-func BenchmarkAddGEEcs1000(b *testing.B) {
+func BenchmarkGGEcsAdd_1000(b *testing.B) {
 	addGameEngineEcs(b, 1000)
 }
 
-func BenchmarkAddGEEcs10000(b *testing.B) {
+func BenchmarkGGEcsAdd_10000(b *testing.B) {
 	addGameEngineEcs(b, 10000)
 }
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index e158a78..17ea575 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -1,46 +1,33 @@
 package iterate
 
 import (
-	"reflect"
 	"testing"
 
 	"github.com/mlange-42/arche/ecs"
 )
 
-func runArcheArchetype(b *testing.B, count int) {
+func runArcheQuery(b *testing.B, count int) {
 	world := ecs.NewWorld()
 
-	comps := []ecs.ComponentType{
-		{ID: 0, Type: reflect.TypeOf(position{})},
-		{ID: 1, Type: reflect.TypeOf(rotation{})},
-	}
-
-	arch := ecs.NewArchetype(comps...)
+	posID := ecs.RegisterComponent[position](&world)
+	rotID := ecs.RegisterComponent[rotation](&world)
 
 	for i := 0; i < count; i++ {
-		arch.Add(
-			world.NewEntity(),
-			ecs.Component{ID: 0, Component: &position{1, 2}},
-			ecs.Component{ID: 1, Component: &rotation{3}},
-		)
+		entity := world.NewEntity()
+		world.Add(entity, posID, rotID)
 	}
 
 	for i := 0; i < b.N; i++ {
-		for j := 0; j < count; j++ {
-			pos := (*position)(arch.Get(i, ecs.ID(0)))
+		b.StopTimer()
+		query := world.Query(posID, rotID)
+		b.StartTimer()
+		for query.Next() {
+			pos := (*position)(query.Get(posID))
 			_ = pos
 		}
 	}
 }
 
-func BenchmarkIterArcheArchetype1000(b *testing.B) {
-	runArcheArchetype(b, 1000)
-}
-
-func BenchmarkIterArcheArchetype10000(b *testing.B) {
-	runArcheArchetype(b, 10000)
-}
-
 func runArcheWorld(b *testing.B, count int) {
 	world := ecs.NewWorld()
 
@@ -57,16 +44,25 @@ func runArcheWorld(b *testing.B, count int) {
 		query := world.Query(posID, rotID)
 		b.StartTimer()
 		for query.Next() {
-			pos := (*position)(query.Get(posID))
+			entity := query.Entity()
+			pos := (*position)(world.Get(entity, posID))
 			_ = pos
 		}
 	}
 }
 
-func BenchmarkIterArcheWorld1000(b *testing.B) {
+func BenchmarkArcheIterQuery_1000(b *testing.B) {
+	runArcheQuery(b, 1000)
+}
+
+func BenchmarkArcheIterQuery_10000(b *testing.B) {
+	runArcheQuery(b, 10000)
+}
+
+func BenchmarkArcheIterWorld_1000(b *testing.B) {
 	runArcheWorld(b, 1000)
 }
 
-func BenchmarkIterArcheWorld10000(b *testing.B) {
+func BenchmarkArcheIterWorld_10000(b *testing.B) {
 	runArcheWorld(b, 10000)
 }
diff --git a/benchmark/iterate/geecs_test.go b/benchmark/iterate/ggecs_test.go
similarity index 86%
rename from benchmark/iterate/geecs_test.go
rename to benchmark/iterate/ggecs_test.go
index 222d15d..fe834c3 100644
--- a/benchmark/iterate/geecs_test.go
+++ b/benchmark/iterate/ggecs_test.go
@@ -28,10 +28,10 @@ func runGameEngineEcs(b *testing.B, count int) {
 	}
 }
 
-func BenchmarkIterGEEcs1000(b *testing.B) {
+func BenchmarkGGEcsIter_1000(b *testing.B) {
 	runGameEngineEcs(b, 1000)
 }
 
-func BenchmarkIterGEEcs10000(b *testing.B) {
+func BenchmarkGGEcsIter_10000(b *testing.B) {
 	runGameEngineEcs(b, 10000)
 }
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index b61b28e..2996129 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -8,23 +8,23 @@ import (
 )
 
 func TestArchetype(t *testing.T) {
-	comps := []ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	arch := NewArchetype(comps...)
+	arch := newArchetype(comps...)
 
 	arch.Add(
 		newEntity(0),
-		Component{ID: 0, Component: &position{1, 2}},
-		Component{ID: 1, Component: &rotation{3}},
+		component{ID: 0, Component: &position{1, 2}},
+		component{ID: 1, Component: &rotation{3}},
 	)
 
 	arch.Add(
 		newEntity(1),
-		Component{ID: 0, Component: &position{4, 5}},
-		Component{ID: 1, Component: &rotation{6}},
+		component{ID: 0, Component: &position{4, 5}},
+		component{ID: 1, Component: &rotation{6}},
 	)
 
 	assert.Equal(t, 2, int(arch.entities.Len()))
@@ -62,7 +62,7 @@ func TestArchetype(t *testing.T) {
 	assert.Panics(t, func() {
 		arch.Add(
 			newEntity(1),
-			Component{ID: 0, Component: &position{4, 5}},
+			component{ID: 0, Component: &position{4, 5}},
 		)
 	})
 
@@ -72,34 +72,34 @@ func TestArchetype(t *testing.T) {
 }
 
 func TestNewArchetype(t *testing.T) {
-	comps := []ComponentType{
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
-	_ = NewArchetype(comps...)
+	_ = newArchetype(comps...)
 
-	comps = []ComponentType{
+	comps = []componentType{
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 		{ID: 0, Type: reflect.TypeOf(position{})},
 	}
 	assert.Panics(t, func() {
-		_ = NewArchetype(comps...)
+		_ = newArchetype(comps...)
 	})
 }
 
-func BenchmarkArchetypeAccess(b *testing.B) {
-	comps := []ComponentType{
+func BenchmarkArchetypeAccess_1000(b *testing.B) {
+	comps := []componentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	arch := NewArchetype(comps...)
+	arch := newArchetype(comps...)
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(
 			newEntity(i),
-			Component{ID: 0, Component: &position{1, 2}},
-			Component{ID: 1, Component: &rotation{3}},
+			component{ID: 0, Component: &position{1, 2}},
+			component{ID: 1, Component: &rotation{3}},
 		)
 	}
 
diff --git a/ecs/entity_pool_test.go b/ecs/entity_pool_test.go
index c69f530..97fe4b2 100644
--- a/ecs/entity_pool_test.go
+++ b/ecs/entity_pool_test.go
@@ -8,11 +8,11 @@ import (
 )
 
 func TestEntityPoolConstructor(t *testing.T) {
-	_ = NewEntityPool()
+	_ = newEntityPool()
 }
 
 func TestEntityPool(t *testing.T) {
-	p := newImplicitListEntityPool()
+	p := newEntityPool()
 
 	expectedAll := []Entity{newEntity(0), newEntity(1), newEntity(2), newEntity(3), newEntity(4)}
 
@@ -50,7 +50,7 @@ func TestEntityPool(t *testing.T) {
 
 func TestEntityPoolStochastic(t *testing.T) {
 	for i := 0; i < 10; i++ {
-		p := newImplicitListEntityPool()
+		p := newEntityPool()
 
 		alive := map[Entity]bool{}
 		for i := 0; i < 10; i++ {
diff --git a/ecs/registry_test.go b/ecs/registry_test.go
index c4d4011..64fdbbc 100644
--- a/ecs/registry_test.go
+++ b/ecs/registry_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestComponentRegistry(t *testing.T) {
-	reg := NewComponentRegistry()
+	reg := newComponentRegistry()
 
 	posType := reflect.TypeOf((*position)(nil)).Elem()
 	rotType := reflect.TypeOf((*rotation)(nil)).Elem()
diff --git a/ecs/storage_test.go b/ecs/storage_test.go
index 0531287..52ceb17 100644
--- a/ecs/storage_test.go
+++ b/ecs/storage_test.go
@@ -20,11 +20,11 @@ type simpleStruct struct {
 
 func TestReflectStorageAddGet(t *testing.T) {
 	obj1 := testStruct{}
-	s := NewReflectStorageFromTemplate(obj1, 1)
+	s := newStorage(reflect.TypeOf(obj1), 1)
 	storageAddGet(t, s)
 }
 
-func storageAddGet(t *testing.T, s Storage) {
+func storageAddGet(t *testing.T, s storage) {
 	obj1 := testStruct{}
 	obj2 := testStruct{1, 2, true, false}
 
@@ -52,12 +52,12 @@ func storageAddGet(t *testing.T, s Storage) {
 
 func TestReflectStorageRemove(t *testing.T) {
 	ref := simpleStruct{}
-	s := NewReflectStorageFromTemplate(ref, 32)
+	s := newStorage(reflect.TypeOf(ref), 32)
 
 	storageRemove(t, s)
 }
 
-func storageRemove(t *testing.T, s Storage) {
+func storageRemove(t *testing.T, s storage) {
 	for i := 0; i < 5; i++ {
 		obj := simpleStruct{i}
 		s.Add(&obj)
@@ -76,7 +76,7 @@ func storageRemove(t *testing.T, s Storage) {
 
 func TestReflectStorageDataSize(t *testing.T) {
 	ref := simpleStruct{}
-	s := NewReflectStorageFromTemplate(ref, 1)
+	s := newStorage(reflect.TypeOf(ref), 1)
 
 	for i := 0; i < 5; i++ {
 		obj := simpleStruct{i}
@@ -100,12 +100,12 @@ func TestReflectStorageDataSize(t *testing.T) {
 }
 
 func TestNewReflectStorage(t *testing.T) {
-	_ = NewReflectStorage(reflect.TypeOf(simpleStruct{}), 32)
+	_ = newStorage(reflect.TypeOf(simpleStruct{}), 32)
 }
 
-func BenchmarkIterReflectStorage(b *testing.B) {
+func BenchmarkIterReflectStorage_1000(b *testing.B) {
 	ref := testStruct{}
-	s := NewReflectStorageFromTemplate(ref, 128)
+	s := newStorage(reflect.TypeOf(ref), 128)
 	for i := 0; i < 1000; i++ {
 		s.Add(&testStruct{})
 	}
@@ -119,7 +119,7 @@ func BenchmarkIterReflectStorage(b *testing.B) {
 	}
 }
 
-func BenchmarkIterSlice(b *testing.B) {
+func BenchmarkIterSlice_1000(b *testing.B) {
 	s := []testStruct{}
 	for i := 0; i < 1000; i++ {
 		s = append(s, testStruct{})
@@ -134,7 +134,7 @@ func BenchmarkIterSlice(b *testing.B) {
 	}
 }
 
-func BenchmarkIterSliceInterface(b *testing.B) {
+func BenchmarkIterSliceInterface_1000(b *testing.B) {
 	s := []interface{}{}
 	for i := 0; i < 1000; i++ {
 		s = append(s, testStruct{})
@@ -149,11 +149,11 @@ func BenchmarkIterSliceInterface(b *testing.B) {
 	}
 }
 
-func BenchmarkAddReflectStorage(b *testing.B) {
+func BenchmarkAddReflectStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := NewReflectStorageFromTemplate(ref, 1024)
+		s := newStorage(reflect.TypeOf(ref), 1024)
 		b.StartTimer()
 
 		for i := 0; i < 1000; i++ {
@@ -162,7 +162,7 @@ func BenchmarkAddReflectStorage(b *testing.B) {
 	}
 }
 
-func BenchmarkAddSlice(b *testing.B) {
+func BenchmarkAddSlice_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
@@ -179,7 +179,7 @@ func BenchmarkAddSlice(b *testing.B) {
 	}
 }
 
-func BenchmarkAddSliceInterface(b *testing.B) {
+func BenchmarkAddSliceInterface_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
@@ -196,11 +196,11 @@ func BenchmarkAddSliceInterface(b *testing.B) {
 	}
 }
 
-func BenchmarkRemoveReflectStorage(b *testing.B) {
+func BenchmarkRemoveReflectStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := NewReflectStorageFromTemplate(ref, 1024)
+		s := newStorage(reflect.TypeOf(ref), 1024)
 		for i := 0; i < 1000; i++ {
 			s.Add(&ref)
 		}
@@ -212,7 +212,7 @@ func BenchmarkRemoveReflectStorage(b *testing.B) {
 	}
 }
 
-func BenchmarkRemoveSlice(b *testing.B) {
+func BenchmarkRemoveSlice_1000(b *testing.B) {
 	ref := testStruct{}
 	template := make([]testStruct, 0)
 	for i := 0; i < 1000; i++ {
@@ -236,7 +236,7 @@ func BenchmarkRemoveSlice(b *testing.B) {
 	}
 }
 
-func BenchmarkRemoveSliceInterface(b *testing.B) {
+func BenchmarkRemoveSliceInterface_1000(b *testing.B) {
 	ref := testStruct{}
 	template := make([]interface{}, 0)
 	for i := 0; i < 1000; i++ {