Repository: mlange-42/arche
Pull request #55 (2023-02-11 21:14:27)

Split package


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index d36332e..f6378c1 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -1,29 +1,31 @@
 package ecs
 
 import (
+	"internal/base"
 	"reflect"
 	"unsafe"
 )
 
 // archetype represents an ECS archetype
 type archetype struct {
-	mask bitMask
-	ids  []ID
+	Mask base.BitMask
+	Ids  []ID
 	// Indirection to avoid a fixed-size array of storages
 	// Increases access time by 50-100%
 	indices    [MaskTotalBits]uint8
-	entities   storage
-	components []storage
+	entities   base.Storage
+	components []base.Storage
 	toAdd      map[ID]*archetype
 	toRemove   map[ID]*archetype
 }
 
 var entityType = reflect.TypeOf(Entity{})
 
-func (a *archetype) init(capacityIncrement int, components ...componentType) {
-	var mask bitMask
-	a.ids = make([]ID, len(components))
-	comps := make([]storage, len(components))
+// Init initializes an archetype
+func (a *archetype) Init(capacityIncrement int, components ...base.ComponentType) {
+	var mask base.BitMask
+	a.Ids = make([]ID, len(components))
+	comps := make([]base.Storage, len(components))
 
 	prev := -1
 	for i, c := range components {
@@ -33,18 +35,18 @@ func (a *archetype) init(capacityIncrement int, components ...componentType) {
 		prev = int(c.ID)
 
 		mask.Set(c.ID, true)
-		a.ids[i] = c.ID
+		a.Ids[i] = c.ID
 		a.indices[c.ID] = uint8(i)
-		comps[i] = storage{}
-		comps[i].init(c.Type, capacityIncrement)
+		comps[i] = base.Storage{}
+		comps[i].Init(c.Type, capacityIncrement)
 	}
 
-	a.mask = mask
+	a.Mask = mask
 	a.components = comps
-	a.entities = storage{}
+	a.entities = base.Storage{}
 	a.toAdd = map[ID]*archetype{}
 	a.toRemove = map[ID]*archetype{}
-	a.entities.init(entityType, capacityIncrement)
+	a.entities.Init(entityType, capacityIncrement)
 }
 
 // GetEntity returns the entity at the given index
@@ -54,7 +56,7 @@ func (a *archetype) GetEntity(index int) Entity {
 
 // Get returns the component with the given ID at the given index
 func (a *archetype) Get(index int, id ID) unsafe.Pointer {
-	if !a.mask.Get(id) {
+	if !a.Mask.Get(id) {
 		return nil
 	}
 	return a.components[a.indices[id]].Get(uint32(index))
@@ -70,7 +72,7 @@ func (a *archetype) GetUnsafe(index int, id ID) unsafe.Pointer {
 
 // Add adds an entity with components to the archetype
 func (a *archetype) Add(entity Entity, components ...Component) uint32 {
-	if len(components) != len(a.ids) {
+	if len(components) != len(a.Ids) {
 		panic("Invalid number of components")
 	}
 	idx := a.entities.Add(&entity)
@@ -81,8 +83,8 @@ 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 {
-	if len(components) != len(a.ids) {
+func (a *archetype) AddPointer(entity Entity, components ...base.ComponentPointer) uint32 {
+	if len(components) != len(a.Ids) {
 		panic("Invalid number of components")
 	}
 	idx := a.entities.Add(&entity)
@@ -108,12 +110,12 @@ func (a *archetype) Remove(index int) bool {
 
 // Components returns the component IDs for this archetype
 func (a *archetype) Components() []ID {
-	return a.ids
+	return a.Ids
 }
 
 // HasComponent returns whether the archetype contains the given component ID
 func (a *archetype) HasComponent(id ID) bool {
-	return a.mask.Get(id)
+	return a.Mask.Get(id)
 }
 
 // Len reports the number of entities in the archetype
@@ -123,23 +125,27 @@ func (a *archetype) Len() uint32 {
 
 // Set overwrites a component with the data behind the given pointer
 func (a *archetype) Set(index uint32, id ID, comp interface{}) unsafe.Pointer {
-	return a.components[a.indices[id]].set(index, comp)
+	return a.components[a.indices[id]].Set(index, comp)
 }
 
+// GetTransitionAdd returns the archetype resulting from adding a component
 func (a *archetype) GetTransitionAdd(id ID) (*archetype, bool) {
 	p, ok := a.toAdd[id]
 	return p, ok
 }
 
+// GetTransitionRemove returns the archetype resulting from removing a component
 func (a *archetype) GetTransitionRemove(id ID) (*archetype, bool) {
 	p, ok := a.toRemove[id]
 	return p, ok
 }
 
+// SetTransitionAdd sets the archetype resulting from adding a component
 func (a *archetype) SetTransitionAdd(id ID, to *archetype) {
 	a.toAdd[id] = to
 }
 
+// SetTransitionRemove sets the archetype resulting from removing a component
 func (a *archetype) SetTransitionRemove(id ID, to *archetype) {
 	a.toRemove[id] = to
 }
diff --git a/ecs/bitmask.go b/ecs/bitmask.go
deleted file mode 100644
index 37c183f..0000000
--- a/ecs/bitmask.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package ecs
-
-import "internal/base"
-
-// bitMask is a bitmask.
-type bitMask = base.BitMask
-
-// MaskTotalBits is the size of Mask in bits.
-//
-// It is the maximum number of component types that may exist in any [World].
-const MaskTotalBits = 64
diff --git a/ecs/entity.go b/ecs/entity.go
index 889ae49..09c04a7 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -9,15 +9,22 @@ type Entity struct {
 	gen uint16
 }
 
+// newEntity creates a new Entity.
 func newEntity(id eid) Entity {
 	return Entity{id, 0}
 }
 
+// newEntityGen creates a new Entity with a given generation.
+func newEntityGen(id eid, gen uint16) Entity {
+	return Entity{id, gen}
+}
+
 // IsZero returns whether this entity is the reserved zero entity.
 func (e Entity) IsZero() bool {
 	return e.id == 0
 }
 
+// entityIndex indicates where an entity is currently stored
 type entityIndex struct {
 	arch  *archetype
 	index uint32
diff --git a/ecs/entity_pool.go b/ecs/entity_pool.go
index 021d45a..29a649e 100644
--- a/ecs/entity_pool.go
+++ b/ecs/entity_pool.go
@@ -1,6 +1,8 @@
 package ecs
 
-import "math"
+import (
+	"math"
+)
 
 // newEntityPool creates a new, initialized Entity pool
 func newEntityPool(capacityIncrement int) entityPool {
diff --git a/ecs/generic.go b/ecs/generic.go
deleted file mode 100644
index 30a3b06..0000000
--- a/ecs/generic.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package ecs
-
-import "reflect"
-
-// ComponentID returns the ID for a component type. Registers the type if it is not already registered.
-func ComponentID[T any](w *World) ID {
-	tp := reflect.TypeOf((*T)(nil)).Elem()
-	return w.componentID(tp)
-}
-
-// Map provides a type-safe way to access a component type by entity ID.
-//
-// Create one with [NewMap].
-type Map[T any] struct {
-	id    ID
-	world *World
-}
-
-// NewMap creates a new [Map] for a component type.
-//
-// Map provides a type-safe way to access a component type by entity ID.
-//
-// See also [World.Get], [World.Has] and [World.Set].
-func NewMap[T any](w *World) Map[T] {
-	return Map[T]{
-		id:    ComponentID[T](w),
-		world: w,
-	}
-}
-
-// Get gets the component for the given entity.
-//
-// See also [World.Get].
-func (g *Map[T]) Get(entity Entity) *T {
-	return (*T)(g.world.Get(entity, g.id))
-}
-
-// Has returns whether the entity has the component.
-//
-// See also [World.Has].
-func (g *Map[T]) Has(entity Entity) bool {
-	return g.world.Has(entity, g.id)
-}
-
-// Set overwrites the component for the given entity.
-//
-// Panics if the entity does not have a component of that type.
-//
-// See also [World.Set].
-func (g *Map[T]) Set(entity Entity, comp *T) *T {
-	return (*T)(g.world.Set(entity, g.id, comp))
-}
-
-// Add adds a component type to an entity.
-//
-// See also [World.Add].
-func Add[A any](w *World, entity Entity) *A {
-	id := ComponentID[A](w)
-	w.Add(entity, id)
-	return (*A)(w.Get(entity, id))
-}
-
-// Add2 adds two component types to an entity.
-//
-// See also [World.Add].
-func Add2[A any, B any](w *World, entity Entity) (*A, *B) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	w.Add(entity, idA, idB)
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
-}
-
-// Add3 adds three component types to an entity.
-//
-// See also [World.Add].
-func Add3[A any, B any, C any](w *World, entity Entity) (*A, *B, *C) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	w.Add(entity, idA, idB, idC)
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
-}
-
-// Add4 adds four component types to an entity.
-//
-// See also [World.Add].
-func Add4[A any, B any, C any, D any](w *World, entity Entity) (*A, *B, *C, *D) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	idD := ComponentID[D](w)
-	w.Add(entity, idA, idB, idC, idD)
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
-}
-
-// Add5 adds five component types to an entity.
-//
-// See also [World.Add].
-func Add5[A any, B any, C any, D any, E any](w *World, entity Entity) (*A, *B, *C, *D, *E) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	idD := ComponentID[D](w)
-	idE := ComponentID[E](w)
-	w.Add(entity, idA, idB, idC, idD, idE)
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
-}
-
-// Assign adds a components to an entity.
-//
-// See also [World.Assign] and [World.AssignN].
-func Assign[A any](w *World, entity Entity, a *A) *A {
-	idA := ComponentID[A](w)
-	w.Assign(entity, idA, a)
-	return (*A)(w.Get(entity, idA))
-}
-
-// Assign2 adds two components to an entity.
-//
-// See also [World.Assign] and [World.AssignN].
-func Assign2[A any, B any](w *World, entity Entity, a *A, b *B) (*A, *B) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	w.AssignN(entity, Component{idA, a}, Component{idB, b})
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
-}
-
-// Assign3 adds three components to an entity.
-//
-// See also [World.Assign] and [World.AssignN].
-func Assign3[A any, B any, C any](w *World, entity Entity, a *A, b *B, c *C) (*A, *B, *C) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c})
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
-}
-
-// Assign4 adds four components to an entity.
-//
-// See also [World.Assign] and [World.AssignN].
-func Assign4[A any, B any, C any, D any](w *World, entity Entity, a *A, b *B, c *C, d *D) (*A, *B, *C, *D) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	idD := ComponentID[D](w)
-	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c}, Component{idD, d})
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
-}
-
-// Assign5 adds four components to an entity.
-//
-// See also [World.Assign] and [World.AssignN].
-func Assign5[A any, B any, C any, D any, E any](w *World, entity Entity, a *A, b *B, c *C, d *D, e *E) (*A, *B, *C, *D, *E) {
-	idA := ComponentID[A](w)
-	idB := ComponentID[B](w)
-	idC := ComponentID[C](w)
-	idD := ComponentID[D](w)
-	idE := ComponentID[E](w)
-	w.AssignN(entity, Component{idA, a}, Component{idB, b}, Component{idC, c}, Component{idD, d}, Component{idE, e})
-	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
-}
-
-// Remove removes a component from an entity.
-//
-// See also [World.Remove].
-func Remove[A any](w *World, entity Entity) {
-	w.Remove(entity, ComponentID[A](w))
-}
-
-// Remove2 removes two components from an entity.
-//
-// See also [World.Remove].
-func Remove2[A any, B any](w *World, entity Entity) {
-	w.Remove(entity, ComponentID[A](w), ComponentID[B](w))
-}
-
-// Remove3 removes three components from an entity.
-//
-// See also [World.Remove].
-func Remove3[A any, B any, C any](w *World, entity Entity) {
-	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w))
-}
-
-// Remove4 removes four components from an entity.
-//
-// See also [World.Remove].
-func Remove4[A any, B any, C any, D any](w *World, entity Entity) {
-	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w))
-}
-
-// Remove5 removes five components from an entity.
-//
-// See also [World.Remove].
-func Remove5[A any, B any, C any, D any, E any](w *World, entity Entity) {
-	w.Remove(entity, ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w), ComponentID[E](w))
-}
diff --git a/ecs/query.go b/ecs/query.go
index ab1e0e8..d722aa2 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -3,6 +3,8 @@ package ecs
 import (
 	"internal/base"
 	"unsafe"
+
+	f "github.com/mlange-42/arche/filter"
 )
 
 // Query is a simple iterator to iterate entities.
@@ -19,8 +21,8 @@ import (
 //	}
 type Query struct {
 	queryIter
-	mask    bitMask
-	exclude bitMask
+	Mask    bitMask
+	Exclude bitMask
 }
 
 // newQuery creates a new Query
@@ -31,8 +33,8 @@ func newQuery(world *World, mask, exclude bitMask, lockBit uint8) Query {
 			index:   -1,
 			lockBit: lockBit,
 		},
-		mask:    mask,
-		exclude: exclude,
+		Mask:    mask,
+		Exclude: exclude,
 	}
 }
 
@@ -43,7 +45,7 @@ func newQuery(world *World, mask, exclude bitMask, lockBit uint8) Query {
 //
 //	query := world.Query(idA, isB).Not(idC, isD)
 func (q Query) Not(comps ...ID) Query {
-	q.exclude = base.NewMask(comps...)
+	q.Exclude = base.NewBitMask(comps...)
 	return q
 }
 
@@ -52,7 +54,7 @@ func (q *Query) Next() bool {
 	if q.archetype.Next() {
 		return true
 	}
-	i, a, ok := q.world.nextArchetype(q.mask, q.exclude, q.index)
+	i, a, ok := q.world.nextArchetype(q.Mask, q.Exclude, q.index)
 	q.index = i
 	if ok {
 		q.archetype = a
@@ -76,17 +78,17 @@ func (q *Query) Next() bool {
 //	        Not(OneOf(idD, idE)),
 //	    }
 //	)
-//	for query.Next() {
-//	    pos := (*position)(query.Get(posID))
+//	for filter.Next() {
+//	    pos := (*position)(filter.Get(posID))
 //	    pos.X += 1.0
 //	}
 type Filter struct {
 	queryIter
-	filter filter
+	filter f.MaskFilter
 }
 
 // newFilter creates a new Filter
-func newFilter(world *World, filter filter, lockBit uint8) Filter {
+func newFilter(world *World, filter f.MaskFilter, lockBit uint8) Filter {
 	return Filter{
 		queryIter: queryIter{
 			world:   world,
diff --git a/ecs/registry.go b/ecs/registry.go
deleted file mode 100644
index 8e8befa..0000000
--- a/ecs/registry.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ecs
-
-import "reflect"
-
-// 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{
-		components: map[reflect.Type]ID{},
-	}
-}
-
-// registerComponent registers a components and assigns an ID for it
-func (r *componentRegistry) registerComponent(tp reflect.Type, totalBits int) ID {
-	id := ID(len(r.components))
-	if int(id) >= totalBits {
-		panic("maximum of 64 component types exceeded")
-	}
-	r.components[tp] = id
-	r.types[id] = tp
-	return id
-}
-
-// ComponentID returns the ID for a component type, and registers it if not already registered
-func (r *componentRegistry) ComponentID(tp reflect.Type) ID {
-	if id, ok := r.components[tp]; ok {
-		return id
-	}
-	return r.registerComponent(tp, MaskTotalBits)
-}
-
-// ComponentType returns the type of a component by ID
-func (r *componentRegistry) ComponentType(id ID) reflect.Type {
-	return r.types[id]
-}
diff --git a/ecs/types.go b/ecs/types.go
index 24e4411..6ec5b50 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -2,8 +2,6 @@ package ecs
 
 import (
 	"internal/base"
-	"reflect"
-	"unsafe"
 )
 
 // eid is the entity identifier/index type
@@ -12,20 +10,19 @@ type eid = base.Eid
 // ID is the component identifier type
 type ID = base.ID
 
+// bitMask is a bitmask.
+type bitMask = base.BitMask
+
+// MaskTotalBits is the size of Mask in bits.
+//
+// It is the maximum number of component types that may exist in any [World].
+const MaskTotalBits = base.MaskTotalBits
+
+// Mask is a mask for a combination of components.
+type Mask = base.Mask
+
 // Component is a Component ID/Component pointer pair
 type Component struct {
 	ID
 	Component interface{}
 }
-
-// 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 {
-	ID
-	Pointer unsafe.Pointer
-}
diff --git a/ecs/world.go b/ecs/world.go
index c1de933..bd623ac 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -4,8 +4,27 @@ import (
 	"internal/base"
 	"reflect"
 	"unsafe"
+
+	f "github.com/mlange-42/arche/filter"
 )
 
+// ComponentID returns the ID for a component type. Registers the type if it is not already registered.
+func ComponentID[T any](w *World) ID {
+	tp := reflect.TypeOf((*T)(nil)).Elem()
+	return w.componentID(tp)
+}
+
+// World is the central type holding [Entity] and component data.
+type World struct {
+	config     Config
+	entities   []entityIndex
+	archetypes base.PagedArr32[archetype]
+	entityPool entityPool
+	bitPool    base.BitPool
+	registry   base.ComponentRegistry
+	locks      bitMask
+}
+
 // NewWorld creates a new [World]
 func NewWorld() World {
 	return FromConfig(NewConfig())
@@ -14,31 +33,20 @@ func NewWorld() World {
 // FromConfig creates a new [World] from a [Config]
 func FromConfig(conf Config) World {
 	arch := archetype{}
-	arch.init(conf.CapacityIncrement)
-	arches := pagedArr32[archetype]{}
+	arch.Init(conf.CapacityIncrement)
+	arches := base.PagedArr32[archetype]{}
 	arches.Add(arch)
 	return World{
 		config:     conf,
-		entities:   []entityIndex{{nil, 0}},
+		entities:   []entityIndex{{arch: nil, index: 0}},
 		entityPool: newEntityPool(conf.CapacityIncrement),
-		bitPool:    newBitPool(),
-		registry:   newComponentRegistry(),
+		bitPool:    base.NewBitPool(),
+		registry:   base.NewComponentRegistry(),
 		archetypes: arches,
 		locks:      bitMask(0),
 	}
 }
 
-// World is the central type holding [Entity] and component data.
-type World struct {
-	config     Config
-	entities   []entityIndex
-	archetypes pagedArr32[archetype]
-	entityPool entityPool
-	bitPool    bitPool
-	registry   componentRegistry
-	locks      bitMask
-}
-
 // NewEntity returns a new or recycled [Entity].
 //
 // Panics when called on a locked world.
@@ -55,9 +63,9 @@ func (w *World) NewEntity() Entity {
 			w.entities = make([]entityIndex, len, len+w.config.CapacityIncrement)
 			copy(w.entities, old)
 		}
-		w.entities = append(w.entities, entityIndex{w.archetypes.Get(0), idx})
+		w.entities = append(w.entities, entityIndex{arch: w.archetypes.Get(0), index: idx})
 	} else {
-		w.entities[entity.id] = entityIndex{w.archetypes.Get(0), idx}
+		w.entities[entity.id] = entityIndex{arch: w.archetypes.Get(0), index: idx}
 	}
 	return entity
 }
@@ -89,24 +97,18 @@ func (w *World) RemEntity(entity Entity) {
 //
 // See also the generic alternatives [Query1], [Query2], [Query3], ...
 func (w *World) Query(comps ...ID) Query {
-	mask := base.NewMask(comps...)
+	mask := base.NewBitMask(comps...)
 	lock := w.bitPool.Get()
 	w.locks.Set(ID(lock), true)
 	return newQuery(w, mask, 0, lock)
 }
 
-func (w *World) query(mask, exclude bitMask) Query {
-	lock := w.bitPool.Get()
-	w.locks.Set(ID(lock), true)
-	return newQuery(w, mask, exclude, lock)
-}
-
 // Filter creates an advanced [Filter] iterator.
 //
 // Locks the world to prevent changes to component compositions.
 //
 // There is no generic alternative for filters.
-func (w *World) Filter(filter filter) Filter {
+func (w *World) Filter(filter f.MaskFilter) Filter {
 	lock := w.bitPool.Get()
 	w.locks.Set(ID(lock), true)
 	return newFilter(w, filter, lock)
@@ -223,7 +225,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	}
 	index := w.entities[entity.id]
 	oldArch := index.arch
-	mask := oldArch.mask
+	mask := oldArch.Mask
 	for _, comp := range add {
 		if mask.Get(comp) {
 			panic("entity already has this component, can't add")
@@ -253,13 +255,13 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 
 	arch := w.findOrCreateArchetype(oldArch, addIDs, rem)
 
-	allComps := make([]componentPointer, 0, len(keepIDs)+len(addIDs))
+	allComps := make([]base.ComponentPointer, 0, len(keepIDs)+len(addIDs))
 	for _, id := range keepIDs {
 		comp := oldArch.Get(int(index.index), id)
-		allComps = append(allComps, componentPointer{id, comp})
+		allComps = append(allComps, base.ComponentPointer{ID: id, Pointer: comp})
 	}
 	for _, id := range addIDs {
-		allComps = append(allComps, componentPointer{id, nil})
+		allComps = append(allComps, base.ComponentPointer{ID: id, Pointer: nil})
 	}
 
 	newIndex := arch.AddPointer(entity, allComps...)
@@ -269,7 +271,7 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 		swapEntity := oldArch.GetEntity(int(index.index))
 		w.entities[swapEntity.id].index = index.index
 	}
-	w.entities[entity.id] = entityIndex{arch, newIndex}
+	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}
 }
 
 // IsLocked returns whether the world is locked by any queries.
@@ -288,7 +290,7 @@ func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 
 func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID) *archetype {
 	curr := start
-	mask := start.mask
+	mask := start.Mask
 	for _, id := range rem {
 		mask.Set(id, false)
 		if next, ok := curr.GetTransitionRemove(id); ok {
@@ -325,7 +327,7 @@ func (w *World) findArchetype(mask bitMask) (*archetype, bool) {
 	length := w.archetypes.Len()
 	for i := 0; i < length; i++ {
 		arch := w.archetypes.Get(i)
-		if arch.mask == mask {
+		if arch.Mask == mask {
 			return arch, true
 		}
 	}
@@ -334,19 +336,19 @@ func (w *World) findArchetype(mask bitMask) (*archetype, bool) {
 
 func (w *World) createArchetype(mask bitMask) *archetype {
 	count := int(mask.TotalBitsSet())
-	types := make([]componentType, count)
+	types := make([]base.ComponentType, count)
 
 	idx := 0
 	for i := 0; i < MaskTotalBits; i++ {
 		id := ID(i)
 		if mask.Get(id) {
-			types[idx] = componentType{id, w.registry.types[id]}
+			types[idx] = base.ComponentType{ID: id, Type: w.registry.Types[id]}
 			idx++
 		}
 	}
 	w.archetypes.Add(archetype{})
 	arch := w.archetypes.Get(w.archetypes.Len() - 1)
-	arch.init(w.config.CapacityIncrement, types...)
+	arch.Init(w.config.CapacityIncrement, types...)
 	return arch
 }
 
@@ -362,21 +364,21 @@ func (w *World) nextArchetype(mask, exclude bitMask, index int) (int, archetypeI
 	}
 	for i := index + 1; i < len; i++ {
 		a := w.archetypes.Get(i)
-		if a.Len() > 0 && a.mask.Contains(mask) && !a.mask.ContainsAny(exclude) {
+		if a.Len() > 0 && a.Mask.Contains(mask) && !a.Mask.ContainsAny(exclude) {
 			return i, newArchetypeIter(a), true
 		}
 	}
 	return len, archetypeIter{}, false
 }
 
-func (w *World) nextArchetypeFilter(filter filter, index int) (int, archetypeIter, bool) {
+func (w *World) nextArchetypeFilter(filter f.MaskFilter, index int) (int, archetypeIter, bool) {
 	len := w.archetypes.Len()
 	if index >= len {
 		panic("exceeded end of query")
 	}
 	for i := index + 1; i < len; i++ {
 		a := w.archetypes.Get(i)
-		if a.Len() > 0 && filter.Matches(Mask{a.mask}) {
+		if a.Len() > 0 && filter.Matches(a.Mask) {
 			return i, newArchetypeIter(a), true
 		}
 	}
diff --git a/ecs/filter.go b/filter/filter.go
similarity index 56%
rename from ecs/filter.go
rename to filter/filter.go
index 1fc1eee..a713cba 100644
--- a/ecs/filter.go
+++ b/filter/filter.go
@@ -1,72 +1,68 @@
-package ecs
+package filter
 
-import "internal/base"
+import (
+	"internal/base"
+)
 
 // Mask is a mask for a combination of components.
-type Mask struct {
-	mask bitMask
+type Mask = base.Mask
+
+// MaskFilter is the interface for logic filters
+type MaskFilter interface {
+	Matches(mask base.BitMask) bool
 }
 
 // All matches all the given components.
 //
 // Like [And] for combining individual components.
-func All(comps ...ID) Mask {
-	return Mask{base.NewMask(comps...)}
+func All(comps ...base.ID) Mask {
+	return base.NewMask(comps...)
 }
 
 // OneOf matches any of the two components.
 //
 // Like [Or] for combining individual components.
-func OneOf(compA ID, compB ID) Or {
-	return Or{Mask{base.NewMask(compA)}, Mask{base.NewMask(compB)}}
-}
-
-type filter interface {
-	Matches(mask Mask) bool
+func OneOf(compA base.ID, compB base.ID) Or {
+	return Or{base.NewMask(compA), base.NewMask(compB)}
 }
 
 // And is a filter for ANDing together components
 type And struct {
-	a filter
-	b filter
+	a MaskFilter
+	b MaskFilter
 }
 
 // Or is a filter for ORing together components
 type Or struct {
-	a filter
-	b filter
+	a MaskFilter
+	b MaskFilter
 }
 
 // XOr is a filter for XORing together components
 type XOr struct {
-	a filter
-	b filter
+	a MaskFilter
+	b MaskFilter
 }
 
 // Not is a filter for excluding components
 type Not Mask
 
 // Matches matches a filter against a mask
-func (f Mask) Matches(mask Mask) bool {
-	return mask.mask.Contains(f.mask)
-}
-
-// Matches matches a filter against a mask
-func (f *And) Matches(mask Mask) bool {
+func (f *And) Matches(mask base.BitMask) bool {
 	return f.a.Matches(mask) && f.b.Matches(mask)
 }
 
 // Matches matches a filter against a mask
-func (f *Or) Matches(mask Mask) bool {
+func (f *Or) Matches(mask base.BitMask) bool {
 	return f.a.Matches(mask) || f.b.Matches(mask)
 }
 
 // Matches matches a filter against a mask
-func (f *XOr) Matches(mask Mask) bool {
+func (f *XOr) Matches(mask base.BitMask) bool {
 	return f.a.Matches(mask) != f.b.Matches(mask)
 }
 
 // Matches matches a filter against a mask
-func (f Not) Matches(mask Mask) bool {
-	return !mask.mask.ContainsAny(f.mask)
+func (f Not) Matches(mask base.BitMask) bool {
+	return !mask.ContainsAny(f.BitMask)
 }
diff --git a/generic/generic.go b/generic/generic.go
new file mode 100644
index 0000000..1104baf
--- /dev/null
+++ b/generic/generic.go
@@ -0,0 +1,193 @@
+package generic
+
+import (
+	"github.com/mlange-42/arche/ecs"
+)
+
+// Map provides a type-safe way to access a component type by entity ID.
+//
+// Create one with [NewMap].
+type Map[T any] struct {
+	id    ecs.ID
+	world *ecs.World
+}
+
+// NewMap creates a new [Map] for a component type.
+//
+// Map provides a type-safe way to access a component type by entity ID.
+//
+// See also [World.Get], [World.Has] and [World.Set].
+func NewMap[T any](w *ecs.World) Map[T] {
+	return Map[T]{
+		id:    ecs.ComponentID[T](w),
+		world: w,
+	}
+}
+
+// Get gets the component for the given entity.
+//
+// See also [World.Get].
+func (g *Map[T]) Get(entity ecs.Entity) *T {
+	return (*T)(g.world.Get(entity, g.id))
+}
+
+// Has returns whether the entity has the component.
+//
+// See also [World.Has].
+func (g *Map[T]) Has(entity ecs.Entity) bool {
+	return g.world.Has(entity, g.id)
+}
+
+// Set overwrites the component for the given entity.
+//
+// Panics if the entity does not have a component of that type.
+//
+// See also [World.Set].
+func (g *Map[T]) Set(entity ecs.Entity, comp *T) *T {
+	return (*T)(g.world.Set(entity, g.id, comp))
+}
+
+// Add adds a component type to an entity.
+//
+// See also [World.Add].
+func Add[A any](w *ecs.World, entity ecs.Entity) *A {
+	id := ecs.ComponentID[A](w)
+	w.Add(entity, id)
+	return (*A)(w.Get(entity, id))
+}
+
+// Add2 adds two component types to an entity.
+//
+// See also [World.Add].
+func Add2[A any, B any](w *ecs.World, entity ecs.Entity) (*A, *B) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	w.Add(entity, idA, idB)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
+}
+
+// Add3 adds three component types to an entity.
+//
+// See also [World.Add].
+func Add3[A any, B any, C any](w *ecs.World, entity ecs.Entity) (*A, *B, *C) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	w.Add(entity, idA, idB, idC)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
+}
+
+// Add4 adds four component types to an entity.
+//
+// See also [World.Add].
+func Add4[A any, B any, C any, D any](w *ecs.World, entity ecs.Entity) (*A, *B, *C, *D) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	idD := ecs.ComponentID[D](w)
+	w.Add(entity, idA, idB, idC, idD)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
+}
+
+// Add5 adds five component types to an entity.
+//
+// See also [World.Add].
+func Add5[A any, B any, C any, D any, E any](w *ecs.World, entity ecs.Entity) (*A, *B, *C, *D, *E) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	idD := ecs.ComponentID[D](w)
+	idE := ecs.ComponentID[E](w)
+	w.Add(entity, idA, idB, idC, idD, idE)
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
+}
+
+// Assign adds a components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign[A any](w *ecs.World, entity ecs.Entity, a *A) *A {
+	idA := ecs.ComponentID[A](w)
+	w.Assign(entity, idA, a)
+	return (*A)(w.Get(entity, idA))
+}
+
+// Assign2 adds two components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign2[A any, B any](w *ecs.World, entity ecs.Entity, a *A, b *B) (*A, *B) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	w.AssignN(entity, ecs.Component{ID: idA, Component: a}, ecs.Component{ID: idB, Component: b})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB))
+}
+
+// Assign3 adds three components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign3[A any, B any, C any](w *ecs.World, entity ecs.Entity, a *A, b *B, c *C) (*A, *B, *C) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	w.AssignN(entity, ecs.Component{ID: idA, Component: a}, ecs.Component{ID: idB, Component: b}, ecs.Component{ID: idC, Component: c})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC))
+}
+
+// Assign4 adds four components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign4[A any, B any, C any, D any](w *ecs.World, entity ecs.Entity, a *A, b *B, c *C, d *D) (*A, *B, *C, *D) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	idD := ecs.ComponentID[D](w)
+	w.AssignN(entity, ecs.Component{ID: idA, Component: a}, ecs.Component{ID: idB, Component: b}, ecs.Component{ID: idC, Component: c}, ecs.Component{ID: idD, Component: d})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD))
+}
+
+// Assign5 adds four components to an entity.
+//
+// See also [World.Assign] and [World.AssignN].
+func Assign5[A any, B any, C any, D any, E any](w *ecs.World, entity ecs.Entity, a *A, b *B, c *C, d *D, e *E) (*A, *B, *C, *D, *E) {
+	idA := ecs.ComponentID[A](w)
+	idB := ecs.ComponentID[B](w)
+	idC := ecs.ComponentID[C](w)
+	idD := ecs.ComponentID[D](w)
+	idE := ecs.ComponentID[E](w)
+	w.AssignN(entity, ecs.Component{ID: idA, Component: a}, ecs.Component{ID: idB, Component: b}, ecs.Component{ID: idC, Component: c}, ecs.Component{ID: idD, Component: d}, ecs.Component{ID: idE, Component: e})
+	return (*A)(w.Get(entity, idA)), (*B)(w.Get(entity, idB)), (*C)(w.Get(entity, idC)), (*D)(w.Get(entity, idD)), (*E)(w.Get(entity, idE))
+}
+
+// Remove removes a component from an entity.
+//
+// See also [World.Remove].
+func Remove[A any](w *ecs.World, entity ecs.Entity) {
+	w.Remove(entity, ecs.ComponentID[A](w))
+}
+
+// Remove2 removes two components from an entity.
+//
+// See also [World.Remove].
+func Remove2[A any, B any](w *ecs.World, entity ecs.Entity) {
+	w.Remove(entity, ecs.ComponentID[A](w), ecs.ComponentID[B](w))
+}
+
+// Remove3 removes three components from an entity.
+//
+// See also [World.Remove].
+func Remove3[A any, B any, C any](w *ecs.World, entity ecs.Entity) {
+	w.Remove(entity, ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w))
+}
+
+// Remove4 removes four components from an entity.
+//
+// See also [World.Remove].
+func Remove4[A any, B any, C any, D any](w *ecs.World, entity ecs.Entity) {
+	w.Remove(entity, ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w))
+}
+
+// Remove5 removes five components from an entity.
+//
+// See also [World.Remove].
+func Remove5[A any, B any, C any, D any, E any](w *ecs.World, entity ecs.Entity) {
+	w.Remove(entity, ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w), ecs.ComponentID[E](w))
+}
diff --git a/ecs/query_generic.go b/generic/query_generic.go
similarity index 75%
rename from ecs/query_generic.go
rename to generic/query_generic.go
index 5e28c2e..fbc5ea6 100644
--- a/ecs/query_generic.go
+++ b/generic/query_generic.go
@@ -1,28 +1,32 @@
-package ecs
+package generic
 
-import "internal/base"
+import (
+	"internal/base"
+
+	"github.com/mlange-42/arche/ecs"
+)
 
 // Q0 is a generic query for no components.
 //
 // Create one with [Query0]
 type Q0 struct {
-	Query
+	ecs.Query
 }
 
 // Query0 creates a generic query for no components.
 //
 // See also [World.Query].
-func Query0(w *World) Q0 {
+func Query0(w *ecs.World) Q0 {
 	return Q0{
-		Query: w.query(0, 0),
+		Query: w.Query(),
 	}
 }
 
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q0) Not(mask Mask) Q0 {
-	q.exclude = q.exclude | mask.mask
+func (q Q0) Not(mask ecs.Mask) Q0 {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -30,15 +34,15 @@ func (q Q0) Not(mask Mask) Q0 {
 //
 // Create one with [Query1]
 type Q1[A any] struct {
-	Query
-	id ID
+	ecs.Query
+	id ecs.ID
 }
 
 // Query1 creates a generic query for one component.
 //
 // See also [World.Query].
-func Query1[A any](w *World) Q1[A] {
-	id := ComponentID[A](w)
+func Query1[A any](w *ecs.World) Q1[A] {
+	id := ecs.ComponentID[A](w)
 	return Q1[A]{
 		Query: w.Query(id),
 		id:    id,
@@ -48,8 +52,8 @@ func Query1[A any](w *World) Q1[A] {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q1[A]) Not(mask Mask) Q1[A] {
-	q.exclude = q.exclude | mask.mask
+func (q Q1[A]) Not(mask ecs.Mask) Q1[A] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -64,15 +68,15 @@ func (q *Q1[A]) Get1() *A {
 //
 // Create one with [Query2]
 type Q2[A any, B any] struct {
-	Query
-	ids [2]ID
+	ecs.Query
+	ids [2]ecs.ID
 }
 
 // Query2 creates a generic query for two components.
 //
 // See also [World.Query].
-func Query2[A any, B any](w *World) Q2[A, B] {
-	ids := [2]ID{ComponentID[A](w), ComponentID[B](w)}
+func Query2[A any, B any](w *ecs.World) Q2[A, B] {
+	ids := [2]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w)}
 	return Q2[A, B]{
 		Query: w.Query(ids[:]...),
 		ids:   ids,
@@ -82,8 +86,8 @@ func Query2[A any, B any](w *World) Q2[A, B] {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q2[A, B]) Not(mask Mask) Q2[A, B] {
-	q.exclude = q.exclude | mask.mask
+func (q Q2[A, B]) Not(mask ecs.Mask) Q2[A, B] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -108,15 +112,15 @@ func (q *Q2[A, B]) Get2() *B {
 //
 // Create one with [Query3]
 type Q3[A any, B any, C any] struct {
-	Query
-	ids [3]ID
+	ecs.Query
+	ids [3]ecs.ID
 }
 
 // Query3 creates a generic query for three components.
 //
 // See also [World.Query].
-func Query3[A any, B any, C any](w *World) Q3[A, B, C] {
-	ids := [3]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w)}
+func Query3[A any, B any, C any](w *ecs.World) Q3[A, B, C] {
+	ids := [3]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w)}
 	return Q3[A, B, C]{
 		Query: w.Query(ids[:]...),
 		ids:   ids,
@@ -126,8 +130,8 @@ func Query3[A any, B any, C any](w *World) Q3[A, B, C] {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q3[A, B, C]) Not(mask Mask) Q3[A, B, C] {
-	q.exclude = q.exclude | mask.mask
+func (q Q3[A, B, C]) Not(mask ecs.Mask) Q3[A, B, C] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -157,15 +161,15 @@ func (q *Q3[A, B, C]) Get3() *C {
 //
 // Create one with [Query4]
 type Q4[A any, B any, C any, D any] struct {
-	Query
-	ids [4]ID
+	ecs.Query
+	ids [4]ecs.ID
 }
 
 // Query4 creates a generic query for four components.
 //
 // See also [World.Query].
-func Query4[A any, B any, C any, D any](w *World) Q4[A, B, C, D] {
-	ids := [4]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w)}
+func Query4[A any, B any, C any, D any](w *ecs.World) Q4[A, B, C, D] {
+	ids := [4]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w)}
 	return Q4[A, B, C, D]{
 		Query: w.Query(ids[:]...),
 		ids:   ids,
@@ -175,8 +179,8 @@ func Query4[A any, B any, C any, D any](w *World) Q4[A, B, C, D] {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q4[A, B, C, D]) Not(mask Mask) Q4[A, B, C, D] {
-	q.exclude = q.exclude | mask.mask
+func (q Q4[A, B, C, D]) Not(mask ecs.Mask) Q4[A, B, C, D] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -214,15 +218,15 @@ func (q *Q4[A, B, C, D]) Get4() *D {
 //
 // Create one with [Query5]
 type Q5[A any, B any, C any, D any, E any] struct {
-	Query
-	ids [5]ID
+	ecs.Query
+	ids [5]ecs.ID
 }
 
 // Query5 creates a generic query for five components.
 //
 // See also [World.Query].
-func Query5[A any, B any, C any, D any, E any](w *World) Q5[A, B, C, D, E] {
-	ids := [5]ID{ComponentID[A](w), ComponentID[B](w), ComponentID[C](w), ComponentID[D](w), ComponentID[E](w)}
+func Query5[A any, B any, C any, D any, E any](w *ecs.World) Q5[A, B, C, D, E] {
+	ids := [5]ecs.ID{ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w), ecs.ComponentID[D](w), ecs.ComponentID[E](w)}
 	return Q5[A, B, C, D, E]{
 		Query: w.Query(ids[:]...),
 		ids:   ids,
@@ -232,8 +236,8 @@ func Query5[A any, B any, C any, D any, E any](w *World) Q5[A, B, C, D, E] {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q5[A, B, C, D, E]) Not(mask Mask) Q5[A, B, C, D, E] {
-	q.exclude = q.exclude | mask.mask
+func (q Q5[A, B, C, D, E]) Not(mask ecs.Mask) Q5[A, B, C, D, E] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -277,17 +281,17 @@ func (q *Q5[A, B, C, D, E]) Get5() *E {
 //
 // Create one with [Query6]
 type Q6[A any, B any, C any, D any, E any, F any] struct {
-	Query
-	ids [6]ID
+	ecs.Query
+	ids [6]ecs.ID
 }
 
 // Query6 creates a generic query for six components.
 //
 // See also [World.Query].
-func Query6[A any, B any, C any, D any, E any, F any](w *World) Q6[A, B, C, D, E, F] {
-	ids := [6]ID{
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
+func Query6[A any, B any, C any, D any, E any, F any](w *ecs.World) Q6[A, B, C, D, E, F] {
+	ids := [6]ecs.ID{
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
 	}
 	return Q6[A, B, C, D, E, F]{
 		Query: w.Query(ids[:]...),
@@ -298,8 +302,8 @@ func Query6[A any, B any, C any, D any, E any, F any](w *World) Q6[A, B, C, D, E
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q6[A, B, C, D, E, F]) Not(mask Mask) Q6[A, B, C, D, E, F] {
-	q.exclude = q.exclude | mask.mask
+func (q Q6[A, B, C, D, E, F]) Not(mask ecs.Mask) Q6[A, B, C, D, E, F] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -349,18 +353,18 @@ func (q *Q6[A, B, C, D, E, F]) Get6() *F {
 //
 // Create one with [Query7]
 type Q7[A any, B any, C any, D any, E any, F any, G any] struct {
-	Query
-	ids [7]ID
+	ecs.Query
+	ids [7]ecs.ID
 }
 
 // Query7 creates a generic query for seven components.
 //
 // See also [World.Query].
-func Query7[A any, B any, C any, D any, E any, F any, G any](w *World) Q7[A, B, C, D, E, F, G] {
-	ids := [7]ID{
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
-		ComponentID[G](w),
+func Query7[A any, B any, C any, D any, E any, F any, G any](w *ecs.World) Q7[A, B, C, D, E, F, G] {
+	ids := [7]ecs.ID{
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
+		ecs.ComponentID[G](w),
 	}
 	return Q7[A, B, C, D, E, F, G]{
 		Query: w.Query(ids[:]...),
@@ -382,8 +386,8 @@ func (q *Q7[A, B, C, D, E, F, G]) GetAll() (*A, *B, *C, *D, *E, *F, *G) {
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q7[A, B, C, D, E, F, G]) Not(mask Mask) Q7[A, B, C, D, E, F, G] {
-	q.exclude = q.exclude | mask.mask
+func (q Q7[A, B, C, D, E, F, G]) Not(mask ecs.Mask) Q7[A, B, C, D, E, F, G] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -428,18 +432,18 @@ func (q *Q7[A, B, C, D, E, F, G]) Get7() *G {
 //
 // Create one with [Query8]
 type Q8[A any, B any, C any, D any, E any, F any, G any, H any] struct {
-	Query
-	ids [8]ID
+	ecs.Query
+	ids [8]ecs.ID
 }
 
 // Query8 creates a generic query for seven components.
 //
 // See also [World.Query].
-func Query8[A any, B any, C any, D any, E any, F any, G any, H any](w *World) Q8[A, B, C, D, E, F, G, H] {
-	ids := [8]ID{
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
-		ComponentID[G](w), ComponentID[H](w),
+func Query8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.World) Q8[A, B, C, D, E, F, G, H] {
+	ids := [8]ecs.ID{
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
+		ecs.ComponentID[G](w), ecs.ComponentID[H](w),
 	}
 	return Q8[A, B, C, D, E, F, G, H]{
 		Query: w.Query(ids[:]...),
@@ -450,8 +454,8 @@ func Query8[A any, B any, C any, D any, E any, F any, G any, H any](w *World) Q8
 // Not excludes entities with the given components from the query.
 //
 // Create the required mask with [Mask1], [Mask2], etc.
-func (q Q8[A, B, C, D, E, F, G, H]) Not(mask Mask) Q8[A, B, C, D, E, F, G, H] {
-	q.exclude = q.exclude | mask.mask
+func (q Q8[A, B, C, D, E, F, G, H]) Not(mask ecs.Mask) Q8[A, B, C, D, E, F, G, H] {
+	q.Exclude = q.Exclude | mask.BitMask
 	return q
 }
 
@@ -508,62 +512,62 @@ func (q *Q8[A, B, C, D, E, F, G, H]) Get8() *H {
 }
 
 // Mask1 creates a component [Mask] for one component type.
-func Mask1[A any](w *World) Mask {
-	return Mask{base.NewMask(ComponentID[A](w))}
+func Mask1[A any](w *ecs.World) ecs.Mask {
+	return base.NewMask(ecs.ComponentID[A](w))
 }
 
 // Mask2 creates a component [Mask] for two component types.
-func Mask2[A any, B any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w),
-	)}
+func Mask2[A any, B any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w),
+	)
 }
 
 // Mask3 creates a component [Mask] for three component types.
-func Mask3[A any, B any, C any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-	)}
+func Mask3[A any, B any, C any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+	)
 }
 
 // Mask4 creates a component [Mask] for four component types.
-func Mask4[A any, B any, C any, D any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w),
-	)}
+func Mask4[A any, B any, C any, D any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w),
+	)
 }
 
 // Mask5 creates a component [Mask] for five component types.
-func Mask5[A any, B any, C any, D any, E any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w),
-	)}
+func Mask5[A any, B any, C any, D any, E any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w),
+	)
 }
 
 // Mask6 creates a component [Mask] for six component types.
-func Mask6[A any, B any, C any, D any, E any, F any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
-	)}
+func Mask6[A any, B any, C any, D any, E any, F any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
+	)
 }
 
 // Mask7 creates a component [Mask] for seven component types.
-func Mask7[A any, B any, C any, D any, E any, F any, G any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
-		ComponentID[G](w),
-	)}
+func Mask7[A any, B any, C any, D any, E any, F any, G any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
+		ecs.ComponentID[G](w),
+	)
 }
 
 // Mask8 creates a component [Mask] for eight component types.
-func Mask8[A any, B any, C any, D any, E any, F any, G any, H any](w *World) Mask {
-	return Mask{base.NewMask(
-		ComponentID[A](w), ComponentID[B](w), ComponentID[C](w),
-		ComponentID[D](w), ComponentID[E](w), ComponentID[F](w),
-		ComponentID[G](w), ComponentID[H](w),
-	)}
+func Mask8[A any, B any, C any, D any, E any, F any, G any, H any](w *ecs.World) ecs.Mask {
+	return base.NewMask(
+		ecs.ComponentID[A](w), ecs.ComponentID[B](w), ecs.ComponentID[C](w),
+		ecs.ComponentID[D](w), ecs.ComponentID[E](w), ecs.ComponentID[F](w),
+		ecs.ComponentID[G](w), ecs.ComponentID[H](w),
+	)
 }
diff --git a/go.work b/go.work
index 73bdcd7..9766735 100644
--- a/go.work
+++ b/go.work
@@ -4,4 +4,5 @@ use (
 	.
 	./benchmark
 	./profile
+	./internal/base
 )
diff --git a/ecs/bit_pool.go b/internal/base/bit_pool.go
similarity index 71%
rename from ecs/bit_pool.go
rename to internal/base/bit_pool.go
index 5152ace..3314075 100644
--- a/ecs/bit_pool.go
+++ b/internal/base/bit_pool.go
@@ -1,26 +1,26 @@
-package ecs
+package base
 
-// newEntityPool creates a new, initialized bit pool
-func newBitPool() bitPool {
-	return bitPool{
-		bits:      [MaskTotalBits]uint8{},
-		next:      0,
-		length:    0,
-		available: 0,
-	}
-}
-
-// bitPool is an entityPool implementation using implicit linked lists.
+// BitPool is an entityPool implementation using implicit linked lists.
 // Implements https://skypjack.github.io/2019-05-06-ecs-baf-part-3/
-type bitPool struct {
+type BitPool struct {
 	bits      [MaskTotalBits]uint8
 	next      uint8
 	length    uint8
 	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 {
+func (p *BitPool) Get() uint8 {
 	if p.available == 0 {
 		if p.length >= MaskTotalBits {
 			panic("run out of the maximum of 64 bits")
@@ -37,7 +37,7 @@ func (p *bitPool) Get() uint8 {
 }
 
 // Recycle hands a bit back for recycling
-func (p *bitPool) Recycle(b uint8) {
+func (p *BitPool) Recycle(b uint8) {
 	p.next, p.bits[b] = b, p.next
 	p.available++
 }
diff --git a/internal/base/bitmask.go b/internal/base/bitmask.go
index 5d4301a..8e60720 100644
--- a/internal/base/bitmask.go
+++ b/internal/base/bitmask.go
@@ -1,16 +1,21 @@
 package base
 
+// MaskTotalBits is the size of Mask in bits.
+//
+// It is the maximum number of component types that may exist in any [World].
+const MaskTotalBits = 64
+
 // BitMask is a bitmask.
 type BitMask uint64
 
 var nibbleToBitsSet = [16]uint{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}
 
-// NewMask creates a new bitmask from a list of IDs.
+// NewBitMask creates a new bitmask from a list of IDs.
 //
 // If any ID is bigger or equal [MaskTotalBits], it'll not be added to the mask.
 //
 // Implementation taken from https://github.com/marioolofo/go-gameengine-ecs.
-func NewMask(ids ...ID) BitMask {
+func NewBitMask(ids ...ID) BitMask {
 	var mask BitMask
 	for _, id := range ids {
 		mask.Set(id, true)
diff --git a/ecs/paged.go b/internal/base/paged.go
similarity index 61%
rename from ecs/paged.go
rename to internal/base/paged.go
index 648fbb6..7c0a0c5 100644
--- a/ecs/paged.go
+++ b/internal/base/paged.go
@@ -1,15 +1,15 @@
-package ecs
+package base
 
 const fixedPageSize = 32
 
-// pagedArr32 is a paged collection working with pages of length 32 arrays
-type pagedArr32[T any] struct {
+// PagedArr32 is a paged collection working with pages of length 32 arrays
+type PagedArr32[T any] struct {
 	pages   [][fixedPageSize]T
 	len     int
 	lenLast int
 }
 
-func (p *pagedArr32[T]) Add(value T) {
+func (p *PagedArr32[T]) Add(value T) {
 	if len(p.pages) == 0 || p.lenLast == fixedPageSize {
 		p.pages = append(p.pages, [fixedPageSize]T{})
 		p.lenLast = 0
@@ -19,10 +19,10 @@ func (p *pagedArr32[T]) Add(value T) {
 	p.lenLast++
 }
 
-func (p *pagedArr32[T]) Get(index int) *T {
+func (p *PagedArr32[T]) Get(index int) *T {
 	return &p.pages[index/fixedPageSize][index%fixedPageSize]
 }
 
-func (p *pagedArr32[T]) Len() int {
+func (p *PagedArr32[T]) Len() int {
 	return p.len
 }
diff --git a/internal/base/registry.go b/internal/base/registry.go
new file mode 100644
index 0000000..b270027
--- /dev/null
+++ b/internal/base/registry.go
@@ -0,0 +1,40 @@
+package base
+
+import "reflect"
+
+// 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{
+		Components: map[reflect.Type]ID{},
+	}
+}
+
+// RegisterComponent registers a components and assigns an ID for it
+func (r *ComponentRegistry) RegisterComponent(tp reflect.Type, totalBits int) ID {
+	id := ID(len(r.Components))
+	if int(id) >= totalBits {
+		panic("maximum of 64 component types exceeded")
+	}
+	r.Components[tp] = id
+	r.Types[id] = tp
+	return id
+}
+
+// ComponentID returns the ID for a component type, and registers it if not already registered
+func (r *ComponentRegistry) ComponentID(tp reflect.Type) ID {
+	if id, ok := r.Components[tp]; ok {
+		return id
+	}
+	return r.RegisterComponent(tp, MaskTotalBits)
+}
+
+// ComponentType returns the type of a component by ID
+func (r *ComponentRegistry) ComponentType(id ID) reflect.Type {
+	return r.Types[id]
+}
diff --git a/ecs/storage.go b/internal/base/storage.go
similarity index 77%
rename from ecs/storage.go
rename to internal/base/storage.go
index c1995fe..78dcfd3 100644
--- a/ecs/storage.go
+++ b/internal/base/storage.go
@@ -1,4 +1,4 @@
-package ecs
+package base
 
 import (
 	"math"
@@ -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,8 +17,8 @@ type storage struct {
 	capacityIncrement uint32
 }
 
-// newStorage creates a new ReflectStorage
-func (s *storage) init(tp reflect.Type, increment int) {
+// Init initializes a storage
+func (s *Storage) Init(tp reflect.Type, increment int) {
 	size := tp.Size()
 	align := uintptr(tp.Align())
 	size = (size + (align - 1)) / align * align
@@ -33,21 +33,21 @@ func (s *storage) init(tp reflect.Type, increment int) {
 }
 
 // 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)
+	s.Set(s.len-1, value)
 	return s.len - 1
 }
 
 // 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)
@@ -55,14 +55,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
@@ -73,7 +73,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
 
@@ -97,7 +97,8 @@ func (s *storage) Remove(index uint32) bool {
 	return false
 }
 
-func (s *storage) set(index uint32, value interface{}) unsafe.Pointer {
+// Set sets the storage at the given index
+func (s *Storage) Set(index uint32, value interface{}) unsafe.Pointer {
 	rValue := reflect.ValueOf(value)
 	dst := s.Get(index)
 	var src unsafe.Pointer
@@ -112,7 +113,7 @@ func (s *storage) set(index uint32, value interface{}) unsafe.Pointer {
 	return dst
 }
 
-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
 
@@ -123,7 +124,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++ {
@@ -133,12 +134,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/internal/base/types.go b/internal/base/types.go
index 1bae03c..6a0164f 100644
--- a/internal/base/types.go
+++ b/internal/base/types.go
@@ -1,7 +1,45 @@
 package base
 
+import (
+	"reflect"
+	"unsafe"
+)
+
 // Eid is the entity identifier/index type
 type Eid uint32
 
 // ID is the component identifier type
 type ID uint8
+
+// Mask is a mask for a combination of components.
+type Mask struct {
+	BitMask
+}
+
+// NewMask creates a new Mask from a list of IDs.
+//
+// If any ID is bigger or equal [MaskTotalBits], it'll not be added to the mask.
+func NewMask(ids ...ID) Mask {
+	var mask BitMask
+	for _, id := range ids {
+		mask.Set(id, true)
+	}
+	return Mask{mask}
+}
+
+// Matches matches a filter against a mask
+func (f Mask) Matches(mask BitMask) bool {
+	return mask.Contains(f.BitMask)
+}
+
+// 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 {
+	ID
+	Pointer unsafe.Pointer
+}


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1089f0c..040b2c9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -35,7 +35,7 @@ jobs:
         run: go get .
       - name: Test
         run: |
-          go test -v ./... -covermode=count -coverprofile="coverage.out"
+          go test -v ./internal/base ./... -covermode=count -coverprofile="coverage.out"
           go tool cover -func="coverage.out"
 
   lint:
@@ -57,6 +57,6 @@ jobs:
             exit 1
           fi
       - name: Lint with vet
-        run: go vet ./...
+        run: go vet ./internal/base ./...
       - name: Lint with staticcheck
-        run: staticcheck ./...
+        run: staticcheck ./internal/base ./...
diff --git a/benchmark/add/arche_test.go b/benchmark/add/arche_test.go
index 52518bd..012a84b 100644
--- a/benchmark/add/arche_test.go
+++ b/benchmark/add/arche_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
 )
 
 func addArcheWorld(b *testing.B, count int) {
@@ -30,7 +31,7 @@ func addArcheGeneric(b *testing.B, count int) {
 
 		for i := 0; i < count; i++ {
 			entity := world.NewEntity()
-			ecs.Add[position](&world, entity)
+			generic.Add[position](&world, entity)
 		}
 	}
 }
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index 3c8851f..abd3bba 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -4,6 +4,8 @@ import (
 	"testing"
 
 	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/filter"
+	"github.com/mlange-42/arche/generic"
 )
 
 func runArcheQuery(b *testing.B, count int) {
@@ -45,7 +47,7 @@ func runArcheQueryFilter(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := world.Filter(ecs.All(posID, rotID))
+		query := world.Filter(filter.All(posID, rotID))
 		b.StartTimer()
 		for query.Next() {
 			pos := (*position)(query.Get(posID))
@@ -69,7 +71,7 @@ func runArcheQueryGeneric(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := ecs.Query2[position, rotation](&world)
+		query := generic.Query2[position, rotation](&world)
 		b.StartTimer()
 		for query.Next() {
 			pos := query.Get1()
@@ -126,7 +128,7 @@ func runArcheQueryGeneric5C(b *testing.B, count int) {
 
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		query := ecs.Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&world)
+		query := generic.Query5[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4](&world)
 		b.StartTimer()
 		for query.Next() {
 			t1, t2, t3, t4, t5 := query.GetAll()
@@ -203,7 +205,7 @@ func runArcheWorldGeneric(b *testing.B, count int) {
 	posID := ecs.ComponentID[position](&world)
 	rotID := ecs.ComponentID[rotation](&world)
 
-	get := ecs.NewMap[position](&world)
+	get := generic.NewMap[position](&world)
 
 	for i := 0; i < count; i++ {
 		entity := world.NewEntity()
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index eadd92f..c486d61 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -1,6 +1,7 @@
 package ecs
 
 import (
+	"internal/base"
 	"reflect"
 	"testing"
 
@@ -8,13 +9,13 @@ import (
 )
 
 func TestArchetype(t *testing.T) {
-	comps := []componentType{
+	comps := []base.ComponentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
 	arch := archetype{}
-	arch.init(32, comps...)
+	arch.Init(32, comps...)
 
 	arch.Add(
 		newEntity(0),
@@ -73,36 +74,36 @@ func TestArchetype(t *testing.T) {
 }
 
 func TestNewArchetype(t *testing.T) {
-	comps := []componentType{
+	comps := []base.ComponentType{
 		{ID: 0, Type: reflect.TypeOf(position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 	arch := archetype{}
-	arch.init(32, comps...)
+	arch.Init(32, comps...)
 
-	comps = []componentType{
+	comps = []base.ComponentType{
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 		{ID: 0, Type: reflect.TypeOf(position{})},
 	}
 	assert.Panics(t, func() {
 		arch := archetype{}
-		arch.init(32, comps...)
+		arch.Init(32, comps...)
 	})
 }
 
 func BenchmarkArchetypeAccess_1000(b *testing.B) {
 	b.StopTimer()
-	comps := []componentType{
-		{ID: 0, Type: reflect.TypeOf(testStruct{})},
+	comps := []base.ComponentType{
+		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
 	arch := archetype{}
-	arch.init(32, comps...)
+	arch.Init(32, comps...)
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(
 			newEntity(eid(i)),
-			Component{ID: 0, Component: &testStruct{}},
+			Component{ID: 0, Component: &testStruct0{}},
 		)
 	}
 	b.StartTimer()
@@ -111,7 +112,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 		len := int(arch.Len())
 		id := ID(0)
 		for j := 0; j < len; j++ {
-			pos := (*testStruct)(arch.Get(i, id))
+			pos := (*testStruct0)(arch.Get(i, id))
 			_ = pos
 		}
 	}
@@ -119,17 +120,17 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 
 func BenchmarkArchetypeAccessUnsafe_1000(b *testing.B) {
 	b.StopTimer()
-	comps := []componentType{
-		{ID: 0, Type: reflect.TypeOf(testStruct{})},
+	comps := []base.ComponentType{
+		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
 	arch := archetype{}
-	arch.init(32, comps...)
+	arch.Init(32, comps...)
 
 	for i := 0; i < 1000; i++ {
 		arch.Add(
 			newEntity(eid(i)),
-			Component{ID: 0, Component: &testStruct{}},
+			Component{ID: 0, Component: &testStruct0{}},
 		)
 	}
 	b.StartTimer()
@@ -138,7 +139,7 @@ func BenchmarkArchetypeAccessUnsafe_1000(b *testing.B) {
 		len := int(arch.Len())
 		id := ID(0)
 		for j := 0; j < len; j++ {
-			pos := (*testStruct)(arch.GetUnsafe(i, id))
+			pos := (*testStruct0)(arch.GetUnsafe(i, id))
 			_ = pos
 		}
 	}
diff --git a/ecs/test_types.go b/ecs/test_types.go
index 89231b1..ee26a7d 100644
--- a/ecs/test_types.go
+++ b/ecs/test_types.go
@@ -26,30 +26,3 @@ type testStruct6 struct{ val int32 }
 type testStruct7 struct{ val int32 }
 type testStruct8 struct{ val int32 }
 type testStruct9 struct{ val int32 }
-
-func registerAll(w *World) []ID {
-	_ = testStruct0{1}
-	_ = testStruct1{1}
-	_ = testStruct2{1}
-	_ = testStruct3{1}
-	_ = testStruct4{1}
-	_ = testStruct5{1}
-	_ = testStruct6{1}
-	_ = testStruct7{1}
-	_ = testStruct8{1}
-	_ = testStruct9{1}
-
-	ids := make([]ID, 10)
-	ids[0] = ComponentID[testStruct0](w)
-	ids[1] = ComponentID[testStruct1](w)
-	ids[2] = ComponentID[testStruct2](w)
-	ids[3] = ComponentID[testStruct3](w)
-	ids[4] = ComponentID[testStruct4](w)
-	ids[5] = ComponentID[testStruct5](w)
-	ids[6] = ComponentID[testStruct6](w)
-	ids[7] = ComponentID[testStruct7](w)
-	ids[8] = ComponentID[testStruct8](w)
-	ids[9] = ComponentID[testStruct9](w)
-
-	return ids
-}
diff --git a/ecs/bitmask_test.go b/ecs/types_test.go
similarity index 86%
rename from ecs/bitmask_test.go
rename to ecs/types_test.go
index 5fc375e..3509028 100644
--- a/ecs/bitmask_test.go
+++ b/ecs/types_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestBitMask(t *testing.T) {
-	mask := base.NewMask(ID(1), ID(2), ID(13), ID(27))
+	mask := base.NewBitMask(ID(1), ID(2), ID(13), ID(27))
 
 	assert.Equal(t, uint(4), mask.TotalBitsSet())
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 9ed1ea4..68f3dae 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -12,33 +12,33 @@ import (
 func TestWorldEntites(t *testing.T) {
 	w := NewWorld()
 
-	assert.Equal(t, Entity{1, 0}, w.NewEntity())
-	assert.Equal(t, Entity{2, 0}, w.NewEntity())
-	assert.Equal(t, Entity{3, 0}, w.NewEntity())
+	assert.Equal(t, newEntityGen(1, 0), w.NewEntity())
+	assert.Equal(t, newEntityGen(2, 0), w.NewEntity())
+	assert.Equal(t, newEntityGen(3, 0), w.NewEntity())
 
 	assert.Equal(t, 0, int(w.entities[0].index))
 	assert.Equal(t, 0, int(w.entities[1].index))
 	assert.Equal(t, 1, int(w.entities[2].index))
 	assert.Equal(t, 2, int(w.entities[3].index))
-	w.RemEntity(Entity{2, 0})
-	assert.False(t, w.Alive(Entity{2, 0}))
+	w.RemEntity(newEntityGen(2, 0))
+	assert.False(t, w.Alive(newEntityGen(2, 0)))
 
 	assert.Equal(t, 0, int(w.entities[1].index))
 	assert.Equal(t, 1, int(w.entities[3].index))
 
-	assert.Equal(t, Entity{2, 1}, w.NewEntity())
-	assert.False(t, w.Alive(Entity{2, 0}))
-	assert.True(t, w.Alive(Entity{2, 1}))
+	assert.Equal(t, newEntityGen(2, 1), w.NewEntity())
+	assert.False(t, w.Alive(newEntityGen(2, 0)))
+	assert.True(t, w.Alive(newEntityGen(2, 1)))
 
 	assert.Equal(t, 2, int(w.entities[2].index))
 
-	w.RemEntity(Entity{3, 0})
-	w.RemEntity(Entity{2, 1})
-	w.RemEntity(Entity{1, 0})
+	w.RemEntity(newEntityGen(3, 0))
+	w.RemEntity(newEntityGen(2, 1))
+	w.RemEntity(newEntityGen(1, 0))
 
-	assert.Panics(t, func() { w.RemEntity(Entity{3, 0}) })
-	assert.Panics(t, func() { w.RemEntity(Entity{2, 1}) })
-	assert.Panics(t, func() { w.RemEntity(Entity{1, 0}) })
+	assert.Panics(t, func() { w.RemEntity(newEntityGen(3, 0)) })
+	assert.Panics(t, func() { w.RemEntity(newEntityGen(2, 1)) })
+	assert.Panics(t, func() { w.RemEntity(newEntityGen(1, 0)) })
 }
 
 func TestWorldComponents(t *testing.T) {
@@ -62,10 +62,10 @@ func TestWorldComponents(t *testing.T) {
 
 	w.Remove(e2, posID)
 
-	maskNone := base.NewMask()
-	maskPos := base.NewMask(posID)
-	maskRot := base.NewMask(rotID)
-	maskPosRot := base.NewMask(posID, rotID)
+	maskNone := base.NewBitMask()
+	maskPos := base.NewBitMask(posID)
+	maskRot := base.NewBitMask(rotID)
+	maskPosRot := base.NewBitMask(posID, rotID)
 
 	archNone, ok := w.findArchetype(maskNone)
 	assert.True(t, ok)
@@ -98,8 +98,8 @@ func TestWorldComponents(t *testing.T) {
 	w.Remove(e0)
 
 	w.RemEntity(e0)
-	assert.Panics(t, func() { w.Has(Entity{1, 0}, posID) })
-	assert.Panics(t, func() { w.Get(Entity{1, 0}, posID) })
+	assert.Panics(t, func() { w.Has(newEntityGen(1, 0), posID) })
+	assert.Panics(t, func() { w.Get(newEntityGen(1, 0), posID) })
 }
 
 func TestWorldLabels(t *testing.T) {
@@ -333,7 +333,7 @@ func TestArchetypeGraph(t *testing.T) {
 	arch01 := world.findOrCreateArchetype(arch0, []ID{velID}, []ID{})
 	arch012 := world.findOrCreateArchetype(arch01, []ID{rotID}, []ID{})
 
-	assert.Equal(t, []ID{0, 1, 2}, arch012.ids)
+	assert.Equal(t, []ID{0, 1, 2}, arch012.Ids)
 
 	archEmpty3 := world.findOrCreateArchetype(arch012, []ID{}, []ID{posID, rotID, velID})
 	assert.Equal(t, archEmpty, archEmpty3)
@@ -390,13 +390,11 @@ func Test1000Archetypes(t *testing.T) {
 
 func TestTypeSizes(t *testing.T) {
 	printTypeSize[World]()
-	printTypeSizeName[pagedArr32[archetype]]("PagedArr32")
+	printTypeSizeName[base.PagedArr32[archetype]]("PagedArr32")
 	printTypeSize[archetype]()
-	printTypeSize[storage]()
+	printTypeSize[base.Storage]()
 	printTypeSize[Query]()
 	printTypeSize[archetypeIter]()
-	printTypeSizeName[Q1[testStruct0]]("Q1")
-	printTypeSizeName[Q8[testStruct0, testStruct1, testStruct2, testStruct3, testStruct4, testStruct5, testStruct6, testStruct7]]("Q8")
 }
 
 func printTypeSize[T any]() {
diff --git a/ecs/filter_test.go b/filter/filter_test.go
similarity index 87%
rename from ecs/filter_test.go
rename to filter/filter_test.go
index 061e468..32eccda 100644
--- a/ecs/filter_test.go
+++ b/filter/filter_test.go
@@ -1,4 +1,4 @@
-package ecs
+package filter
 
 import (
 	"testing"
@@ -7,14 +7,13 @@ import (
 )
 
 func TestLogicFilters(t *testing.T) {
-	w := NewWorld()
 
-	hasA := Mask1[testStruct0](&w)
-	hasB := Mask1[testStruct1](&w)
-	hasAll := Mask2[testStruct0, testStruct1](&w)
+	hasA := All(0)
+	hasB := All(1)
+	hasAll := All(0, 1)
 	hasNone := Mask{}
 
-	var filter filter
+	var filter MaskFilter
 	filter = hasA
 	assert.True(t, match(filter, hasAll))
 	assert.True(t, match(filter, hasA))
@@ -64,6 +63,6 @@ func TestLogicFilters(t *testing.T) {
 	assert.True(t, match(filter, hasNone))
 }
 
-func match(f filter, m Mask) bool {
-	return f.Matches(m)
+func match(f MaskFilter, m Mask) bool {
+	return f.Matches(m.BitMask)
 }
diff --git a/ecs/generic_test.go b/generic/generic_test.go
similarity index 60%
rename from ecs/generic_test.go
rename to generic/generic_test.go
index eee599a..0322c78 100644
--- a/ecs/generic_test.go
+++ b/generic/generic_test.go
@@ -1,13 +1,52 @@
-package ecs
+package generic
 
 import (
 	"testing"
 
+	"github.com/mlange-42/arche/ecs"
 	"github.com/stretchr/testify/assert"
 )
 
+type testStruct0 struct{ val int32 }
+type testStruct1 struct{ val int32 }
+type testStruct2 struct{ val int32 }
+type testStruct3 struct{ val int32 }
+type testStruct4 struct{ val int32 }
+type testStruct5 struct{ val int32 }
+type testStruct6 struct{ val int32 }
+type testStruct7 struct{ val int32 }
+type testStruct8 struct{ val int32 }
+type testStruct9 struct{ val int32 }
+
+func registerAll(w *ecs.World) []ecs.ID {
+	_ = testStruct0{1}
+	_ = testStruct1{1}
+	_ = testStruct2{1}
+	_ = testStruct3{1}
+	_ = testStruct4{1}
+	_ = testStruct5{1}
+	_ = testStruct6{1}
+	_ = testStruct7{1}
+	_ = testStruct8{1}
+	_ = testStruct9{1}
+
+	ids := make([]ecs.ID, 10)
+	ids[0] = ecs.ComponentID[testStruct0](w)
+	ids[1] = ecs.ComponentID[testStruct1](w)
+	ids[2] = ecs.ComponentID[testStruct2](w)
+	ids[3] = ecs.ComponentID[testStruct3](w)
+	ids[4] = ecs.ComponentID[testStruct4](w)
+	ids[5] = ecs.ComponentID[testStruct5](w)
+	ids[6] = ecs.ComponentID[testStruct6](w)
+	ids[7] = ecs.ComponentID[testStruct7](w)
+	ids[8] = ecs.ComponentID[testStruct8](w)
+	ids[9] = ecs.ComponentID[testStruct9](w)
+
+	return ids
+}
+
 func TestGenericMap(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 	get := NewMap[testStruct0](&w)
 
 	e0 := w.NewEntity()
@@ -27,7 +66,7 @@ func TestGenericMap(t *testing.T) {
 }
 
 func TestGenericAddRemove(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 	get := NewMap[testStruct0](&w)
 
 	e0 := w.NewEntity()
@@ -51,7 +90,7 @@ func TestGenericAddRemove(t *testing.T) {
 }
 
 func TestGenericAssignRemove(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	e0 := w.NewEntity()
 
diff --git a/ecs/query_generic_test.go b/generic/query_generic_test.go
similarity index 92%
rename from ecs/query_generic_test.go
rename to generic/query_generic_test.go
index 2e77aa9..bf8d610 100644
--- a/ecs/query_generic_test.go
+++ b/generic/query_generic_test.go
@@ -1,14 +1,15 @@
-package ecs
+package generic
 
 import (
 	"internal/base"
 	"testing"
 
+	"github.com/mlange-42/arche/ecs"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestQuery0(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -33,7 +34,7 @@ func TestQuery0(t *testing.T) {
 }
 
 func TestQuery1(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -60,7 +61,7 @@ func TestQuery1(t *testing.T) {
 }
 
 func TestQuery2(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -98,7 +99,7 @@ func TestQuery2(t *testing.T) {
 }
 
 func TestQuery3(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -143,7 +144,7 @@ func TestQuery3(t *testing.T) {
 }
 
 func TestQuery4(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -194,7 +195,7 @@ func TestQuery4(t *testing.T) {
 }
 
 func TestQuery5(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -252,7 +253,7 @@ func TestQuery5(t *testing.T) {
 }
 
 func TestQuery6(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -317,7 +318,7 @@ func TestQuery6(t *testing.T) {
 }
 
 func TestQuery7(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -389,7 +390,7 @@ func TestQuery7(t *testing.T) {
 }
 
 func TestQuery8(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	registerAll(&w)
 
@@ -463,49 +464,49 @@ func TestQuery8(t *testing.T) {
 }
 
 func TestGenericMask(t *testing.T) {
-	w := NewWorld()
+	w := ecs.NewWorld()
 
 	assert.Equal(t,
-		Mask1[testStruct0](&w).mask,
-		base.NewMask(0),
+		Mask1[testStruct0](&w).BitMask,
+		base.NewBitMask(0),
 	)
 
 	assert.Equal(t,
-		Mask2[testStruct0, testStruct1](&w).mask,
-		base.NewMask(0, 1),
+		Mask2[testStruct0, testStruct1](&w).BitMask,
+		base.NewBitMask(0, 1),
 	)
 
 	assert.Equal(t,
-		Mask3[testStruct0, testStruct1, testStruct2](&w).mask,
-		base.NewMask(0, 1, 2),
+		Mask3[testStruct0, testStruct1, testStruct2](&w).BitMask,
+		base.NewBitMask(0, 1, 2),
 	)
 
 	assert.Equal(t,
-		Mask4[testStruct0, testStruct1, testStruct2, testStruct3](&w).mask,
-		base.NewMask(0, 1, 2, 3),
+		Mask4[testStruct0, testStruct1, testStruct2, testStruct3](&w).BitMask,
+		base.NewBitMask(0, 1, 2, 3),
 	)
 
 	assert.Equal(t,
 		Mask5[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4](&w).mask,
-		base.NewMask(0, 1, 2, 3, 4),
+			testStruct4](&w).BitMask,
+		base.NewBitMask(0, 1, 2, 3, 4),
 	)
 
 	assert.Equal(t,
 		Mask6[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5](&w).mask,
-		base.NewMask(0, 1, 2, 3, 4, 5),
+			testStruct4, testStruct5](&w).BitMask,
+		base.NewBitMask(0, 1, 2, 3, 4, 5),
 	)
 
 	assert.Equal(t,
 		Mask7[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5, testStruct6](&w).mask,
-		base.NewMask(0, 1, 2, 3, 4, 5, 6),
+			testStruct4, testStruct5, testStruct6](&w).BitMask,
+		base.NewBitMask(0, 1, 2, 3, 4, 5, 6),
 	)
 
 	assert.Equal(t,
 		Mask8[testStruct0, testStruct1, testStruct2, testStruct3,
-			testStruct4, testStruct5, testStruct6, testStruct7](&w).mask,
-		base.NewMask(0, 1, 2, 3, 4, 5, 6, 7),
+			testStruct4, testStruct5, testStruct6, testStruct7](&w).BitMask,
+		base.NewBitMask(0, 1, 2, 3, 4, 5, 6, 7),
 	)
 }
diff --git a/ecs/bit_pool_test.go b/internal/base/bit_pool_test.go
similarity index 92%
rename from ecs/bit_pool_test.go
rename to internal/base/bit_pool_test.go
index abaa204..35df12e 100644
--- a/ecs/bit_pool_test.go
+++ b/internal/base/bit_pool_test.go
@@ -1,4 +1,4 @@
-package ecs
+package base
 
 import (
 	"testing"
@@ -7,7 +7,7 @@ import (
 )
 
 func TestBitPool(t *testing.T) {
-	p := newBitPool()
+	p := NewBitPool()
 
 	for i := 0; i < MaskTotalBits; i++ {
 		assert.Equal(t, i, int(p.Get()))
diff --git a/internal/base/bitmask_test.go b/internal/base/bitmask_test.go
index 6d15b46..6febfc2 100644
--- a/internal/base/bitmask_test.go
+++ b/internal/base/bitmask_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func TestBitMask(t *testing.T) {
-	mask := NewMask(ID(1), ID(2), ID(13), ID(27))
+	mask := NewBitMask(ID(1), ID(2), ID(13), ID(27))
 
 	assert.Equal(t, uint(4), mask.TotalBitsSet())
 
@@ -25,8 +25,8 @@ func TestBitMask(t *testing.T) {
 	assert.True(t, mask.Get(0))
 	assert.False(t, mask.Get(1))
 
-	other1 := NewMask(ID(1), ID(2), ID(32))
-	other2 := NewMask(ID(0), ID(2))
+	other1 := NewBitMask(ID(1), ID(2), ID(32))
+	other2 := NewBitMask(ID(0), ID(2))
 
 	assert.False(t, mask.Contains(other1))
 	assert.True(t, mask.Contains(other2))
@@ -34,9 +34,9 @@ func TestBitMask(t *testing.T) {
 	mask.Reset()
 	assert.Equal(t, uint(0), mask.TotalBitsSet())
 
-	mask = NewMask(ID(1), ID(2), ID(13), ID(27))
-	other1 = NewMask(ID(1), ID(32))
-	other2 = NewMask(ID(0), ID(32))
+	mask = NewBitMask(ID(1), ID(2), ID(13), ID(27))
+	other1 = NewBitMask(ID(1), ID(32))
+	other2 = NewBitMask(ID(0), ID(32))
 
 	assert.True(t, mask.ContainsAny(other1))
 	assert.False(t, mask.ContainsAny(other2))
diff --git a/ecs/paged_test.go b/internal/base/paged_test.go
similarity index 84%
rename from ecs/paged_test.go
rename to internal/base/paged_test.go
index aa45244..be895df 100644
--- a/ecs/paged_test.go
+++ b/internal/base/paged_test.go
@@ -1,4 +1,4 @@
-package ecs
+package base
 
 import (
 	"testing"
@@ -7,7 +7,7 @@ import (
 )
 
 func TestPagedArr32(t *testing.T) {
-	a := pagedArr32[int]{}
+	a := PagedArr32[int]{}
 
 	for i := 0; i < 66; i++ {
 		a.Add(i)
diff --git a/ecs/registry_test.go b/internal/base/registry_test.go
similarity index 61%
rename from ecs/registry_test.go
rename to internal/base/registry_test.go
index eb9c242..c0a55c3 100644
--- a/ecs/registry_test.go
+++ b/internal/base/registry_test.go
@@ -1,4 +1,4 @@
-package ecs
+package base
 
 import (
 	"reflect"
@@ -7,13 +7,22 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+type position struct {
+	X int
+	Y int
+}
+
+type rotation struct {
+	Angle int
+}
+
 func TestComponentRegistry(t *testing.T) {
-	reg := newComponentRegistry()
+	reg := NewComponentRegistry()
 
 	posType := reflect.TypeOf((*position)(nil)).Elem()
 	rotType := reflect.TypeOf((*rotation)(nil)).Elem()
 
-	reg.registerComponent(posType, MaskTotalBits)
+	reg.RegisterComponent(posType, MaskTotalBits)
 
 	assert.Equal(t, ID(0), reg.ComponentID(posType))
 	assert.Equal(t, ID(1), reg.ComponentID(rotType))
@@ -23,11 +32,11 @@ func TestComponentRegistry(t *testing.T) {
 }
 
 func TestComponentRegistryOverflow(t *testing.T) {
-	reg := newComponentRegistry()
+	reg := NewComponentRegistry()
 
-	reg.registerComponent(reflect.TypeOf((*position)(nil)).Elem(), 1)
+	reg.RegisterComponent(reflect.TypeOf((*position)(nil)).Elem(), 1)
 
 	assert.Panics(t, func() {
-		reg.registerComponent(reflect.TypeOf((*rotation)(nil)).Elem(), 1)
+		reg.RegisterComponent(reflect.TypeOf((*rotation)(nil)).Elem(), 1)
 	})
 }
diff --git a/ecs/storage_test.go b/internal/base/storage_test.go
similarity index 91%
rename from ecs/storage_test.go
rename to internal/base/storage_test.go
index 301854f..4246071 100644
--- a/ecs/storage_test.go
+++ b/internal/base/storage_test.go
@@ -1,4 +1,4 @@
-package ecs
+package base
 
 import (
 	"reflect"
@@ -20,12 +20,12 @@ type simpleStruct struct {
 
 func TestReflectStorageAddGet(t *testing.T) {
 	obj1 := testStruct{}
-	s := storage{}
-	s.init(reflect.TypeOf(obj1), 1)
+	s := Storage{}
+	s.Init(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}
 
@@ -53,13 +53,13 @@ func storageAddGet(t *testing.T, s storage) {
 
 func TestReflectStorageRemove(t *testing.T) {
 	ref := simpleStruct{}
-	s := storage{}
-	s.init(reflect.TypeOf(ref), 32)
+	s := Storage{}
+	s.Init(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)
@@ -78,8 +78,8 @@ func storageRemove(t *testing.T, s storage) {
 
 func TestReflectStorageDataSize(t *testing.T) {
 	ref := simpleStruct{}
-	s := storage{}
-	s.init(reflect.TypeOf(ref), 1)
+	s := Storage{}
+	s.Init(reflect.TypeOf(ref), 1)
 
 	for i := 0; i < 5; i++ {
 		obj := simpleStruct{i}
@@ -103,15 +103,15 @@ func TestReflectStorageDataSize(t *testing.T) {
 }
 
 func TestNewReflectStorage(t *testing.T) {
-	s := storage{}
-	s.init(reflect.TypeOf(simpleStruct{}), 32)
+	s := Storage{}
+	s.Init(reflect.TypeOf(simpleStruct{}), 32)
 }
 
 func BenchmarkIterReflectStorage_1000(b *testing.B) {
 	b.StopTimer()
 	ref := testStruct{}
-	s := storage{}
-	s.init(reflect.TypeOf(ref), 128)
+	s := Storage{}
+	s.Init(reflect.TypeOf(ref), 128)
 	for i := 0; i < 1000; i++ {
 		s.Add(&testStruct{})
 	}
@@ -165,8 +165,8 @@ func BenchmarkAddReflectStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := storage{}
-		s.init(reflect.TypeOf(ref), 1024)
+		s := Storage{}
+		s.Init(reflect.TypeOf(ref), 1024)
 		b.StartTimer()
 
 		for i := 0; i < 1000; i++ {
@@ -213,8 +213,8 @@ func BenchmarkRemoveReflectStorage_1000(b *testing.B) {
 	ref := testStruct{}
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
-		s := storage{}
-		s.init(reflect.TypeOf(ref), 1024)
+		s := Storage{}
+		s.Init(reflect.TypeOf(ref), 1024)
 		for i := 0; i < 1000; i++ {
 			s.Add(&ref)
 		}