Repository: mlange-42/arche
Pull request #48 (2023-02-10 10:48:52)

Set/overwrite components
add generic and index-based ways to overwrite complete components


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 800577d..117560c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,10 @@
 
 ### Features
 
-* Generic queries, for type safety and ergonomics (#47)
-* Generic add, assign and remove, for type safety and ergonomics (#47)
-* Generic get and has, for ergonomics but slow (#47)
+* Added a layer of generic access as alternative for using component IDs, for type safety and ergonomics (#47, #48)
+  * Generic queries like `Query1[T]`, `Query2[T, U]`, ... (#47)
+  * Generic add, assign and remove (`Add[T]()`, `Add2[T, U](), ...`) (#47)
+  * Generic get, has, and set through component mapper `Map[T]` (#47)
 
 ### Other
 
diff --git a/ecs/generic.go b/ecs/generic.go
index 106bf2b..30a3b06 100644
--- a/ecs/generic.go
+++ b/ecs/generic.go
@@ -8,34 +8,49 @@ func ComponentID[T any](w *World) ID {
 	return w.componentID(tp)
 }
 
-// Getter provides a type-safe way to access components by entity ID.
+// Map provides a type-safe way to access a component type by entity ID.
 //
-// Create one with [NewGetter].
-type Getter[T any] struct {
+// Create one with [NewMap].
+type Map[T any] struct {
 	id    ID
 	world *World
 }
 
-// NewGetter creates a new [Getter] for a component type.
+// NewMap creates a new [Map] for a component type.
 //
-// Getter provides a type-safe way to access components by entity ID.
+// Map provides a type-safe way to access a component type by entity ID.
 //
-// See also [World.Get] and [World.Has].
-func NewGetter[T any](w *World) Getter[T] {
-	return Getter[T]{
+// 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,
 	}
 }
 
-func (g *Getter[T]) Get(entity Entity) *T {
+// 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))
 }
 
-func (g *Getter[T]) Has(entity Entity) bool {
+// 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].
diff --git a/ecs/world.go b/ecs/world.go
index 34edc5c..89e62b8 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -104,7 +104,7 @@ func (w *World) Alive(entity Entity) bool {
 // Returns `nil` if the entity has no such component.
 // Panics when called for an already removed entity.
 //
-// See also [Getter]/[NewGetter].
+// See also [Map.Get] for a generic variant.
 func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 	index := w.entities[entity.id]
 	arch := index.arch
@@ -120,7 +120,7 @@ func (w *World) Get(entity Entity, comp ID) unsafe.Pointer {
 //
 // Panics when called for an already removed entity.
 //
-// See also [Getter]/[NewGetter].
+// See also [Map.Has] for a generic variant.
 func (w *World) Has(entity Entity, comp ID) bool {
 	index := w.entities[entity.id]
 	return index.arch.HasComponent(comp)
@@ -152,6 +152,22 @@ func (w *World) Assign(entity Entity, id ID, comp interface{}) unsafe.Pointer {
 	return w.copyTo(entity, id, comp)
 }
 
+// Set overwrites a component for an [Entity], using a given pointer for the content.
+//
+// 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!
+//
+// Panics if the entity does not have a component of that type.
+//
+// See also [Map.Set] for a generic variant.
+func (w *World) Set(entity Entity, id ID, comp interface{}) unsafe.Pointer {
+	return w.copyTo(entity, id, comp)
+}
+
 // AssignN assigns multiple components to an [Entity], using pointers for the content.
 // See also [World.Assign].
 //


Test patch:
diff --git a/benchmark/iterate/arche_test.go b/benchmark/iterate/arche_test.go
index ac2a73b..dd23ac3 100644
--- a/benchmark/iterate/arche_test.go
+++ b/benchmark/iterate/arche_test.go
@@ -179,7 +179,7 @@ func runArcheWorldGeneric(b *testing.B, count int) {
 	posID := ecs.ComponentID[position](&world)
 	rotID := ecs.ComponentID[rotation](&world)
 
-	get := ecs.NewGetter[position](&world)
+	get := ecs.NewMap[position](&world)
 
 	for i := 0; i < count; i++ {
 		entity := world.NewEntity()
diff --git a/ecs/generic_test.go b/ecs/generic_test.go
index 4daf4be..eee599a 100644
--- a/ecs/generic_test.go
+++ b/ecs/generic_test.go
@@ -2,11 +2,33 @@ package ecs
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
+func TestGenericMap(t *testing.T) {
+	w := NewWorld()
+	get := NewMap[testStruct0](&w)
+
+	e0 := w.NewEntity()
+
+	Add[testStruct0](&w, e0)
+	has := get.Has(e0)
+	_ = get.Get(e0)
+	assert.True(t, has)
+
+	_ = get.Set(e0, &testStruct0{100})
+	str := get.Get(e0)
+
+	assert.Equal(t, 100, int(str.val))
+
+	get2 := NewMap[testStruct1](&w)
+	assert.Panics(t, func() { get2.Set(e0, &testStruct1{}) })
+}
+
 func TestGenericAddRemove(t *testing.T) {
 	w := NewWorld()
-	get := NewGetter[testStruct0](&w)
+	get := NewMap[testStruct0](&w)
 
 	e0 := w.NewEntity()
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index ac104cd..ecdd546 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -169,7 +169,7 @@ func TestWorldExchange(t *testing.T) {
 	assert.Panics(t, func() { w.Exchange(e1, []ID{}, []ID{posID}) })
 }
 
-func TestWorldAssign(t *testing.T) {
+func TestWorldAssignSet(t *testing.T) {
 	w := NewWorld()
 
 	posID := ComponentID[position](&w)
@@ -201,6 +201,10 @@ func TestWorldAssign(t *testing.T) {
 
 	pos = (*position)(w.Get(e2, posID))
 	assert.Equal(t, 4, pos.X)
+
+	_ = (*position)(w.Set(e2, posID, &position{7, 8}))
+	pos = (*position)(w.Get(e2, posID))
+	assert.Equal(t, 7, pos.X)
 }
 func TestWorldGetComponents(t *testing.T) {
 	w := NewWorld()