Finish ECS ( I hope )

This commit is contained in:
Miroslav Vasilev 2025-06-27 09:45:55 +03:00
parent a18862a976
commit f299540797
13 changed files with 204 additions and 30 deletions

4
go.mod
View file

@ -8,5 +8,9 @@ require (
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

9
go.sum
View file

@ -1,8 +1,17 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -37,27 +37,27 @@ type Component interface {
Type() ComponentType Type() ComponentType
} }
type ComponentStorage[T Component] struct { type ComponentStorage struct {
forType ComponentType forType ComponentType
storage map[Entity]T storage map[Entity]Component
} }
func CreateComponentStorage[T Component](forType ComponentType) *ComponentStorage[T] { func CreateComponentStorage(forType ComponentType) *ComponentStorage {
return &ComponentStorage[T]{ return &ComponentStorage{
forType: forType, forType: forType,
storage: map[Entity]T{}, storage: map[Entity]Component{},
} }
} }
func (cs *ComponentStorage[T]) ComponentType() ComponentType { func (cs *ComponentStorage) ComponentType() ComponentType {
return cs.forType return cs.forType
} }
func (cs *ComponentStorage[T]) Entities() iter.Seq[Entity] { func (cs *ComponentStorage) Entities() iter.Seq[Entity] {
return maps.Keys(cs.storage) return maps.Keys(cs.storage)
} }
func (cs *ComponentStorage[T]) Query(query func(comp T) bool) iter.Seq[Entity] { func queryComponents(cs *ComponentStorage, query func(comp Component) bool) iter.Seq[Entity] {
return func(yield func(Entity) bool) { return func(yield func(Entity) bool) {
for k, v := range cs.storage { for k, v := range cs.storage {
if !query(v) { if !query(v) {
@ -71,20 +71,20 @@ func (cs *ComponentStorage[T]) Query(query func(comp T) bool) iter.Seq[Entity] {
} }
} }
func (cs *ComponentStorage[T]) Set(e Entity, component T) { func (cs *ComponentStorage) Set(e Entity, component Component) {
cs.storage[e] = component cs.storage[e] = component
} }
func (cs *ComponentStorage[T]) Get(e Entity) (component T, ok bool) { func (cs *ComponentStorage) Get(e Entity) (component Component, ok bool) {
component, ok = cs.storage[e] component, ok = cs.storage[e]
return return
} }
func (cs *ComponentStorage[T]) Delete(e Entity) { func (cs *ComponentStorage) Delete(e Entity) {
delete(cs.storage, e) delete(cs.storage, e)
} }
func (cs *ComponentStorage[T]) All() map[Entity]T { func (cs *ComponentStorage) All() map[Entity]Component {
return cs.storage return cs.storage
} }
@ -116,14 +116,14 @@ func (s *System) Execute(world *World, delta time.Duration) {
type World struct { type World struct {
systems []*System systems []*System
componentsByType map[ComponentType]any componentsByType map[ComponentType]*ComponentStorage
resources map[Resource]any resources map[Resource]any
} }
func CreateWorld() (world *World) { func CreateWorld() (world *World) {
world = &World{ world = &World{
systems: []*System{}, systems: []*System{},
componentsByType: map[ComponentType]any{}, componentsByType: map[ComponentType]*ComponentStorage{},
resources: map[Resource]any{}, resources: map[Resource]any{},
} }
@ -138,9 +138,7 @@ func (w *World) Tick(delta time.Duration) {
func DeleteEntity(world *World, entity Entity) { func DeleteEntity(world *World, entity Entity) {
for _, s := range world.componentsByType { for _, s := range world.componentsByType {
storage := s.(*ComponentStorage[Component]) s.Delete(entity)
storage.Delete(entity)
} }
} }
@ -180,13 +178,13 @@ func registerComponent[T Component](world *World, compType ComponentType) {
return return
} }
world.componentsByType[compType] = CreateComponentStorage[T](compType) world.componentsByType[compType] = CreateComponentStorage(compType)
} }
func SetComponent[T Component](world *World, entity Entity, component T) { func SetComponent[T Component](world *World, entity Entity, component T) {
registerComponent[T](world, component.Type()) registerComponent[T](world, component.Type())
compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T]) compStorage := world.componentsByType[component.Type()]
compStorage.Set(entity, component) compStorage.Set(entity, component)
} }
@ -194,7 +192,10 @@ func SetComponent[T Component](world *World, entity Entity, component T) {
func GetComponent[T Component](world *World, entity Entity) (component T, exists bool) { func GetComponent[T Component](world *World, entity Entity) (component T, exists bool) {
storage := GetComponentStorage[T](world) storage := GetComponentStorage[T](world)
return storage.Get(entity) val, exists := storage.Get(entity)
casted, castSuccess := val.(T)
return casted, (exists && castSuccess)
} }
func DeleteComponent[T Component](world *World, entity Entity) { func DeleteComponent[T Component](world *World, entity Entity) {
@ -203,14 +204,14 @@ func DeleteComponent[T Component](world *World, entity Entity) {
storage.Delete(entity) storage.Delete(entity)
} }
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) { func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) {
var zero T var zero T
compType := zero.Type() compType := zero.Type()
registerComponent[T](world, compType) registerComponent[T](world, compType)
return world.componentsByType[compType].(*ComponentStorage[T]) return world.componentsByType[compType]
} }
func IterateEntitiesWithComponent[T Component](world *World) iter.Seq[Entity] { func IterateEntitiesWithComponent[T Component](world *World) iter.Seq[Entity] {
@ -222,7 +223,16 @@ func IterateEntitiesWithComponent[T Component](world *World) iter.Seq[Entity] {
func QueryEntitiesWithComponent[T Component](world *World, query func(comp T) bool) iter.Seq[Entity] { func QueryEntitiesWithComponent[T Component](world *World, query func(comp T) bool) iter.Seq[Entity] {
storage := GetComponentStorage[T](world) storage := GetComponentStorage[T](world)
return storage.Query(query) return queryComponents(storage, func(comp Component) bool {
val, ok := comp.(T)
// Cast unsuccessful, assume failure
if !ok {
return false
}
return query(val)
})
} }
func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (entities []Entity) { func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (entities []Entity) {
@ -236,7 +246,7 @@ func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (
return return
} }
storage, ok := world.componentsByType[compType].(*ComponentStorage[Component]) storage, ok := world.componentsByType[compType]
// If we can't find the storage for this component, then it hasn't been used yet. // If we can't find the storage for this component, then it hasn't been used yet.
// Therefore, no entity could have all components requested. Return empty. // Therefore, no entity could have all components requested. Return empty.

155
internal/ecs/ecs_test.go Normal file
View file

@ -0,0 +1,155 @@
package ecs
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
type TestComponent struct {
Value string
}
func (tc TestComponent) Type() ComponentType {
return 1
}
func TestEntityCreation(t *testing.T) {
u := uuid.New()
entity := CreateEntity(u)
assert.Equal(t, u, entity.AsUUID())
newEntity := NewEntity()
assert.NotEqual(t, uuid.Nil, newEntity.AsUUID())
nilEntity := NilEntity()
assert.Equal(t, uuid.Nil, nilEntity.AsUUID())
}
func TestComponentStorage(t *testing.T) {
cs := CreateComponentStorage(1)
entity := NewEntity()
comp := TestComponent{"hello"}
cs.Set(entity, comp)
ret, ok := cs.Get(entity)
assert.True(t, ok)
assert.Equal(t, comp, ret)
cs.Delete(entity)
_, ok = cs.Get(entity)
assert.False(t, ok)
}
func TestSystemExecution(t *testing.T) {
called := false
s := CreateSystem("test", 0, func(world *World, delta time.Duration) error {
called = true
return nil
})
world := CreateWorld()
RegisterSystem(world, s)
world.Tick(time.Millisecond)
assert.True(t, called)
}
func TestWorldComponentLifecycle(t *testing.T) {
world := CreateWorld()
entity := NewEntity()
comp := TestComponent{"value"}
SetComponent(world, entity, comp)
ret, ok := GetComponent[TestComponent](world, entity)
assert.True(t, ok)
assert.Equal(t, comp, ret)
DeleteComponent[TestComponent](world, entity)
_, ok = GetComponent[TestComponent](world, entity)
assert.False(t, ok)
}
func TestWorldResourceManagement(t *testing.T) {
world := CreateWorld()
resourceKey := Resource("test_resource")
value := "some data"
SetResource(world, resourceKey, value)
ret, err := GetResource[string](world, resourceKey)
assert.NoError(t, err)
assert.Equal(t, value, ret)
RemoveResource(world, resourceKey)
_, err = GetResource[string](world, resourceKey)
assert.Error(t, err)
}
func TestEntityDeletion(t *testing.T) {
world := CreateWorld()
entity := NewEntity()
comp := TestComponent{"remove me"}
SetComponent(world, entity, comp)
DeleteEntity(world, entity)
_, ok := GetComponent[TestComponent](world, entity)
assert.False(t, ok)
}
func TestIterateEntitiesWithComponent(t *testing.T) {
world := CreateWorld()
entities := []Entity{
NewEntity(),
NewEntity(),
NewEntity(),
}
for _, e := range entities {
SetComponent(world, e, TestComponent{"iter"})
}
count := 0
for range IterateEntitiesWithComponent[TestComponent](world) {
count++
}
assert.Equal(t, len(entities), count)
}
func TestQueryEntitiesWithComponent(t *testing.T) {
world := CreateWorld()
match := NewEntity()
nonMatch := NewEntity()
SetComponent(world, match, TestComponent{"match"})
SetComponent(world, nonMatch, TestComponent{"skip"})
results := []Entity{}
for e := range QueryEntitiesWithComponent(world, func(c TestComponent) bool {
return c.Value == "match"
}) {
results = append(results, e)
}
assert.Len(t, results, 1)
assert.Equal(t, match, results[0])
}
type SecondComponent struct{ Flag bool }
func (SecondComponent) Type() ComponentType { return 2 }
func TestFindEntitiesWithComponents(t *testing.T) {
world := CreateWorld()
entity1 := NewEntity()
entity2 := NewEntity()
SetComponent(world, entity1, TestComponent{"a"})
SetComponent(world, entity1, SecondComponent{true})
SetComponent(world, entity2, TestComponent{"b"})
results := FindEntitiesWithComponents(world, 1, 2)
assert.Len(t, results, 1)
assert.Equal(t, entity1, results[0])
}

View file

@ -12,12 +12,6 @@ func newECSError(v ...any) *ecsError {
} }
} }
func newFormattedECSError(format string, v ...any) *ecsError {
return &ecsError{
err: fmt.Sprintf(format, v...),
}
}
func (err *ecsError) Error() string { func (err *ecsError) Error() string {
return err.err return err.err
} }

View file

@ -129,6 +129,8 @@ func (game *LastMUDGame) consumeOutputs() {
contents: contents.Contents, contents: contents.Contents,
}) })
} }
ecs.DeleteEntities(game.world.World, entities...)
} }
func (game *LastMUDGame) shutdown() { func (game *LastMUDGame) shutdown() {