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 (
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
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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
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/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/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
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 ComponentStorage[T Component] struct {
type ComponentStorage struct {
forType ComponentType
storage map[Entity]T
storage map[Entity]Component
}
func CreateComponentStorage[T Component](forType ComponentType) *ComponentStorage[T] {
return &ComponentStorage[T]{
func CreateComponentStorage(forType ComponentType) *ComponentStorage {
return &ComponentStorage{
forType: forType,
storage: map[Entity]T{},
storage: map[Entity]Component{},
}
}
func (cs *ComponentStorage[T]) ComponentType() ComponentType {
func (cs *ComponentStorage) ComponentType() ComponentType {
return cs.forType
}
func (cs *ComponentStorage[T]) Entities() iter.Seq[Entity] {
func (cs *ComponentStorage) Entities() iter.Seq[Entity] {
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) {
for k, v := range cs.storage {
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
}
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]
return
}
func (cs *ComponentStorage[T]) Delete(e Entity) {
func (cs *ComponentStorage) Delete(e Entity) {
delete(cs.storage, e)
}
func (cs *ComponentStorage[T]) All() map[Entity]T {
func (cs *ComponentStorage) All() map[Entity]Component {
return cs.storage
}
@ -116,14 +116,14 @@ func (s *System) Execute(world *World, delta time.Duration) {
type World struct {
systems []*System
componentsByType map[ComponentType]any
componentsByType map[ComponentType]*ComponentStorage
resources map[Resource]any
}
func CreateWorld() (world *World) {
world = &World{
systems: []*System{},
componentsByType: map[ComponentType]any{},
componentsByType: map[ComponentType]*ComponentStorage{},
resources: map[Resource]any{},
}
@ -138,9 +138,7 @@ func (w *World) Tick(delta time.Duration) {
func DeleteEntity(world *World, entity Entity) {
for _, s := range world.componentsByType {
storage := s.(*ComponentStorage[Component])
storage.Delete(entity)
s.Delete(entity)
}
}
@ -180,13 +178,13 @@ func registerComponent[T Component](world *World, compType ComponentType) {
return
}
world.componentsByType[compType] = CreateComponentStorage[T](compType)
world.componentsByType[compType] = CreateComponentStorage(compType)
}
func SetComponent[T Component](world *World, entity Entity, component T) {
registerComponent[T](world, component.Type())
compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T])
compStorage := world.componentsByType[component.Type()]
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) {
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) {
@ -203,14 +204,14 @@ func DeleteComponent[T Component](world *World, entity Entity) {
storage.Delete(entity)
}
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) {
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) {
var zero T
compType := zero.Type()
registerComponent[T](world, compType)
return world.componentsByType[compType].(*ComponentStorage[T])
return world.componentsByType[compType]
}
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] {
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) {
@ -236,7 +246,7 @@ func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (
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.
// 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 {
return err.err
}

View file

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