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 (
|
||||
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
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/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=
|
||||
|
|
|
@ -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
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 {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
ecs.DeleteEntities(game.world.World, entities...)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) shutdown() {
|
||||
|
|
Loading…
Add table
Reference in a new issue