Repository: mlange-42/arche
Pull request #268 (2023-05-04 12:57:17)

Reduce size of `archNode` and `entityIndex`
* Move stuff that is not used in hot code from `archNode` to a struct behind a pointer
* Move `isTarget` from `entityIndex` into a separate bitset

coveralls: ## Pull Request Test Coverage Report for [Build 4883153931](https://coveralls.io/builds/59624155)

* **285** of **285**   **(100.0%)**  changed or added relevant lines in **5** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/59624155/badge)](https://coveralls.io/builds/59624155) |
| :-- | --: |
| Change from base [Build 4882379906](https://coveralls.io/builds/59619005): |  0.0% |
| Covered Lines: | 3679 |
| Relevant Lines: | 3679 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 091e7ca..739d0c9 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -11,234 +11,6 @@ import (
 // layoutSize is the size of an archetype column layout in bytes.
 var layoutSize = unsafe.Sizeof(layout{})
 
-// archetypeNode is a node in the archetype graph.
-type archetypeNode struct {
-	Mask              Mask                  // Mask of the archetype
-	Ids               []ID                  // List of component IDs
-	Types             []reflect.Type        // Component type per column
-	archetype         *archetype            // The single archetype for nodes without entity
-	archetypes        pagedSlice[archetype] // Storage for archetypes in nodes with entity relation
-	archetypeMap      map[Entity]*archetype // Mapping from relation targets to archetypes
-	freeIndices       []int32               // Indices of free/inactive archetypes
-	TransitionAdd     idMap[*archetypeNode] // Mapping from component ID to add to the resulting archetype
-	TransitionRemove  idMap[*archetypeNode] // Mapping from component ID to remove to the resulting archetype
-	Relation          int8                  // The node's relation component ID. Negative value stands for no relation
-	zeroValue         []byte                // Used as source for setting storage to zero
-	zeroPointer       unsafe.Pointer        // Points to zeroValue for fast access
-	capacityIncrement uint32                // Capacity increment
-	IsActive          bool
-}
-
-// Creates a new archetypeNode
-func newArchetypeNode(mask Mask, relation int8, capacityIncrement int, components []componentType) archetypeNode {
-	var arch map[Entity]*archetype
-	if relation >= 0 {
-		arch = map[Entity]*archetype{}
-	}
-	ids := make([]ID, len(components))
-	types := make([]reflect.Type, len(components))
-
-	var maxSize uintptr = 0
-	prev := -1
-	for i, c := range components {
-		if int(c.ID) <= prev {
-			panic("component arguments must be sorted by ID")
-		}
-		prev = int(c.ID)
-
-		ids[i] = c.ID
-		types[i] = c.Type
-		size, align := c.Type.Size(), uintptr(c.Type.Align())
-		size = (size + (align - 1)) / align * align
-		if size > maxSize {
-			maxSize = size
-		}
-	}
-
-	var zeroValue []byte
-	var zeroPointer unsafe.Pointer
-	if maxSize > 0 {
-		zeroValue = make([]byte, maxSize)
-		zeroPointer = unsafe.Pointer(&zeroValue[0])
-	}
-
-	return archetypeNode{
-		Mask:              mask,
-		Ids:               ids,
-		Types:             types,
-		archetypeMap:      arch,
-		TransitionAdd:     newIDMap[*archetypeNode](),
-		TransitionRemove:  newIDMap[*archetypeNode](),
-		Relation:          relation,
-		capacityIncrement: uint32(capacityIncrement),
-		zeroValue:         zeroValue,
-		zeroPointer:       zeroPointer,
-	}
-}
-
-// Matches the archetype node against a filter.
-// Ignores the relation target.
-func (a *archetypeNode) Matches(f Filter) bool {
-	return f.Matches(a.Mask, nil)
-}
-
-// Archetypes of the node.
-// Returns a single wrapped archetype if there are no relations.
-// Returns nil if the node has no archetype(s).
-func (a *archetypeNode) Archetypes() archetypes {
-	if a.HasRelation() {
-		return &a.archetypes
-	}
-	if a.archetype == nil {
-		return nil
-	}
-	return singleArchetype{Archetype: a.archetype}
-}
-
-// GetArchetype returns the archetype for the given relation target.
-//
-// The target is ignored if the node has no relation component.
-func (a *archetypeNode) GetArchetype(target Entity) *archetype {
-	if a.Relation >= 0 {
-		return a.archetypeMap[target]
-	}
-	return a.archetype
-}
-
-// SetArchetype sets the archetype for a node without a relation.
-//
-// Do not use on nodes without a relation component!
-func (a *archetypeNode) SetArchetype(arch *archetype) {
-	a.archetype = arch
-}
-
-// CreateArchetype creates a new archetype in nodes with relation component.
-func (a *archetypeNode) CreateArchetype(target Entity) *archetype {
-	var arch *archetype
-	var archIndex int32
-	lenFree := len(a.freeIndices)
-	if lenFree > 0 {
-		archIndex = a.freeIndices[lenFree-1]
-		arch = a.archetypes.Get(archIndex)
-		a.freeIndices = a.freeIndices[:lenFree-1]
-		arch.Activate(target, archIndex)
-	} else {
-		a.archetypes.Add(archetype{})
-		archIndex := a.archetypes.Len() - 1
-		arch = a.archetypes.Get(archIndex)
-		arch.Init(a, archIndex, true, target)
-	}
-	a.archetypeMap[target] = arch
-	return arch
-}
-
-// RemoveArchetype de-activates an archetype.
-// The archetype will be re-used by CreateArchetype.
-func (a *archetypeNode) RemoveArchetype(arch *archetype) {
-	delete(a.archetypeMap, arch.RelationTarget)
-	idx := arch.index
-	a.freeIndices = append(a.freeIndices, idx)
-	a.archetypes.Get(idx).Deactivate()
-}
-
-// HasRelation returns whether the node has a relation component.
-func (a *archetypeNode) HasRelation() bool {
-	return a.Relation >= 0
-}
-
-// Stats generates statistics for an archetype node.
-func (a *archetypeNode) Stats(reg *componentRegistry[ID]) stats.NodeStats {
-	ids := a.Ids
-	aCompCount := len(ids)
-	aTypes := make([]reflect.Type, aCompCount)
-	for j, id := range ids {
-		aTypes[j], _ = reg.ComponentType(id)
-	}
-
-	arches := a.Archetypes()
-	var numArches int32
-	cap := 0
-	count := 0
-	memory := 0
-	var archStats []stats.ArchetypeStats
-	if arches != nil {
-		numArches = arches.Len()
-		archStats = make([]stats.ArchetypeStats, numArches)
-		var i int32
-		for i = 0; i < numArches; i++ {
-			archStats[i] = arches.Get(i).Stats(reg)
-			stats := &archStats[i]
-			cap += stats.Capacity
-			count += stats.Size
-			memory += stats.Memory
-		}
-	}
-
-	memPerEntity := 0
-	for j := range ids {
-		memPerEntity += int(aTypes[j].Size())
-	}
-
-	return stats.NodeStats{
-		ArchetypeCount:       int(numArches),
-		ActiveArchetypeCount: int(numArches) - len(a.freeIndices),
-		IsActive:             a.IsActive,
-		HasRelation:          a.HasRelation(),
-		Components:           aCompCount,
-		ComponentIDs:         ids,
-		ComponentTypes:       aTypes,
-		Memory:               memory,
-		MemoryPerEntity:      memPerEntity,
-		Size:                 count,
-		Capacity:             cap,
-		Archetypes:           archStats,
-	}
-}
-
-// UpdateStats updates statistics for an archetype node.
-func (a *archetypeNode) UpdateStats(stats *stats.NodeStats, reg *componentRegistry[ID]) {
-	if !a.IsActive {
-		return
-	}
-
-	arches := a.Archetypes()
-
-	if !stats.IsActive {
-		temp := a.Stats(reg)
-		*stats = temp
-		return
-	}
-
-	cap := 0
-	count := 0
-	memory := 0
-
-	cntOld := int32(len(stats.Archetypes))
-	cntNew := int32(arches.Len())
-	var i int32
-	for i = 0; i < cntOld; i++ {
-		arch := &stats.Archetypes[i]
-		arches.Get(i).UpdateStats(stats, arch, reg)
-		cap += arch.Capacity
-		count += arch.Size
-		memory += arch.Memory
-	}
-	for i = cntOld; i < cntNew; i++ {
-		arch := arches.Get(i).Stats(reg)
-		stats.Archetypes = append(stats.Archetypes, arch)
-		cap += arch.Capacity
-		count += arch.Size
-		memory += arch.Memory
-	}
-
-	stats.IsActive = true
-	stats.ArchetypeCount = int(cntNew)
-	stats.ActiveArchetypeCount = int(cntNew) - len(a.freeIndices)
-	stats.Capacity = cap
-	stats.Size = count
-	stats.Memory = memory
-}
-
 // Helper for accessing data from an archetype
 type archetypeAccess struct {
 	Mask              Mask           // Archetype's mask
@@ -295,7 +67,7 @@ func (l *layout) Get(index uintptr) unsafe.Pointer {
 // archetype represents an ECS archetype
 type archetype struct {
 	archetypeAccess                 // Access helper, passed to queries.
-	graphNode       *archetypeNode  // Node in the archetype graph.
+	node            *archNode       // Node in the archetype graph.
 	layouts         []layout        // Column layouts by ID.
 	indices         idMap[uint32]   // Mapping from IDs to buffer indices.
 	buffers         []reflect.Value // Reflection arrays containing component data.
@@ -306,7 +78,7 @@ type archetype struct {
 }
 
 // Init initializes an archetype
-func (a *archetype) Init(node *archetypeNode, index int32, forStorage bool, relation Entity) {
+func (a *archetype) Init(node *archNode, index int32, forStorage bool, relation Entity) {
 	if !node.IsActive {
 		node.IsActive = true
 	}
@@ -343,7 +115,7 @@ func (a *archetype) Init(node *archetypeNode, index int32, forStorage bool, rela
 		RelationComponent: node.Relation,
 	}
 
-	a.graphNode = node
+	a.node = node
 
 	a.len = 0
 	a.cap = uint32(cap)
@@ -366,7 +138,7 @@ func (a *archetype) AllocN(count uint32) {
 
 // Add adds an entity with components to the archetype.
 func (a *archetype) Add(entity Entity, components ...Component) uintptr {
-	if len(components) != len(a.graphNode.Ids) {
+	if len(components) != len(a.node.Ids) {
 		panic("Invalid number of components")
 	}
 	idx := uintptr(a.len)
@@ -397,7 +169,7 @@ func (a *archetype) Remove(index uintptr) bool {
 	old := uintptr(a.len - 1)
 
 	if index != old {
-		for _, id := range a.graphNode.Ids {
+		for _, id := range a.node.Ids {
 			lay := a.getLayout(id)
 			size := lay.itemSize
 			if size == 0 {
@@ -416,7 +188,7 @@ func (a *archetype) Remove(index uintptr) bool {
 
 // ZeroAll resets a block of storage in all buffers.
 func (a *archetype) ZeroAll(index uintptr) {
-	for _, id := range a.graphNode.Ids {
+	for _, id := range a.node.Ids {
 		a.Zero(index, id)
 	}
 }
@@ -429,7 +201,7 @@ func (a *archetype) Zero(index uintptr, id ID) {
 		return
 	}
 	dst := unsafe.Add(lay.pointer, index*size)
-	a.copy(a.graphNode.zeroPointer, dst, size)
+	a.copy(a.node.zeroPointer, dst, size)
 }
 
 // SetEntity overwrites an entity
@@ -498,7 +270,7 @@ func (a *archetype) IsActive() bool {
 
 // Components returns the component IDs for this archetype
 func (a *archetype) Components() []ID {
-	return a.graphNode.Ids
+	return a.node.Ids
 }
 
 // Len reports the number of entities in the archetype
@@ -522,7 +294,7 @@ func (a *archetype) Stats(reg *componentRegistry[ID]) stats.ArchetypeStats {
 
 	cap := int(a.Cap())
 	memPerEntity := 0
-	for _, id := range a.graphNode.Ids {
+	for _, id := range a.node.Ids {
 		lay := a.getLayout(id)
 		memPerEntity += int(lay.itemSize)
 	}
@@ -560,14 +332,14 @@ func (a *archetype) extend(by uint32) {
 	if a.cap >= required {
 		return
 	}
-	a.cap = capacityU32(required, a.graphNode.capacityIncrement)
+	a.cap = capacityU32(required, a.node.capacityIncrement)
 
 	old := a.entityBuffer
 	a.entityBuffer = reflect.New(reflect.ArrayOf(int(a.cap), entityType)).Elem()
 	a.entityPointer = a.entityBuffer.Addr().UnsafePointer()
 	reflect.Copy(a.entityBuffer, old)
 
-	for _, id := range a.graphNode.Ids {
+	for _, id := range a.node.Ids {
 		lay := a.getLayout(id)
 		if lay.itemSize == 0 {
 			continue
diff --git a/ecs/archetype_node.go b/ecs/archetype_node.go
new file mode 100644
index 0000000..00ef41b
--- /dev/null
+++ b/ecs/archetype_node.go
@@ -0,0 +1,242 @@
+package ecs
+
+import (
+	"reflect"
+	"unsafe"
+
+	"github.com/mlange-42/arche/ecs/stats"
+)
+
+// archNode is a node in the archetype graph.
+type archNode struct {
+	*nodeData
+	Mask             Mask             // Mask of the archetype
+	TransitionAdd    idMap[*archNode] // Mapping from component ID to add to the resulting archetype
+	TransitionRemove idMap[*archNode] // Mapping from component ID to remove to the resulting archetype
+	Relation         int8             // The node's relation component ID. Negative value stands for no relation
+	IsActive         bool
+}
+
+type nodeData struct {
+	Ids               []ID                  // List of component IDs
+	Types             []reflect.Type        // Component type per column
+	archetype         *archetype            // The single archetype for nodes without entity
+	archetypes        pagedSlice[archetype] // Storage for archetypes in nodes with entity relation
+	archetypeMap      map[Entity]*archetype // Mapping from relation targets to archetypes
+	freeIndices       []int32               // Indices of free/inactive archetypes
+	zeroValue         []byte                // Used as source for setting storage to zero
+	zeroPointer       unsafe.Pointer        // Points to zeroValue for fast access
+	capacityIncrement uint32                // Capacity increment
+}
+
+// Creates a new archNode
+func newArchNode(mask Mask, data *nodeData, relation int8, capacityIncrement int, components []componentType) archNode {
+	var arch map[Entity]*archetype
+	if relation >= 0 {
+		arch = map[Entity]*archetype{}
+	}
+	ids := make([]ID, len(components))
+	types := make([]reflect.Type, len(components))
+
+	var maxSize uintptr = 0
+	prev := -1
+	for i, c := range components {
+		if int(c.ID) <= prev {
+			panic("component arguments must be sorted by ID")
+		}
+		prev = int(c.ID)
+
+		ids[i] = c.ID
+		types[i] = c.Type
+		size, align := c.Type.Size(), uintptr(c.Type.Align())
+		size = (size + (align - 1)) / align * align
+		if size > maxSize {
+			maxSize = size
+		}
+	}
+
+	var zeroValue []byte
+	var zeroPointer unsafe.Pointer
+	if maxSize > 0 {
+		zeroValue = make([]byte, maxSize)
+		zeroPointer = unsafe.Pointer(&zeroValue[0])
+	}
+
+	data.Ids = ids
+	data.Types = types
+	data.archetypeMap = arch
+	data.capacityIncrement = uint32(capacityIncrement)
+	data.zeroValue = zeroValue
+	data.zeroPointer = zeroPointer
+
+	return archNode{
+		nodeData:         data,
+		Mask:             mask,
+		TransitionAdd:    newIDMap[*archNode](),
+		TransitionRemove: newIDMap[*archNode](),
+		Relation:         relation,
+	}
+}
+
+// Matches the archetype node against a filter.
+// Ignores the relation target.
+func (a *archNode) Matches(f Filter) bool {
+	return f.Matches(a.Mask, nil)
+}
+
+// Archetypes of the node.
+// Returns a single wrapped archetype if there are no relations.
+// Returns nil if the node has no archetype(s).
+func (a *archNode) Archetypes() archetypes {
+	if a.HasRelation() {
+		return &a.archetypes
+	}
+	if a.archetype == nil {
+		return nil
+	}
+	return singleArchetype{Archetype: a.archetype}
+}
+
+// GetArchetype returns the archetype for the given relation target.
+//
+// The target is ignored if the node has no relation component.
+func (a *archNode) GetArchetype(target Entity) *archetype {
+	if a.Relation >= 0 {
+		return a.archetypeMap[target]
+	}
+	return a.archetype
+}
+
+// SetArchetype sets the archetype for a node without a relation.
+//
+// Do not use on nodes without a relation component!
+func (a *archNode) SetArchetype(arch *archetype) {
+	a.archetype = arch
+}
+
+// CreateArchetype creates a new archetype in nodes with relation component.
+func (a *archNode) CreateArchetype(target Entity) *archetype {
+	var arch *archetype
+	var archIndex int32
+	lenFree := len(a.freeIndices)
+	if lenFree > 0 {
+		archIndex = a.freeIndices[lenFree-1]
+		arch = a.archetypes.Get(archIndex)
+		a.freeIndices = a.freeIndices[:lenFree-1]
+		arch.Activate(target, archIndex)
+	} else {
+		a.archetypes.Add(archetype{})
+		archIndex := a.archetypes.Len() - 1
+		arch = a.archetypes.Get(archIndex)
+		arch.Init(a, archIndex, true, target)
+	}
+	a.archetypeMap[target] = arch
+	return arch
+}
+
+// RemoveArchetype de-activates an archetype.
+// The archetype will be re-used by CreateArchetype.
+func (a *archNode) RemoveArchetype(arch *archetype) {
+	delete(a.archetypeMap, arch.RelationTarget)
+	idx := arch.index
+	a.freeIndices = append(a.freeIndices, idx)
+	a.archetypes.Get(idx).Deactivate()
+}
+
+// HasRelation returns whether the node has a relation component.
+func (a *archNode) HasRelation() bool {
+	return a.Relation >= 0
+}
+
+// Stats generates statistics for an archetype node.
+func (a *archNode) Stats(reg *componentRegistry[ID]) stats.NodeStats {
+	ids := a.Ids
+	aCompCount := len(ids)
+	aTypes := make([]reflect.Type, aCompCount)
+	for j, id := range ids {
+		aTypes[j], _ = reg.ComponentType(id)
+	}
+
+	arches := a.Archetypes()
+	var numArches int32
+	cap := 0
+	count := 0
+	memory := 0
+	var archStats []stats.ArchetypeStats
+	if arches != nil {
+		numArches = arches.Len()
+		archStats = make([]stats.ArchetypeStats, numArches)
+		var i int32
+		for i = 0; i < numArches; i++ {
+			archStats[i] = arches.Get(i).Stats(reg)
+			stats := &archStats[i]
+			cap += stats.Capacity
+			count += stats.Size
+			memory += stats.Memory
+		}
+	}
+
+	memPerEntity := 0
+	for j := range ids {
+		memPerEntity += int(aTypes[j].Size())
+	}
+
+	return stats.NodeStats{
+		ArchetypeCount:       int(numArches),
+		ActiveArchetypeCount: int(numArches) - len(a.freeIndices),
+		IsActive:             a.IsActive,
+		HasRelation:          a.HasRelation(),
+		Components:           aCompCount,
+		ComponentIDs:         ids,
+		ComponentTypes:       aTypes,
+		Memory:               memory,
+		MemoryPerEntity:      memPerEntity,
+		Size:                 count,
+		Capacity:             cap,
+		Archetypes:           archStats,
+	}
+}
+
+// UpdateStats updates statistics for an archetype node.
+func (a *archNode) UpdateStats(stats *stats.NodeStats, reg *componentRegistry[ID]) {
+	if !a.IsActive {
+		return
+	}
+
+	arches := a.Archetypes()
+
+	if !stats.IsActive {
+		temp := a.Stats(reg)
+		*stats = temp
+		return
+	}
+
+	cap := 0
+	count := 0
+	memory := 0
+
+	cntOld := int32(len(stats.Archetypes))
+	cntNew := int32(arches.Len())
+	var i int32
+	for i = 0; i < cntOld; i++ {
+		arch := &stats.Archetypes[i]
+		arches.Get(i).UpdateStats(stats, arch, reg)
+		cap += arch.Capacity
+		count += arch.Size
+		memory += arch.Memory
+	}
+	for i = cntOld; i < cntNew; i++ {
+		arch := arches.Get(i).Stats(reg)
+		stats.Archetypes = append(stats.Archetypes, arch)
+		cap += arch.Capacity
+		count += arch.Size
+		memory += arch.Memory
+	}
+
+	stats.IsActive = true
+	stats.ArchetypeCount = int(cntNew)
+	stats.ActiveArchetypeCount = int(cntNew) - len(a.freeIndices)
+	stats.Capacity = cap
+	stats.Size = count
+	stats.Memory = memory
+}
diff --git a/ecs/archetypes.go b/ecs/archetypes.go
index 4eeb51d..4d0cf2d 100644
--- a/ecs/archetypes.go
+++ b/ecs/archetypes.go
@@ -2,7 +2,7 @@ package ecs
 
 // Interface for an iterator over multiple archetypes.
 type nodes interface {
-	Get(index int32) *archetypeNode
+	Get(index int32) *archNode
 	Len() int32
 }
 
diff --git a/ecs/bitset.go b/ecs/bitset.go
new file mode 100644
index 0000000..622cd84
--- /dev/null
+++ b/ecs/bitset.go
@@ -0,0 +1,47 @@
+package ecs
+
+// Data structure for compact storage of booleans.
+type bitSet struct {
+	data []uint64
+}
+
+// Get a value.
+func (b *bitSet) Get(bit eid) bool {
+	chunk, bit := bit/wordSize, bit%wordSize
+	mask := uint64(1 << bit)
+	return b.data[chunk]&mask == mask
+}
+
+// Set a value.
+func (b *bitSet) Set(bit eid, value bool) {
+	chunk, bit := bit/wordSize, bit%wordSize
+	if value {
+		b.data[chunk] |= uint64(1 << bit)
+	} else {
+		b.data[chunk] &= uint64(^(1 << bit))
+	}
+}
+
+// Reset all values.
+func (b *bitSet) Reset() {
+	ln := len(b.data)
+	for i := 0; i < ln; i++ {
+		b.data[i] = 0
+	}
+}
+
+// Extend to hold at least the given bits.
+func (b *bitSet) ExtendTo(length int) {
+	chunks, bit := length/wordSize, length%wordSize
+	if bit > 0 {
+		chunks++
+	}
+	if len(b.data) >= chunks {
+		return
+	}
+
+	old := b.data
+	b.data = make([]uint64, chunks)
+
+	copy(b.data, old)
+}
diff --git a/ecs/entity.go b/ecs/entity.go
index f4ad27d..029b29d 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -40,7 +40,6 @@ func (e Entity) IsZero() bool {
 
 // entityIndex indicates where an entity is currently stored.
 type entityIndex struct {
-	arch     *archetype // Entity's current archetype
-	index    uintptr    // Entity's current index in the archetype
-	isTarget bool
+	arch  *archetype // Entity's current archetype
+	index uintptr    // Entity's current index in the archetype
 }
diff --git a/ecs/util.go b/ecs/util.go
index 39aa558..2da54aa 100644
--- a/ecs/util.go
+++ b/ecs/util.go
@@ -130,10 +130,10 @@ func (p *pagedSlice[T]) Len() int32 {
 func debugPrintWorld(w *World) string {
 	sb := strings.Builder{}
 
-	ln := w.graph.Len()
+	ln := w.nodes.Len()
 	var i int32
 	for i = 0; i < ln; i++ {
-		nd := w.graph.Get(i)
+		nd := w.nodes.Get(i)
 		if !nd.IsActive {
 			fmt.Fprintf(&sb, "Node %v (inactive)\n", nd.Ids)
 			continue
diff --git a/ecs/world.go b/ecs/world.go
index 8cc1fc1..bd99030 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -62,18 +62,20 @@ func AddResource[T any](w *World, res *T) ResID {
 
 // World is the central type holding [Entity] and component data, as well as resources.
 type World struct {
-	config        Config                    // World configuration.
-	listener      func(e *EntityEvent)      // Component change listener.
-	resources     Resources                 // World resources.
-	entities      []entityIndex             // Mapping from entities to archetype and index.
-	entityPool    entityPool                // Pool for entities.
-	archetypes    pagedSlice[archetype]     // The archetypes.
-	graph         pagedSlice[archetypeNode] // The archetype graph.
-	relationNodes []*archetypeNode          // Archetype nodes that have an entity relation.
-	locks         lockMask                  // World locks.
-	registry      componentRegistry[ID]     // Component registry.
-	filterCache   Cache                     // Cache for registered filters.
-	stats         stats.WorldStats          // Cached world statistics
+	config         Config                // World configuration.
+	listener       func(e *EntityEvent)  // Component change listener.
+	resources      Resources             // World resources.
+	entities       []entityIndex         // Mapping from entities to archetype and index.
+	targetEntities bitSet                // Whether entities are potential relation targets.
+	entityPool     entityPool            // Pool for entities.
+	archetypes     pagedSlice[archetype] // The archetypes.
+	nodes          pagedSlice[archNode]  // The archetype graph.
+	nodeData       pagedSlice[nodeData]  // The archetype graph's data.
+	relationNodes  []*archNode           // Archetype nodes that have an entity relation.
+	locks          lockMask              // World locks.
+	registry       componentRegistry[ID] // Component registry.
+	filterCache    Cache                 // Cache for registered filters.
+	stats          stats.WorldStats      // Cached world statistics
 }
 
 // NewWorld creates a new [World] from an optional [Config].
@@ -100,18 +102,22 @@ func fromConfig(conf Config) World {
 	}
 	entities := make([]entityIndex, 1, conf.CapacityIncrement)
 	entities[0] = entityIndex{arch: nil, index: 0}
+	targetEntities := bitSet{}
+	targetEntities.ExtendTo(1)
+
 	w := World{
-		config:        conf,
-		entities:      entities,
-		entityPool:    newEntityPool(uint32(conf.CapacityIncrement)),
-		registry:      newComponentRegistry(),
-		archetypes:    newPagedSlice[archetype](),
-		graph:         newPagedSlice[archetypeNode](),
-		relationNodes: []*archetypeNode{},
-		locks:         lockMask{},
-		listener:      nil,
-		resources:     newResources(),
-		filterCache:   newCache(),
+		config:         conf,
+		entities:       entities,
+		targetEntities: targetEntities,
+		entityPool:     newEntityPool(uint32(conf.CapacityIncrement)),
+		registry:       newComponentRegistry(),
+		archetypes:     newPagedSlice[archetype](),
+		nodes:          newPagedSlice[archNode](),
+		relationNodes:  []*archNode{},
+		locks:          lockMask{},
+		listener:       nil,
+		resources:      newResources(),
+		filterCache:    newCache(),
 	}
 	node := w.createArchetypeNode(Mask{}, -1)
 	w.createArchetype(node, Entity{}, false)
@@ -145,7 +151,7 @@ func (w *World) NewEntity(comps ...ID) Entity {
 	entity := w.createEntity(arch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -183,7 +189,7 @@ func (w *World) NewEntityWith(comps ...Component) Entity {
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -206,11 +212,11 @@ func (w *World) newEntityTarget(targetID ID, target Entity, comps ...ID) Entity
 	entity := w.createEntity(arch)
 
 	if !target.IsZero() {
-		w.entities[target.id].isTarget = true
+		w.targetEntities.Set(target.id, true)
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -235,7 +241,7 @@ func (w *World) newEntityTargetWith(targetID ID, target Entity, comps ...Compone
 	entity := w.createEntity(arch)
 
 	if !target.IsZero() {
-		w.entities[target.id].isTarget = true
+		w.targetEntities.Set(target.id, true)
 	}
 
 	for _, c := range comps {
@@ -243,7 +249,7 @@ func (w *World) newEntityTargetWith(targetID ID, target Entity, comps ...Compone
 	}
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+		w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 	}
 	return entity
 }
@@ -259,7 +265,7 @@ func (w *World) newEntities(count int, targetID int8, target Entity, comps ...ID
 		for i = 0; i < cnt; i++ {
 			idx := startIdx + i
 			entity := arch.GetEntity(uintptr(idx))
-			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, comps, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 		}
 	}
 
@@ -290,7 +296,7 @@ func (w *World) newEntitiesWith(count int, targetID int8, target Entity, comps .
 		for i = 0; i < cnt; i++ {
 			idx := startIdx + i
 			entity := arch.GetEntity(uintptr(idx))
-			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.graphNode.Ids, 1, Entity{}, arch.RelationTarget, false})
+			w.listener(&EntityEvent{entity, Mask{}, arch.Mask, ids, nil, arch.node.Ids, 1, Entity{}, arch.RelationTarget, false})
 		}
 	}
 
@@ -328,7 +334,7 @@ func (w *World) RemoveEntity(entity Entity) {
 
 	if w.listener != nil {
 		lock := w.lock()
-		w.listener(&EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.graphNode.Ids, nil, -1, oldArch.RelationTarget, Entity{}, false})
+		w.listener(&EntityEvent{entity, oldArch.Mask, Mask{}, nil, oldArch.node.Ids, nil, -1, oldArch.RelationTarget, Entity{}, false})
 		w.unlock(lock)
 	}
 
@@ -342,9 +348,9 @@ func (w *World) RemoveEntity(entity Entity) {
 	}
 	index.arch = nil
 
-	if index.isTarget {
+	if w.targetEntities.Get(entity.id) {
 		w.cleanupArchetypes(entity)
-		index.isTarget = false
+		w.targetEntities.Set(entity.id, false)
 	}
 
 	w.cleanupArchetype(oldArch)
@@ -362,11 +368,11 @@ func (w *World) removeEntities(filter Filter) int {
 	lock := w.lock()
 
 	var count uintptr
-	numNodes := w.graph.Len()
+	numNodes := w.nodes.Len()
 
 	var n int32
 	for n = 0; n < numNodes; n++ {
-		node := w.graph.Get(n)
+		node := w.nodes.Get(n)
 		if !node.IsActive {
 			continue
 		}
@@ -400,9 +406,9 @@ func (w *World) removeEntities(filter Filter) int {
 				index := &w.entities[entity.id]
 				index.arch = nil
 
-				if index.isTarget {
+				if w.targetEntities.Get(entity.id) {
 					w.cleanupArchetypes(entity)
-					index.isTarget = false
+					w.targetEntities.Set(entity.id, false)
 				}
 
 				w.entityPool.Recycle(entity)
@@ -619,12 +625,12 @@ func (w *World) exchange(entity Entity, add []ID, rem []ID, relation int8, targe
 		swapEntity := oldArch.GetEntity(index.index)
 		w.entities[swapEntity.id].index = index.index
 	}
-	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex, isTarget: index.isTarget}
+	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}
 
 	w.cleanupArchetype(oldArch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.graphNode.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, false})
+		w.listener(&EntityEvent{entity, oldMask, arch.Mask, add, rem, arch.node.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, false})
 	}
 }
 
@@ -785,13 +791,13 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 		return
 	}
 
-	arch := oldArch.graphNode.GetArchetype(target)
+	arch := oldArch.node.GetArchetype(target)
 	if arch == nil {
-		arch = w.createArchetype(oldArch.graphNode, target, true)
+		arch = w.createArchetype(oldArch.node, target, true)
 	}
 
 	newIndex := arch.Alloc(entity)
-	for _, id := range oldArch.graphNode.Ids {
+	for _, id := range oldArch.node.Ids {
 		comp := oldArch.Get(index.index, id)
 		arch.SetPointer(newIndex, id, comp)
 	}
@@ -802,13 +808,13 @@ func (w *World) setRelation(entity Entity, comp ID, target Entity) {
 		swapEntity := oldArch.GetEntity(index.index)
 		w.entities[swapEntity.id].index = index.index
 	}
-	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex, isTarget: index.isTarget}
-	w.entities[target.id].isTarget = true
+	w.entities[entity.id] = entityIndex{arch: arch, index: newIndex}
+	w.targetEntities.Set(target.id, true)
 
 	w.cleanupArchetype(oldArch)
 
 	if w.listener != nil {
-		w.listener(&EntityEvent{entity, arch.Mask, arch.Mask, nil, nil, arch.graphNode.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, true})
+		w.listener(&EntityEvent{entity, arch.Mask, arch.Mask, nil, nil, arch.node.Ids, 0, oldArch.RelationTarget, arch.RelationTarget, true})
 	}
 }
 
@@ -858,9 +864,9 @@ func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID,
 	}
 	oldIDs := oldArch.Components()
 
-	arch := oldArch.graphNode.GetArchetype(target)
+	arch := oldArch.node.GetArchetype(target)
 	if arch == nil {
-		arch = w.createArchetype(oldArch.graphNode, target, true)
+		arch = w.createArchetype(oldArch.node, target, true)
 	}
 
 	startIdx := uintptr(arch.Len())
@@ -893,7 +899,7 @@ func (w *World) setRelationArch(oldArch *archetype, oldArchLen uint32, comp ID,
 }
 
 func (w *World) checkRelation(arch *archetype, comp ID) {
-	if arch.graphNode.Relation != int8(comp) {
+	if arch.node.Relation != int8(comp) {
 		w.relationError(arch, comp)
 	}
 }
@@ -916,14 +922,15 @@ func (w *World) Reset() {
 	w.checkLocked()
 
 	w.entities = w.entities[:1]
+	w.targetEntities.Reset()
 	w.entityPool.Reset()
 	w.locks.Reset()
 	w.resources.reset()
 
-	len := w.graph.Len()
+	len := w.nodes.Len()
 	var i int32
 	for i = 0; i < len; i++ {
-		node := w.graph.Get(i)
+		node := w.nodes.Get(i)
 		if !node.IsActive {
 			continue
 		}
@@ -962,7 +969,7 @@ func (w *World) Query(filter Filter) Query {
 		return newArchesQuery(w, cached, l, archetypes)
 	}
 
-	return newQuery(w, filter, l, &w.graph, false)
+	return newQuery(w, filter, l, &w.nodes, false)
 }
 
 // Resources of the world.
@@ -1041,11 +1048,11 @@ func (w *World) Stats() *stats.WorldStats {
 	memory := cap(w.entities)*int(entityIndexSize) + w.entityPool.TotalCap()*int(entitySize)
 
 	cntOld := int32(len(w.stats.Nodes))
-	cntNew := int32(w.graph.Len())
+	cntNew := int32(w.nodes.Len())
 	cntActive := 0
 	var i int32
 	for i = 0; i < cntOld; i++ {
-		node := w.graph.Get(i)
+		node := w.nodes.Get(i)
 		nodeStats := &w.stats.Nodes[i]
 		node.UpdateStats(nodeStats, &w.registry)
 		if node.IsActive {
@@ -1054,7 +1061,7 @@ func (w *World) Stats() *stats.WorldStats {
 		}
 	}
 	for i = cntOld; i < cntNew; i++ {
-		node := w.graph.Get(i)
+		node := w.nodes.Get(i)
 		w.stats.Nodes = append(w.stats.Nodes, node.Stats(&w.registry))
 		if node.IsActive {
 			memory += w.stats.Nodes[i].Memory
@@ -1108,7 +1115,7 @@ func (w *World) newEntitiesNoNotify(count int, targetID int8, target Entity, com
 	if targetID >= 0 {
 		w.checkRelation(arch, uint8(targetID))
 		if !target.IsZero() {
-			w.entities[target.id].isTarget = true
+			w.targetEntities.Set(target.id, true)
 		}
 	}
 
@@ -1143,7 +1150,7 @@ func (w *World) newEntitiesWithNoNotify(count int, targetID int8, target Entity,
 	if targetID >= 0 {
 		w.checkRelation(arch, uint8(targetID))
 		if !target.IsZero() {
-			w.entities[target.id].isTarget = true
+			w.targetEntities.Set(target.id, true)
 		}
 	}
 
@@ -1172,10 +1179,13 @@ func (w *World) createEntity(arch *archetype) Entity {
 			old := w.entities
 			w.entities = make([]entityIndex, len, len+w.config.CapacityIncrement)
 			copy(w.entities, old)
+
 		}
 		w.entities = append(w.entities, entityIndex{arch: arch, index: idx})
+		w.targetEntities.ExtendTo(len + w.config.CapacityIncrement)
 	} else {
 		w.entities[entity.id] = entityIndex{arch: arch, index: idx}
+		w.targetEntities.Set(entity.id, false)
 	}
 	return entity
 }
@@ -1187,14 +1197,15 @@ func (w *World) createEntities(arch *archetype, count uint32) {
 
 	len := len(w.entities)
 	required := len + int(count) - w.entityPool.Available()
+	capacity := capacity(required, w.config.CapacityIncrement)
 	if required > cap(w.entities) {
-		cap := capacity(required, w.config.CapacityIncrement)
 		old := w.entities
-		w.entities = make([]entityIndex, required, cap)
+		w.entities = make([]entityIndex, required, capacity)
 		copy(w.entities, old)
 	} else if required > len {
 		w.entities = w.entities[:required]
 	}
+	w.targetEntities.ExtendTo(capacity)
 
 	var i uint32
 	for i = 0; i < count; i++ {
@@ -1202,6 +1213,7 @@ func (w *World) createEntities(arch *archetype, count uint32) {
 		entity := w.entityPool.Get()
 		arch.SetEntity(uintptr(idx), entity)
 		w.entities[entity.id] = entityIndex{arch: arch, index: uintptr(idx)}
+		w.targetEntities.Set(entity.id, false)
 	}
 }
 
@@ -1220,7 +1232,7 @@ func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 // searching by mask and extending the graph if necessary.
 // A new archetype is created for the final graph node if not already present.
 func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID, target Entity) *archetype {
-	curr := start.graphNode
+	curr := start.node
 	mask := start.Mask
 	relation := start.RelationComponent
 	for _, id := range rem {
@@ -1263,7 +1275,7 @@ func (w *World) findOrCreateArchetype(start *archetype, add []ID, rem []ID, targ
 
 // Tries to find an archetype for a mask, when it can't be reached through the archetype graph.
 // Creates an archetype graph node.
-func (w *World) findOrCreateArchetypeSlow(mask Mask, relation int8) (*archetypeNode, bool) {
+func (w *World) findOrCreateArchetypeSlow(mask Mask, relation int8) (*archNode, bool) {
 	if arch, ok := w.findArchetypeSlow(mask); ok {
 		return arch, false
 	}
@@ -1271,20 +1283,20 @@ func (w *World) findOrCreateArchetypeSlow(mask Mask, relation int8) (*archetypeN
 }
 
 // Searches for an archetype by a mask.
-func (w *World) findArchetypeSlow(mask Mask) (*archetypeNode, bool) {
-	length := w.graph.Len()
+func (w *World) findArchetypeSlow(mask Mask) (*archNode, bool) {
+	length := w.nodes.Len()
 	var i int32
 	for i = 0; i < length; i++ {
-		node := w.graph.Get(i)
-		if node.Mask == mask {
-			return node, true
+		nd := w.nodes.Get(i)
+		if nd.Mask == mask {
+			return nd, true
 		}
 	}
 	return nil, false
 }
 
 // Creates a node in the archetype graph.
-func (w *World) createArchetypeNode(mask Mask, relation int8) *archetypeNode {
+func (w *World) createArchetypeNode(mask Mask, relation int8) *archNode {
 	capInc := w.config.CapacityIncrement
 	if relation >= 0 {
 		capInc = w.config.RelationCapacityIncrement
@@ -1292,16 +1304,17 @@ func (w *World) createArchetypeNode(mask Mask, relation int8) *archetypeNode {
 
 	types := maskToTypes(mask, &w.registry)
 
-	w.graph.Add(newArchetypeNode(mask, relation, capInc, types))
-	node := w.graph.Get(w.graph.Len() - 1)
-	w.relationNodes = append(w.relationNodes, node)
-	return node
+	w.nodeData.Add(nodeData{})
+	w.nodes.Add(newArchNode(mask, w.nodeData.Get(w.nodeData.Len()-1), relation, capInc, types))
+	nd := w.nodes.Get(w.nodes.Len() - 1)
+	w.relationNodes = append(w.relationNodes, nd)
+	return nd
 }
 
 // Creates an archetype for the given archetype graph node.
 // Initializes the archetype with a capacity according to CapacityIncrement if forStorage is true,
 // and with a capacity of 1 otherwise.
-func (w *World) createArchetype(node *archetypeNode, target Entity, forStorage bool) *archetype {
+func (w *World) createArchetype(node *archNode, target Entity, forStorage bool) *archetype {
 	var arch *archetype
 	if node.HasRelation() {
 		arch = node.CreateArchetype(target)
@@ -1320,10 +1333,10 @@ func (w *World) createArchetype(node *archetypeNode, target Entity, forStorage b
 // Returns all archetypes that match the given filter. Used by [Cache].
 func (w *World) getArchetypes(filter Filter) archetypePointers {
 	arches := []*archetype{}
-	ln := w.graph.Len()
+	ln := w.nodes.Len()
 	var i int32
 	for i = 0; i < ln; i++ {
-		nd := w.graph.Get(i)
+		nd := w.nodes.Get(i)
 		if !nd.IsActive || !nd.Matches(filter) {
 			continue
 		}
@@ -1352,7 +1365,7 @@ func (w *World) getArchetypes(filter Filter) archetypePointers {
 
 // Removes the archetype if it is empty, and has a relation to a dead target.
 func (w *World) cleanupArchetype(arch *archetype) {
-	if arch.Len() > 0 || !arch.graphNode.HasRelation() {
+	if arch.Len() > 0 || !arch.node.HasRelation() {
 		return
 	}
 	target := arch.RelationTarget
@@ -1374,7 +1387,7 @@ func (w *World) cleanupArchetypes(target Entity) {
 
 // Removes/da-activates a relation archetype.
 func (w *World) removeArchetype(arch *archetype) {
-	arch.graphNode.RemoveArchetype(arch)
+	arch.node.RemoveArchetype(arch)
 	w.filterCache.removeArchetype(arch)
 }
 
@@ -1407,13 +1420,13 @@ func (w *World) notifyQuery(batchArch *batchArchetype) {
 	var i uintptr
 
 	event := EntityEvent{
-		Entity{}, Mask{}, arch.Mask, batchArch.Added, batchArch.Removed, arch.graphNode.Ids, 1,
+		Entity{}, Mask{}, arch.Mask, batchArch.Added, batchArch.Removed, arch.node.Ids, 1,
 		Entity{}, arch.RelationTarget, false,
 	}
 
 	oldArch := batchArch.OldArchetype
 	if oldArch != nil {
-		event.OldMask = oldArch.graphNode.Mask
+		event.OldMask = oldArch.node.Mask
 		event.AddedRemoved = 0
 		event.OldTarget = oldArch.RelationTarget
 		event.TargetChanged = event.OldMask == event.NewMask


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 6780236..34da468 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -13,7 +13,7 @@ func TestArchetype(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 32, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, false, Entity{})
 
@@ -71,7 +71,7 @@ func TestNewArchetype(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 32, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, true, Entity{})
 	assert.Equal(t, 32, int(arch.Cap()))
@@ -85,7 +85,7 @@ func TestNewArchetype(t *testing.T) {
 		{ID: 0, Type: reflect.TypeOf(Position{})},
 	}
 	assert.Panics(t, func() {
-		node := newArchetypeNode(All(0, 1), -1, 32, comps)
+		node := newArchNode(All(0, 1), &nodeData{}, -1, 32, comps)
 		arch := archetype{}
 		arch.Init(&node, 0, true, Entity{})
 	})
@@ -97,7 +97,7 @@ func TestArchetypeExtend(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 8, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 8, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, true, Entity{})
 
@@ -119,7 +119,7 @@ func TestArchetypeAlloc(t *testing.T) {
 		{ID: 0, Type: reflect.TypeOf(Position{})},
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
-	node := newArchetypeNode(All(0, 1), -1, 8, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 8, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, true, Entity{})
 
@@ -144,7 +144,7 @@ func TestArchetypeAddGetSet(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(label{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 1, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 1, comps)
 	a := archetype{}
 	a.Init(&node, 0, true, Entity{})
 
@@ -179,7 +179,7 @@ func TestArchetypeReset(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 32, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, false, Entity{})
 
@@ -225,7 +225,7 @@ func TestArchetypeZero(t *testing.T) {
 		{ID: 1, Type: reflect.TypeOf(rotation{})},
 	}
 
-	node := newArchetypeNode(All(0, 1), -1, 32, comps)
+	node := newArchNode(All(0, 1), &nodeData{}, -1, 32, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, false, Entity{})
 
@@ -257,7 +257,7 @@ func BenchmarkIterArchetype_1000(b *testing.B) {
 		{ID: 0, Type: reflect.TypeOf(testStruct0{})},
 	}
 
-	node := newArchetypeNode(All(0), -1, 32, comps)
+	node := newArchNode(All(0), &nodeData{}, -1, 32, comps)
 	arch := archetype{}
 	arch.Init(&node, 0, true, Entity{})
 
diff --git a/ecs/bitset_test.go b/ecs/bitset_test.go
new file mode 100644
index 0000000..a932b07
--- /dev/null
+++ b/ecs/bitset_test.go
@@ -0,0 +1,28 @@
+package ecs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBitSet(t *testing.T) {
+	b := bitSet{}
+
+	b.ExtendTo(64)
+	assert.Equal(t, 1, len(b.data))
+	b.ExtendTo(65)
+	assert.Equal(t, 2, len(b.data))
+	b.ExtendTo(120)
+	assert.Equal(t, 2, len(b.data))
+
+	assert.False(t, b.Get(127))
+	b.Set(127, true)
+	assert.True(t, b.Get(127))
+	b.Set(127, false)
+	assert.False(t, b.Get(127))
+
+	b.Set(63, true)
+	b.Reset()
+	assert.False(t, b.Get(63))
+}
diff --git a/ecs/world_test.go b/ecs/world_test.go
index c6c98e2..b5d6f80 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -25,8 +25,8 @@ func TestWorldConfig(t *testing.T) {
 	world.NewEntity()
 	world.NewEntity(relID)
 
-	assert.Equal(t, uint32(32), world.graph.Get(0).capacityIncrement)
-	assert.Equal(t, uint32(8), world.graph.Get(1).capacityIncrement)
+	assert.Equal(t, uint32(32), world.nodes.Get(0).capacityIncrement)
+	assert.Equal(t, uint32(8), world.nodes.Get(1).capacityIncrement)
 }
 
 func TestWorldEntites(t *testing.T) {
@@ -699,8 +699,8 @@ func TestWorldRelationSet(t *testing.T) {
 	e1 := world.NewEntity(relID, rotID)
 	e2 := world.NewEntity(relID, rotID)
 
-	assert.Equal(t, int32(3), world.graph.Len())
-	assert.Equal(t, int32(1), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(3), world.nodes.Len())
+	assert.Equal(t, int32(1), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	assert.Equal(t, Entity{}, world.Relations().Get(e1, relID))
@@ -709,8 +709,8 @@ func TestWorldRelationSet(t *testing.T) {
 
 	assert.Equal(t, targ, world.Relations().Get(e1, relID))
 	assert.Equal(t, targ, world.Relations().GetUnchecked(e1, relID))
-	assert.Equal(t, int32(3), world.graph.Len())
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(3), world.nodes.Len())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Relations().Set(e1, relID, Entity{})
@@ -724,7 +724,7 @@ func TestWorldRelationSet(t *testing.T) {
 	world.Relations().Set(e1, relID, Entity{})
 
 	assert.Equal(t, Entity{}, world.Relations().Get(e1, relID))
-	assert.Equal(t, int32(3), world.graph.Len())
+	assert.Equal(t, int32(3), world.nodes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Remove(e2, relID)
@@ -743,9 +743,9 @@ func TestWorldRelationSet(t *testing.T) {
 	world.RemoveEntity(targ)
 	assert.Panics(t, func() { world.Relations().Set(e3, relID, targ) })
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
-	assert.True(t, world.graph.Get(2).archetypes.Get(0).IsActive())
-	assert.False(t, world.graph.Get(2).archetypes.Get(1).IsActive())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
+	assert.True(t, world.nodes.Get(2).archetypes.Get(0).IsActive())
+	assert.False(t, world.nodes.Get(2).archetypes.Get(1).IsActive())
 
 	assert.Equal(t, 9, len(events))
 }
@@ -831,14 +831,14 @@ func TestWorldRelationRemove(t *testing.T) {
 	filter := RelationFilter(All(relID), targ)
 	world.Cache().Register(filter)
 
-	assert.Equal(t, int32(3), world.graph.Len())
-	assert.Equal(t, int32(1), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(3), world.nodes.Len())
+	assert.Equal(t, int32(1), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Relations().Set(e1, relID, targ)
 	world.Relations().Set(e2, relID, targ)
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.RemoveEntity(targ)
@@ -847,13 +847,13 @@ func TestWorldRelationRemove(t *testing.T) {
 	world.Relations().Set(e1, relID, Entity{})
 	world.Relations().Set(e2, relID, Entity{})
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Relations().Set(e1, relID, targ2)
 	world.Relations().Set(e2, relID, targ2)
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Relations().Set(e1, relID, Entity{})
@@ -867,16 +867,16 @@ func TestWorldRelationRemove(t *testing.T) {
 	world.Relations().Set(e1, relID, targ3)
 	world.Relations().Set(e2, relID, targ3)
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
-	assert.Equal(t, targ3, world.graph.Get(2).archetypes.Get(1).RelationTarget)
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
+	assert.Equal(t, targ3, world.nodes.Get(2).archetypes.Get(1).RelationTarget)
 	assert.Equal(t, int32(1), world.archetypes.Len())
 
 	world.Batch().RemoveEntities(All())
 	world.Batch().RemoveEntities(All())
 
-	assert.Equal(t, int32(2), world.graph.Get(2).archetypes.Len())
-	assert.True(t, world.graph.Get(2).archetypes.Get(0).IsActive())
-	assert.False(t, world.graph.Get(2).archetypes.Get(1).IsActive())
+	assert.Equal(t, int32(2), world.nodes.Get(2).archetypes.Len())
+	assert.True(t, world.nodes.Get(2).archetypes.Get(0).IsActive())
+	assert.False(t, world.nodes.Get(2).archetypes.Get(1).IsActive())
 }
 
 func TestWorldRelationQuery(t *testing.T) {
@@ -1362,17 +1362,17 @@ func TestArchetypeGraph(t *testing.T) {
 	archEmpty2 := world.findOrCreateArchetype(arch0, []ID{}, []ID{velID, posID}, Entity{})
 	assert.Equal(t, archEmpty, archEmpty2)
 	assert.Equal(t, int32(2), world.archetypes.Len())
-	assert.Equal(t, int32(3), world.graph.Len())
+	assert.Equal(t, int32(3), world.nodes.Len())
 
 	archEmpty3 := world.findOrCreateArchetype(arch0, []ID{}, []ID{posID, velID}, Entity{})
 	assert.Equal(t, archEmpty, archEmpty3)
 	assert.Equal(t, int32(2), world.archetypes.Len())
-	assert.Equal(t, int32(4), world.graph.Len())
+	assert.Equal(t, int32(4), world.nodes.Len())
 
 	arch01 := world.findOrCreateArchetype(arch0, []ID{velID}, []ID{}, Entity{})
 	arch012 := world.findOrCreateArchetype(arch01, []ID{rotID}, []ID{}, Entity{})
 
-	assert.Equal(t, []ID{0, 1, 2}, arch012.graphNode.Ids)
+	assert.Equal(t, []ID{0, 1, 2}, arch012.node.Ids)
 
 	archEmpty4 := world.findOrCreateArchetype(arch012, []ID{}, []ID{posID, rotID, velID}, Entity{})
 	assert.Equal(t, archEmpty, archEmpty4)
@@ -1598,7 +1598,8 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSizeName[pagedSlice[archetype]]("pagedArr32")
 	printTypeSize[archetype]()
 	printTypeSize[archetypeAccess]()
-	printTypeSize[archetypeNode]()
+	printTypeSize[archNode]()
+	printTypeSize[nodeData]()
 	printTypeSize[layout]()
 	printTypeSize[entityPool]()
 	printTypeSizeName[componentRegistry[ID]]("componentRegistry")