Finish ECS ( I hope )
This commit is contained in:
parent
a18862a976
commit
f299540797
13 changed files with 204 additions and 30 deletions
4
go.mod
4
go.mod
|
@ -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
9
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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
155
internal/ecs/ecs_test.go
Normal 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])
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
0
internal/ecs/log/lastmud-2025-06-27_09-01-07.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-01-07.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-01-17.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-01-17.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-00.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-00.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-02.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-02.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-04.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-05-04.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-10-43.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-10-43.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-34-39.log
Normal file
0
internal/ecs/log/lastmud-2025-06-27_09-34-39.log
Normal 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() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue