Repository: mlange-42/arche
Pull request #150 (2023-03-23 23:46:44)

Move resource handling out of the world
An object to handle resources can now be obtained via `world.Resources()`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 625fe72..9eda4b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Breaking changes
 
 * Generic mappers do no longer return all components when creating entities or components (!145)
+* Resources API moved out of the world, to a helper to get by `World.Resources()` (!150)
 
 ### Features
 
diff --git a/ecs/resources.go b/ecs/resources.go
index c247a36..da7dfb2 100644
--- a/ecs/resources.go
+++ b/ecs/resources.go
@@ -5,13 +5,15 @@ import (
 	"reflect"
 )
 
-type resources struct {
+// Resources manage a world's resources.
+// Access is using [World.Resources].
+type Resources struct {
 	registry  componentRegistry[ResID]
 	resources []any
 }
 
-func newResources() resources {
-	return resources{
+func newResources() Resources {
+	return Resources{
 		registry:  newComponentRegistry(),
 		resources: make([]any, MaskTotalBits),
 	}
@@ -21,7 +23,9 @@ 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(id ResID, res any) {
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Add] for a generic variant.
+func (r *Resources) Add(id ResID, res any) {
 	if r.resources[id] != nil {
 		panic(fmt.Sprintf("Resource of ID %d was already added (type %v)", id, reflect.TypeOf(res)))
 	}
@@ -31,7 +35,9 @@ func (r *resources) Add(id ResID, res any) {
 // Remove removes a resource from the world.
 //
 // Panics if there is no resource of the given type.
-func (r *resources) Remove(id ResID) {
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Remove] for a generic variant.
+func (r *Resources) Remove(id ResID) {
 	if r.resources[id] == nil {
 		panic(fmt.Sprintf("Resource of ID %d is not present", id))
 	}
@@ -41,17 +47,21 @@ func (r *resources) Remove(id ResID) {
 // Get returns a pointer to the resource of the given type.
 //
 // Returns nil if there is no such resource.
-func (r *resources) Get(id ResID) interface{} {
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Get] for a generic variant.
+func (r *Resources) Get(id ResID) interface{} {
 	return r.resources[id]
 }
 
 // Has returns whether the world has the given resource.
-func (r *resources) Has(id ResID) bool {
+//
+// See also [github.com/mlange-42/arche/generic.Resource.Has] for a generic variant.
+func (r *Resources) Has(id ResID) bool {
 	return r.resources[id] != nil
 }
 
-// Reset removes all resources.
-func (r *resources) Reset() {
+// reset removes all resources.
+func (r *Resources) reset() {
 	for i := 0; i < len(r.resources); i++ {
 		r.resources[i] = nil
 	}
diff --git a/ecs/world.go b/ecs/world.go
index 02dca90..67c65f1 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -32,7 +32,7 @@ func ResourceID[T any](w *World) ResID {
 // and [github.com/mlange-42/arche/generic.Resource.Get] for a generic variant.
 // These methods are more than 20 times faster than the GetResource function.
 func GetResource[T any](w *World) *T {
-	return w.GetResource(ResourceID[T](w)).(*T)
+	return w.resources.Get(ResourceID[T](w)).(*T)
 }
 
 // AddResource adds a resource to the world.
@@ -42,7 +42,7 @@ func GetResource[T any](w *World) *T {
 // 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)
+	w.resources.Add(ResourceID[T](w), res)
 }
 
 // World is the central type holding [Entity] and component data, as well as resources.
@@ -56,7 +56,7 @@ type World struct {
 	registry   componentRegistry[ID]
 	locks      Mask
 	listener   func(e EntityEvent)
-	resources  resources
+	resources  Resources
 }
 
 // NewWorld creates a new [World] from an optional [Config].
@@ -447,40 +447,11 @@ func (w *World) Exchange(entity Entity, add []ID, rem []ID) {
 	}
 }
 
-// AddResource adds a resource to the world.
-// The resource should always be a pointer.
-//
-// Panics if there is already a resource of the given type.
-//
-// 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.
-//
-// Returns nil if there is no such resource.
-//
-// See also [github.com/mlange-42/arche/generic.Resource.Get] for a generic variant.
-func (w *World) GetResource(id ResID) interface{} {
-	return w.resources.Get(id)
-}
-
-// HasResource returns whether the world has the given resource type.
+// Resources of the world.
 //
-// See also [github.com/mlange-42/arche/generic.Resource.Has] for a generic variant.
-func (w *World) HasResource(id ResID) bool {
-	return w.resources.Has(id)
+// Resources are component-like data that is not associated to an entity, but unique to the world.
+func (w *World) Resources() *Resources {
+	return &w.resources
 }
 
 // Reset removes all entities and resources as well as the listener from the world.
@@ -495,7 +466,7 @@ func (w *World) Reset() {
 	w.entities = w.entities[:1]
 	w.entityPool.Reset()
 	w.bitPool.Reset()
-	w.resources.Reset()
+	w.resources.reset()
 
 	w.listener = nil
 
diff --git a/examples/resources/main.go b/examples/resources/main.go
index af8de6f..6e25eef 100644
--- a/examples/resources/main.go
+++ b/examples/resources/main.go
@@ -35,7 +35,7 @@ func run(w *ecs.World) {
 
 	for {
 		// Get the the TimeStep resource from the world
-		time := w.GetResource(timeStepID).(*TimeStep)
+		time := w.Resources().Get(timeStepID).(*TimeStep)
 
 		// Use the resource
 		time.Step++
diff --git a/generic/resource.go b/generic/resource.go
index d778f56..a48df46 100644
--- a/generic/resource.go
+++ b/generic/resource.go
@@ -15,7 +15,7 @@ type Resource[T any] struct {
 //
 // [Resource] provides a type-safe way to access a world resources.
 //
-// See also [ecs.World.GetResource], [ecs.World.HasResource] and [ecs.World.AddResource].
+// See also [ecs.World.Resources].
 func NewResource[T any](w *ecs.World) Resource[T] {
 	return Resource[T]{
 		id:    ecs.ResourceID[T](w),
@@ -32,32 +32,32 @@ func (g *Resource[T]) ID() ecs.ResID {
 //
 // Panics if there is already a resource of the given type.
 //
-// See also [ecs.World.AddResource].
+// See also [ecs.Resources.Add].
 func (g *Resource[T]) Add(res *T) {
-	g.world.AddResource(g.id, res)
+	g.world.Resources().Add(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].
+// See also [ecs.Resources.Remove].
 func (g *Resource[T]) Remove() {
-	g.world.RemoveResource(g.id)
+	g.world.Resources().Remove(g.id)
 }
 
 // Get gets the resource of the given type.
 //
 // Returns nil if there is no such resource.
 //
-// See also [ecs.World.GetResource].
+// See also [ecs.Resources.Get].
 func (g *Resource[T]) Get() *T {
-	return g.world.GetResource(g.id).(*T)
+	return g.world.Resources().Get(g.id).(*T)
 }
 
 // Has returns whether the world has the resource type.
 //
-// See also [ecs.World.HasResource].
+// See also [ecs.Resources.Has].
 func (g *Resource[T]) Has() bool {
-	return g.world.HasResource(g.id)
+	return g.world.Resources().Has(g.id)
 }


Test patch:
diff --git a/benchmark/arche/resources/arche_test.go b/benchmark/arche/resources/arche_test.go
index 62d859d..4705d09 100644
--- a/benchmark/arche/resources/arche_test.go
+++ b/benchmark/arche/resources/arche_test.go
@@ -12,14 +12,16 @@ func BenchmarkArcheGetResource(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
+	resources := w.Resources()
+
 	posID := ecs.ResourceID[c.Position](&w)
-	w.AddResource(posID, &c.Position{X: 1, Y: 2})
+	resources.Add(posID, &c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
 	var res *c.Position
 	for i := 0; i < b.N; i++ {
-		res = w.GetResource(posID).(*c.Position)
+		res = resources.Get(posID).(*c.Position)
 	}
 
 	_ = res
@@ -46,14 +48,16 @@ func BenchmarkArcheHasResource(b *testing.B) {
 	b.StopTimer()
 
 	w := ecs.NewWorld()
+	resources := w.Resources()
+
 	posID := ecs.ResourceID[c.Position](&w)
-	w.AddResource(posID, &c.Position{X: 1, Y: 2})
+	resources.Add(posID, &c.Position{X: 1, Y: 2})
 
 	b.StartTimer()
 
 	var res bool
 	for i := 0; i < b.N; i++ {
-		res = w.HasResource(posID)
+		res = resources.Has(posID)
 	}
 
 	_ = res
diff --git a/ecs/resources_test.go b/ecs/resources_test.go
index 5ef1bf9..1d661b8 100644
--- a/ecs/resources_test.go
+++ b/ecs/resources_test.go
@@ -53,7 +53,7 @@ func TestResourcesReset(t *testing.T) {
 	assert.True(t, ok)
 	assert.Equal(t, rotation{5}, *rot)
 
-	res.Reset()
+	res.reset()
 
 	assert.False(t, res.Has(posID))
 	assert.False(t, res.Has(rotID))
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 67e5fcb..7924726 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -552,27 +552,27 @@ func TestWorldResources(t *testing.T) {
 	posID := ResourceID[position](&w)
 	rotID := ResourceID[rotation](&w)
 
-	assert.False(t, w.HasResource(posID))
-	assert.Nil(t, w.GetResource(posID))
+	assert.False(t, w.Resources().Has(posID))
+	assert.Nil(t, w.Resources().Get(posID))
 
 	AddResource(&w, &position{1, 2})
 
-	assert.True(t, w.HasResource(posID))
-	pos, ok := w.GetResource(posID).(*position)
+	assert.True(t, w.Resources().Has(posID))
+	pos, ok := w.Resources().Get(posID).(*position)
 
 	assert.True(t, ok)
 	assert.Equal(t, position{1, 2}, *pos)
 
-	assert.Panics(t, func() { w.AddResource(posID, &position{1, 2}) })
+	assert.Panics(t, func() { w.Resources().Add(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) })
+	w.Resources().Add(rotID, &rotation{5})
+	assert.True(t, w.Resources().Has(rotID))
+	w.Resources().Remove(rotID)
+	assert.False(t, w.Resources().Has(rotID))
+	assert.Panics(t, func() { w.Resources().Remove(rotID) })
 }
 
 func TestRegisterComponents(t *testing.T) {
@@ -870,7 +870,7 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSize[bitPool]()
 	printTypeSize[Query]()
 	printTypeSize[archetypeIter]()
-	printTypeSize[resources]()
+	printTypeSize[Resources]()
 	printTypeSizeName[reflect.Value]("reflect.Value")
 }
 
@@ -895,7 +895,7 @@ func BenchmarkGetResource(b *testing.B) {
 
 	var res *position
 	for i := 0; i < b.N; i++ {
-		res = w.GetResource(posID).(*position)
+		res = w.Resources().Get(posID).(*position)
 	}
 
 	_ = res