Repository: mlange-42/arche
Pull request #38 (2023-02-08 22:18:46)

Component exchange and assignment
Adds these methods to `World`:

```go
func (w *World) Exchange(entity Entity, add []ID, rem []ID)
func (w *World) Assign(entity Entity, id ID, comp interface{}) unsafe.Pointer
func (w *World) AssignN(entity Entity, comps ...Component)
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67bd4b9..63a7ee1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
 
 ## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.1.4...main)
 
+### Features
+
+* `World` has method `Exchange` to add and remove components in one go (#38)
+* `World` has method `Assign` add and assign components in one go (#38)
+* `World` has method `AssignN` add and assign multiple components in one go (#38)
+
 ### Other
 
 * Optimization of `Query` iteration, avoids allocations and makes it approx. 30% faster (#35)
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 63cdbcd..6147cba 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -7,8 +7,10 @@ import (
 
 // archetype represents an ECS archetype
 type archetype struct {
-	mask       Mask
-	ids        []ID
+	mask Mask
+	ids  []ID
+	// Indirection to avoid a fixed-size array of storages
+	// Increases access time by 50-100%
 	indices    [MaskTotalBits]uint8
 	entities   storage
 	components []storage
@@ -52,7 +54,7 @@ func (a *archetype) Get(index int, id ID) unsafe.Pointer {
 }
 
 // Add adds an entity with components to the archetype
-func (a *archetype) Add(entity Entity, components ...component) uint32 {
+func (a *archetype) Add(entity Entity, components ...Component) uint32 {
 	if len(components) != len(a.ids) {
 		panic("Invalid number of components")
 	}
@@ -103,3 +105,8 @@ func (a *archetype) HasComponent(id ID) bool {
 func (a *archetype) Len() uint32 {
 	return a.entities.Len()
 }
+
+// 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)
+}
diff --git a/ecs/storage.go b/ecs/storage.go
index 93700ba..c1995fe 100644
--- a/ecs/storage.go
+++ b/ecs/storage.go
@@ -97,7 +97,7 @@ func (s *storage) Remove(index uint32) bool {
 	return false
 }
 
-func (s *storage) set(index uint32, value interface{}) {
+func (s *storage) set(index uint32, value interface{}) unsafe.Pointer {
 	rValue := reflect.ValueOf(value)
 	dst := s.Get(index)
 	var src unsafe.Pointer
@@ -109,6 +109,7 @@ func (s *storage) set(index uint32, value interface{}) {
 	srcSlice := (*[math.MaxInt32]byte)(src)[:size:size]
 
 	copy(dstSlice, srcSlice)
+	return dst
 }
 
 func (s *storage) setPointer(index uint32, value unsafe.Pointer) {
diff --git a/ecs/types.go b/ecs/types.go
index c778cd9..83e2bb1 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -8,8 +8,8 @@ import (
 // ID defines the format for the components identifier
 type ID uint32
 
-// component is a component ID with a reference struct
-type component struct {
+// Component is a Component ID/Component pointer pair
+type Component struct {
 	ID
 	Component interface{}
 }
diff --git a/ecs/world.go b/ecs/world.go
index aa8740c..c5df03e 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -2,7 +2,6 @@ package ecs
 
 import (
 	"reflect"
-	"sort"
 	"unsafe"
 )
 
@@ -48,7 +47,6 @@ type World struct {
 // NewEntity returns a new or recycled [Entity].
 //
 // Panics when called on a locked world.
-//
 // Do not use during [Query] iteration!
 func (w *World) NewEntity() Entity {
 	w.checkLocked()
@@ -72,7 +70,6 @@ func (w *World) NewEntity() Entity {
 // RemEntity removes and recycles an [Entity].
 //
 // Panics when called on a locked world or for an already removed entity.
-//
 // Do not use during [Query] iteration!
 func (w *World) RemEntity(entity Entity) {
 	w.checkLocked()
@@ -117,47 +114,42 @@ func (w *World) Has(entity Entity, comp ID) bool {
 // Add adds components to an [Entity].
 //
 // Panics when called on a locked world or for an already removed entity.
-//
 // Do not use during [Query] iteration!
 func (w *World) Add(entity Entity, comps ...ID) {
-	w.checkLocked()
-
-	if len(comps) == 0 {
-		return
-	}
-	index := w.entities[entity.id]
-	oldArch := index.arch
-	mask := oldArch.mask
-	for _, comp := range comps {
-		mask.Set(comp, true)
-	}
-
-	oldIDs := oldArch.Components()
+	w.Exchange(entity, comps, []ID{})
+}
 
-	arch, ok := w.findArchetype(mask)
-	if !ok {
-		ids := append(oldIDs, comps...)
-		arch = w.createArchetype(ids...)
-	}
+// Assign assigns a component to an [Entity], using a given pointer for the content.
+// See also [World.AssignN].
+//
+// The passed component must be a pointer.
+// Returns a pointer to the assigned memory.
+// The passed in pointer is not a valid reference to that memory!
+//
+// Panics when called on a locked world or for an already removed entity.
+// Do not use during [Query] iteration!
+func (w *World) Assign(entity Entity, id ID, comp interface{}) unsafe.Pointer {
+	w.Exchange(entity, []ID{id}, []ID{})
+	return w.copyTo(entity, id, comp)
+}
 
-	allComps := make([]componentPointer, 0, len(oldIDs)+len(comps))
-	for _, id := range oldIDs {
-		comp := oldArch.Get(int(index.index), id)
-		allComps = append(allComps, componentPointer{id, comp})
-	}
-	for _, id := range comps {
-		allComps = append(allComps, componentPointer{id, nil})
+// AssignN assigns multiple components to an [Entity], using pointers for the content.
+// See also [World.Assign].
+//
+// The passed components must be pointers.
+// The passed in pointers are no valid references to the assigned memory!
+//
+// Panics when called on a locked world or for an already removed entity.
+// Do not use during [Query] iteration!
+func (w *World) AssignN(entity Entity, comps ...Component) {
+	ids := make([]ID, len(comps))
+	for i, c := range comps {
+		ids[i] = c.ID
 	}
-
-	newIndex := arch.AddPointer(entity, allComps...)
-
-	swapped := oldArch.Remove(int(index.index))
-
-	if swapped {
-		swapEntity := oldArch.GetEntity(int(index.index))
-		w.entities[swapEntity.id].index = index.index
+	w.Exchange(entity, ids, []ID{})
+	for _, c := range comps {
+		w.copyTo(entity, c.ID, c.Component)
 	}
-	w.entities[entity.id] = entityIndex{arch, newIndex}
 }
 
 // Remove removes components from an entity.
@@ -166,39 +158,61 @@ func (w *World) Add(entity Entity, comps ...ID) {
 //
 // Do not use during [Query] iteration!
 func (w *World) Remove(entity Entity, comps ...ID) {
+	w.Exchange(entity, []ID{}, comps)
+}
+
+// Exchange adds and removes components in one pass
+//
+// Panics when called on a locked world or for an already removed entity.
+// Do not use during [Query] iteration!
+func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	w.checkLocked()
 
-	if len(comps) == 0 {
+	if len(add) == 0 && len(rem) == 0 {
 		return
 	}
 	index := w.entities[entity.id]
 	oldArch := index.arch
 	mask := oldArch.mask
-	for _, comp := range comps {
+	for _, comp := range add {
+		if mask.Get(comp) {
+			panic("entity already has this component, can't add")
+		}
+		mask.Set(comp, true)
+	}
+	for _, comp := range rem {
+		if !mask.Get(comp) {
+			panic("entity does not have this component, can't remove")
+		}
 		mask.Set(comp, false)
 	}
 
 	oldIDs := oldArch.Components()
-	newIDs := make([]ID, 0, len(oldIDs))
+	keepIDs := make([]ID, 0, len(oldIDs))
 	for _, id := range oldIDs {
 		if mask.Get(id) {
-			newIDs = append(newIDs, id)
+			keepIDs = append(keepIDs, id)
 		}
 	}
-
-	arch, ok := w.findArchetype(mask)
-	if !ok {
-		arch = w.createArchetype(newIDs...)
+	addIDs := make([]ID, 0, len(add))
+	for _, id := range add {
+		if mask.Get(id) {
+			addIDs = append(addIDs, id)
+		}
 	}
 
-	allComps := make([]componentPointer, 0, len(newIDs))
-	for _, id := range newIDs {
+	arch := w.findOrCreateArchetype(mask)
+
+	allComps := make([]componentPointer, 0, len(keepIDs)+len(addIDs))
+	for _, id := range keepIDs {
 		comp := oldArch.Get(int(index.index), id)
 		allComps = append(allComps, componentPointer{id, comp})
 	}
+	for _, id := range addIDs {
+		allComps = append(allComps, componentPointer{id, nil})
+	}
 
 	newIndex := arch.AddPointer(entity, allComps...)
-
 	swapped := oldArch.Remove(int(index.index))
 
 	if swapped {
@@ -208,6 +222,22 @@ func (w *World) Remove(entity Entity, comps ...ID) {
 	w.entities[entity.id] = entityIndex{arch, newIndex}
 }
 
+func (w *World) copyTo(entity Entity, id ID, comp interface{}) unsafe.Pointer {
+	if !w.Has(entity, id) {
+		panic("can't copy component into entity that has no such component type")
+	}
+	index := w.entities[entity.id]
+	arch := index.arch
+	return arch.Set(index.index, id, comp)
+}
+
+func (w *World) findOrCreateArchetype(mask Mask) *archetype {
+	if arch, ok := w.findArchetype(mask); ok {
+		return arch
+	}
+	return w.createArchetype(mask)
+}
+
 func (w *World) findArchetype(mask Mask) (*archetype, bool) {
 	length := w.archetypes.Len()
 	for i := 0; i < length; i++ {
@@ -219,11 +249,17 @@ func (w *World) findArchetype(mask Mask) (*archetype, bool) {
 	return nil, false
 }
 
-func (w *World) createArchetype(comps ...ID) *archetype {
-	sort.Slice(comps, func(i, j int) bool { return comps[i] < comps[j] })
-	types := make([]componentType, len(comps))
-	for i, id := range comps {
-		types[i] = componentType{id, w.registry.types[id]}
+func (w *World) createArchetype(mask Mask) *archetype {
+	count := int(mask.TotalBitsSet())
+	types := make([]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]}
+			idx++
+		}
 	}
 	w.archetypes.Add(archetype{})
 	arch := w.archetypes.Get(w.archetypes.Len() - 1)


Test patch:
diff --git a/ecs/archetype_test.go b/ecs/archetype_test.go
index 6aac4bc..9b27cca 100644
--- a/ecs/archetype_test.go
+++ b/ecs/archetype_test.go
@@ -18,14 +18,14 @@ func TestArchetype(t *testing.T) {
 
 	arch.Add(
 		newEntity(0),
-		component{ID: 0, Component: &position{1, 2}},
-		component{ID: 1, Component: &rotation{3}},
+		Component{ID: 0, Component: &position{1, 2}},
+		Component{ID: 1, Component: &rotation{3}},
 	)
 
 	arch.Add(
 		newEntity(1),
-		component{ID: 0, Component: &position{4, 5}},
-		component{ID: 1, Component: &rotation{6}},
+		Component{ID: 0, Component: &position{4, 5}},
+		Component{ID: 1, Component: &rotation{6}},
 	)
 
 	assert.Equal(t, 2, int(arch.entities.Len()))
@@ -63,7 +63,7 @@ func TestArchetype(t *testing.T) {
 	assert.Panics(t, func() {
 		arch.Add(
 			newEntity(1),
-			component{ID: 0, Component: &position{4, 5}},
+			Component{ID: 0, Component: &position{4, 5}},
 		)
 	})
 
@@ -102,7 +102,7 @@ func BenchmarkArchetypeAccess_1000(b *testing.B) {
 	for i := 0; i < 1000; i++ {
 		arch.Add(
 			newEntity(i),
-			component{ID: 0, Component: &testStruct{}},
+			Component{ID: 0, Component: &testStruct{}},
 		)
 	}
 	b.StartTimer()
diff --git a/ecs/test_types.go b/ecs/test_types.go
index 39a327f..c8f37ed 100644
--- a/ecs/test_types.go
+++ b/ecs/test_types.go
@@ -5,6 +5,11 @@ type position struct {
 	Y int
 }
 
+type velocity struct {
+	X int
+	Y int
+}
+
 type rotation struct {
 	Angle int
 }
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 213ce87..5539a1a 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -97,6 +97,80 @@ func TestWorldComponents(t *testing.T) {
 	w.Remove(e0)
 }
 
+func TestWorldExchange(t *testing.T) {
+	w := NewWorld()
+
+	posID := ComponentID[position](&w)
+	velID := ComponentID[velocity](&w)
+	rotID := ComponentID[rotation](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+
+	w.Exchange(e0, []ID{posID}, []ID{})
+	w.Exchange(e1, []ID{posID, rotID}, []ID{})
+	w.Exchange(e2, []ID{rotID}, []ID{})
+
+	assert.True(t, w.Has(e0, posID))
+	assert.False(t, w.Has(e0, rotID))
+
+	assert.True(t, w.Has(e1, posID))
+	assert.True(t, w.Has(e1, rotID))
+
+	assert.False(t, w.Has(e2, posID))
+	assert.True(t, w.Has(e2, rotID))
+
+	w.Exchange(e2, []ID{posID}, []ID{})
+	assert.True(t, w.Has(e2, posID))
+	assert.True(t, w.Has(e2, rotID))
+
+	w.Exchange(e0, []ID{rotID}, []ID{posID})
+	assert.False(t, w.Has(e0, posID))
+	assert.True(t, w.Has(e0, rotID))
+
+	w.Exchange(e1, []ID{velID}, []ID{posID})
+	assert.False(t, w.Has(e1, posID))
+	assert.True(t, w.Has(e1, rotID))
+	assert.True(t, w.Has(e1, velID))
+
+	assert.Panics(t, func() { w.Exchange(e1, []ID{velID}, []ID{}) })
+	assert.Panics(t, func() { w.Exchange(e1, []ID{}, []ID{posID}) })
+}
+
+func TestWorldAssign(t *testing.T) {
+	w := NewWorld()
+
+	posID := ComponentID[position](&w)
+	velID := ComponentID[velocity](&w)
+	rotID := ComponentID[rotation](&w)
+
+	e0 := w.NewEntity()
+	e1 := w.NewEntity()
+
+	pos := (*position)(w.Assign(e0, posID, &position{2, 3}))
+	assert.Equal(t, 2, pos.X)
+	pos.X = 5
+
+	pos = (*position)(w.Get(e0, posID))
+	assert.Equal(t, 5, pos.X)
+
+	assert.Panics(t, func() { _ = (*position)(w.Assign(e0, posID, &position{2, 3})) })
+	assert.Panics(t, func() { _ = (*position)(w.copyTo(e1, posID, &position{2, 3})) })
+
+	e2 := w.NewEntity()
+	w.AssignN(e2,
+		Component{velID, &velocity{1, 2}},
+		Component{rotID, &rotation{3}},
+		Component{posID, &position{4, 5}},
+	)
+	assert.True(t, w.Has(e2, velID))
+	assert.True(t, w.Has(e2, rotID))
+	assert.True(t, w.Has(e2, posID))
+
+	pos = (*position)(w.Get(e2, posID))
+	assert.Equal(t, 4, pos.X)
+}
 func TestWorldGetComponents(t *testing.T) {
 	w := NewWorld()