Repository: mlange-42/arche
Pull request #135 (2023-03-20 17:04:01)

Remove resources, Release v0.5.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53624bd..10ed944 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,13 @@
 # Changelog
 
-## [[unpublished]](https://github.com/mlange-42/arche/compare/v0.4.6...main)
+## [[v0.5.0]](https://github.com/mlange-42/arche/compare/v0.4.6...v0.5.0)
+
+Feature release. Does not break v0.4.x projects.
 
 ### Features
 
 * The World handles ECS resources, i.e. component-like global data (!132)
-* Adds generic access to world resources (!132)
+* Generic access to world resources (!132)
 
 ### Other
 
diff --git a/ecs/resources.go b/ecs/resources.go
index 14a41e3..1c63434 100644
--- a/ecs/resources.go
+++ b/ecs/resources.go
@@ -21,14 +21,21 @@ func newResources() resources {
 // The resource should always be a pointer.
 //
 // Panics if there is already a resource of the given type.
-func (r *resources) Add(res any) ResID {
-	tp := reflect.TypeOf(res).Elem()
-	id := r.registry.ComponentID(tp)
+func (r *resources) Add(id ResID, res any) {
 	if r.resources[id] != nil {
-		panic(fmt.Sprintf("Resource of type %v was already added", tp))
+		panic(fmt.Sprintf("Resource of ID %d was already added (type %v)", id, reflect.TypeOf(res)))
 	}
 	r.resources[id] = res
-	return id
+}
+
+// Remove removes a resource from the world.
+//
+// Panics if there is no resource of the given type.
+func (r *resources) Remove(id ResID) {
+	if r.resources[id] == nil {
+		panic(fmt.Sprintf("Resource of ID %d is not present", id))
+	}
+	r.resources[id] = nil
 }
 
 // Get returns a pointer to the resource of the given type.
diff --git a/ecs/world.go b/ecs/world.go
index aa4d068..1c46901 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -35,7 +35,17 @@ func GetResource[T any](w *World) *T {
 	return w.GetResource(ResourceID[T](w)).(*T)
 }
 
-// World is the central type holding [Entity] and component data.
+// AddResource adds a resource to the world.
+//
+// Panics if there is already such a resource.
+//
+// Uses reflection. For more efficient access, see [World.AddResource],
+// and [github.com/mlange-42/arche/generic.Resource.Add] for a generic variant.
+func AddResource[T any](w *World, res *T) {
+	w.AddResource(ResourceID[T](w), res)
+}
+
+// World is the central type holding [Entity] and component data, as well as resources.
 type World struct {
 	config     Config
 	entities   []entityIndex
@@ -401,12 +411,21 @@ func (w *World) SetListener(listener func(e EntityEvent)) {
 // AddResource adds a resource to the world.
 // The resource should always be a pointer.
 //
-// Returns the [ID] assigned to the resource type.
-// See also [ResourceID].
-//
 // Panics if there is already a resource of the given type.
-func (w *World) AddResource(res any) ResID {
-	return w.resources.Add(res)
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Add] for a generic variant.
+func (w *World) AddResource(id ResID, res any) {
+	w.resources.Add(id, res)
+}
+
+// RemoveResource removes a resource from the world.
+// The resource should always be a pointer.
+//
+// Panics if there is no resource of the given type.
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Remove] for a generic variant.
+func (w *World) RemoveResource(id ResID) {
+	w.resources.Remove(id)
 }
 
 // GetResource returns a pointer to the given resource type.
diff --git a/examples/resources/main.go b/examples/resources/main.go
index dba3932..af8de6f 100644
--- a/examples/resources/main.go
+++ b/examples/resources/main.go
@@ -21,7 +21,7 @@ func main() {
 	world := ecs.NewWorld()
 
 	// Add a resource to the world
-	world.AddResource(&TimeStep{0})
+	ecs.AddResource(&world, &TimeStep{0})
 
 	// Run the simulation
 	run(&world)
diff --git a/generic/resource.go b/generic/resource.go
index 73d1e08..d778f56 100644
--- a/generic/resource.go
+++ b/generic/resource.go
@@ -28,6 +28,24 @@ func (g *Resource[T]) ID() ecs.ResID {
 	return g.id
 }
 
+// Add adds a resource to the world.
+//
+// Panics if there is already a resource of the given type.
+//
+// See also [ecs.World.AddResource].
+func (g *Resource[T]) Add(res *T) {
+	g.world.AddResource(g.id, res)
+}
+
+// Remove removes a resource from the world.
+//
+// Panics if there is no resource of the given type.
+//
+// See also [ecs.World.RemoveResource].
+func (g *Resource[T]) Remove() {
+	g.world.RemoveResource(g.id)
+}
+
 // Get gets the resource of the given type.
 //
 // Returns nil if there is no such resource.


Test patch:
diff --git a/benchmark/arche/resources/arche_test.go b/benchmark/arche/resources/arche_test.go
index 00efa0e..62d859d 100644
--- a/benchmark/arche/resources/arche_test.go
+++ b/benchmark/arche/resources/arche_test.go
@@ -12,8 +12,8 @@ func BenchmarkArcheGetResource(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
-	w.AddResource(&c.Position{X: 1, Y: 2})
 	posID := ecs.ResourceID[c.Position](&w)
+	w.AddResource(posID, &c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
@@ -29,8 +29,8 @@ func BenchmarkArcheGetResourceGeneric(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
-	w.AddResource(&c.Position{X: 1, Y: 2})
 	mapper := generic.NewResource[c.Position](&w)
+	mapper.Add(&c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
@@ -46,8 +46,8 @@ func BenchmarkArcheHasResource(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
-	w.AddResource(&c.Position{X: 1, Y: 2})
 	posID := ecs.ResourceID[c.Position](&w)
+	w.AddResource(posID, &c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
@@ -63,8 +63,8 @@ func BenchmarkArcheHasResourceGeneric(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
-	w.AddResource(&c.Position{X: 1, Y: 2})
 	mapper := generic.NewResource[c.Position](&w)
+	mapper.Add(&c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index f183732..0529b56 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -422,11 +422,12 @@ func TestWorldResources(t *testing.T) {
 	w := NewWorld()
 
 	posID := ResourceID[position](&w)
+	rotID := ResourceID[rotation](&w)
 
 	assert.False(t, w.HasResource(posID))
 	assert.Nil(t, w.GetResource(posID))
 
-	w.AddResource(&position{1, 2})
+	AddResource(&w, &position{1, 2})
 
 	fmt.Println(w.resources.registry.Components)
 
@@ -436,10 +437,16 @@ func TestWorldResources(t *testing.T) {
 	assert.True(t, ok)
 	assert.Equal(t, position{1, 2}, *pos)
 
-	assert.Panics(t, func() { w.AddResource(&position{1, 2}) })
+	assert.Panics(t, func() { w.AddResource(posID, &position{1, 2}) })
 
 	pos = GetResource[position](&w)
 	assert.Equal(t, position{1, 2}, *pos)
+
+	w.AddResource(rotID, &rotation{5})
+	assert.True(t, w.HasResource(rotID))
+	w.RemoveResource(rotID)
+	assert.False(t, w.HasResource(rotID))
+	assert.Panics(t, func() { w.RemoveResource(rotID) })
 }
 
 func TestRegisterComponents(t *testing.T) {
@@ -640,7 +647,7 @@ func BenchmarkGetResource(b *testing.B) {
 	b.StopTimer()
 
 	w := NewWorld()
-	w.AddResource(&position{1, 2})
+	AddResource(&w, &position{1, 2})
 	posID := ResourceID[position](&w)
 
 	b.StartTimer()
@@ -657,7 +664,7 @@ func BenchmarkGetResourceShortcut(b *testing.B) {
 	b.StopTimer()
 
 	w := NewWorld()
-	w.AddResource(&position{1, 2})
+	AddResource(&w, &position{1, 2})
 
 	b.StartTimer()
 
diff --git a/generic/resource_test.go b/generic/resource_test.go
index ee5a8ea..087c44a 100644
--- a/generic/resource_test.go
+++ b/generic/resource_test.go
@@ -14,10 +14,13 @@ func TestGenericResource(t *testing.T) {
 	assert.Equal(t, ecs.ResourceID[testStruct0](&w), get.ID())
 
 	assert.False(t, get.Has())
-	w.AddResource(&testStruct0{100})
+	get.Add(&testStruct0{100})
 
 	assert.True(t, get.Has())
 	res := get.Get()
 
 	assert.Equal(t, testStruct0{100}, *res)
+
+	get.Remove()
+	assert.False(t, get.Has())
 }