├── .gitignore ├── LICENSE ├── README.md ├── archetype.go ├── archetype_test.go ├── deferredactions.go ├── entityid.go ├── example ├── helloecsgo │ ├── .gitignore │ └── helloecsgo.go └── snake │ └── main.go ├── executiongroup.go ├── executiongroup_test.go ├── go.mod ├── go.sum ├── observer.go ├── registry.go └── system.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vong Kong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECSGo 2 | ECSGo is an Entity Component System(ECS) in Go. 3 | This is made with Generic Go, so it needs Go 1.18 version 4 | 5 | - Cache friendly data storage 6 | - Run systems in concurrently with analyzing dependency tree. 7 | 8 | 9 | ## Example 10 | ```go 11 | package main 12 | 13 | import ( 14 | "context" 15 | "log" 16 | "time" 17 | 18 | "github.com/kongbong/ecsgo" 19 | ) 20 | 21 | type Position struct { 22 | X float32 23 | Y float32 24 | } 25 | 26 | type Velocity struct { 27 | X float32 28 | Y float32 29 | } 30 | 31 | type HP struct { 32 | Hp float32 33 | MaxHp float32 34 | } 35 | 36 | type EnemyTag struct{} 37 | 38 | func main() { 39 | registry := ecsgo.NewRegistry() 40 | 41 | sys1 := registry.AddSystem("VelocitySystem", 0, func(ctx *ecsgo.ExecutionContext) error { 42 | qr := ctx.GetQueryResult(0) 43 | log.Println("This system should have not any archtype", qr.GetArcheTypeCount()) 44 | return nil 45 | }) 46 | q1 := sys1.NewQuery() 47 | ecsgo.AddReadWriteComponent[Velocity](q1) 48 | ecsgo.AddExcludeComponent[EnemyTag](q1) 49 | 50 | o := registry.AddObserver("AddVelocityObserver", func(ctx *ecsgo.ObserverContext) error { 51 | vel := ecsgo.GetComponentObserver[Velocity](ctx) 52 | log.Println("This is one time called system", ctx.GetEntityId(), vel) 53 | return nil 54 | }) 55 | ecsgo.AddComponentToObserver[Velocity](o) 56 | 57 | sys2 := registry.AddSystem("VelocitySystem2", 0, func(ctx *ecsgo.ExecutionContext) error { 58 | qr := ctx.GetQueryResult(0) 59 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 60 | vel := ecsgo.GetComponentByAccessor[Velocity](accessor) 61 | log.Println("VelocitySystem2", accessor.GetEntityId(), vel) 62 | return nil 63 | }) 64 | return nil 65 | }) 66 | q2 := sys2.NewQuery() 67 | ecsgo.AddExcludeComponent[HP](q2) 68 | ecsgo.AddReadonlyComponent[Velocity](q2) 69 | 70 | sys3 := registry.AddSystem("PositionAndVelocity", 0, func(ctx *ecsgo.ExecutionContext) error { 71 | qr := ctx.GetQueryResult(0) 72 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 73 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 74 | vel := ecsgo.GetComponentByAccessor[Velocity](accessor) 75 | log.Println("Position, Velocity system", accessor.GetEntityId(), pos, vel, ctx.GetDeltaTime()) 76 | pos.X++ 77 | pos.Y++ 78 | vel.X++ 79 | vel.Y++ 80 | return nil 81 | }) 82 | return nil 83 | }) 84 | q3 := sys3.NewQuery() 85 | ecsgo.AddReadWriteComponent[Position](q3) 86 | ecsgo.AddReadWriteComponent[Velocity](q3) 87 | ecsgo.AddReadonlyComponent[EnemyTag](q3) 88 | 89 | entity := registry.CreateEntity() 90 | ecsgo.AddComponent(registry, entity, Position{10, 10}) 91 | ecsgo.AddComponent(registry, entity, Velocity{20, 20}) 92 | ecsgo.AddComponent(registry, entity, EnemyTag{}) 93 | 94 | ctx := context.Background() 95 | for i := 0; i < 10; i++ { 96 | registry.Tick(time.Second, ctx) 97 | time.Sleep(time.Second) 98 | } 99 | } 100 | ``` -------------------------------------------------------------------------------- /archetype.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "reflect" 5 | "slices" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type ArcheType struct { 11 | enitityIds []EntityId 12 | components map[reflect.Type]cmpInterface 13 | 14 | entityIdxMap map[EntityId]int 15 | 16 | debugComponentStr []string 17 | } 18 | 19 | type cmpInterface interface { 20 | onAddEntity(idx int) 21 | onRemoveEntity(idx, lastIdx int) 22 | copyDataToOtherArcheType(acc *ArcheTypeAccessor, other *ArcheType) error 23 | } 24 | 25 | func newArcheType(types ...reflect.Type) *ArcheType { 26 | a := &ArcheType{ 27 | components: make(map[reflect.Type]cmpInterface), 28 | entityIdxMap: make(map[EntityId]int), 29 | } 30 | for _, t := range types { 31 | a.components[t] = nil 32 | a.debugComponentStr = append(a.debugComponentStr, t.String()) 33 | } 34 | return a 35 | } 36 | 37 | func (a *ArcheType) getEntityCount() int { 38 | return len(a.enitityIds) 39 | } 40 | 41 | func (a *ArcheType) hasComponent(t reflect.Type) bool { 42 | _, found := a.components[t] 43 | return found 44 | } 45 | 46 | func (a *ArcheType) hasEntity(entityId EntityId) bool { 47 | _, found := a.entityIdxMap[entityId] 48 | return found 49 | } 50 | 51 | func HasArcheTypeComponent[T any](a *ArcheType) bool { 52 | var t T 53 | return a.hasComponent(reflect.TypeOf(t)) 54 | } 55 | 56 | func (a *ArcheType) getComponentTypeList() []reflect.Type { 57 | var typeList = make([]reflect.Type, len(a.components)) 58 | var i int 59 | for t := range a.components { 60 | typeList[i] = t 61 | } 62 | return typeList 63 | } 64 | 65 | func (a *ArcheType) equalComponents(types []reflect.Type) bool { 66 | if len(types) != len(a.components) { 67 | return false 68 | } 69 | for _, t := range types { 70 | _, found := a.components[t] 71 | if !found { 72 | return false 73 | } 74 | } 75 | return true 76 | } 77 | 78 | func (a *ArcheType) addEntity(entityId EntityId) { 79 | _, added := a.entityIdxMap[entityId] 80 | if added { 81 | // already added 82 | return 83 | } 84 | idx := len(a.enitityIds) 85 | a.enitityIds = append(a.enitityIds, entityId) 86 | a.entityIdxMap[entityId] = idx 87 | 88 | for _, v := range a.components { 89 | if v != nil { 90 | v.onAddEntity(idx) 91 | } 92 | } 93 | } 94 | 95 | func (a *ArcheType) removeEntity(entityId EntityId) { 96 | idx, found := a.entityIdxMap[entityId] 97 | if !found { 98 | // not added 99 | return 100 | } 101 | lastIdx := len(a.enitityIds) - 1 102 | lastEntityId := a.enitityIds[lastIdx] 103 | 104 | delete(a.entityIdxMap, entityId) 105 | 106 | if idx == lastIdx { 107 | // remove last 108 | a.enitityIds = a.enitityIds[:lastIdx] 109 | } else { 110 | // swap 111 | a.enitityIds[idx] = lastEntityId 112 | a.entityIdxMap[lastEntityId] = idx 113 | slices.Delete(a.enitityIds, lastIdx, lastIdx+1) 114 | } 115 | for _, v := range a.components { 116 | if v != nil { 117 | v.onRemoveEntity(idx, lastIdx) 118 | } 119 | } 120 | } 121 | 122 | func (a *ArcheType) copyDataToOtherArcheType(entityId EntityId, ty reflect.Type, other *ArcheType) error { 123 | cmp := a.components[ty] 124 | if cmp == nil { 125 | // no data 126 | return nil 127 | } 128 | return cmp.copyDataToOtherArcheType(a.GetAccessor(entityId), other) 129 | } 130 | 131 | type compData[T any] struct { 132 | arr []T 133 | } 134 | 135 | func newCompData[T any](size int) *compData[T] { 136 | return &compData[T]{ 137 | arr: make([]T, size), 138 | } 139 | } 140 | 141 | func (c *compData[T]) onAddEntity(idx int) { 142 | if idx != len(c.arr) { 143 | panic("added index should be same with length of array") 144 | } 145 | var t T 146 | c.arr = append(c.arr, t) 147 | } 148 | 149 | func (c *compData[T]) onRemoveEntity(idx, lastIdx int) { 150 | if lastIdx != len(c.arr)-1 { 151 | panic("lastIdx should be same with last index of array") 152 | } 153 | c.arr[idx] = c.arr[lastIdx] 154 | slices.Delete(c.arr, lastIdx, lastIdx+1) 155 | } 156 | 157 | func (c *compData[T]) copyDataToOtherArcheType(acc *ArcheTypeAccessor, other *ArcheType) error { 158 | otherAcc := other.GetAccessor(acc.entityId) 159 | if otherAcc == nil { 160 | return errors.Errorf("other Archetype doesn't have entityId %v", acc.entityId) 161 | } 162 | success := setArcheTypeComponentByIdx[T](other, otherAcc.idx, c.arr[acc.idx]) 163 | if !success { 164 | return errors.Errorf("failed to set archetype data") 165 | } 166 | return nil 167 | } 168 | 169 | func getArcheTypeComponent[T any](a *ArcheType, entityId EntityId) *T { 170 | idx, found := a.entityIdxMap[entityId] 171 | if !found { 172 | return nil 173 | } 174 | 175 | return getArcheTypeComponentByIdx[T](a, idx) 176 | } 177 | 178 | func getArcheTypeComponentByIdx[T any](a *ArcheType, idx int) *T { 179 | if idx < 0 || idx >= len(a.enitityIds) { 180 | return nil 181 | } 182 | 183 | var t T 184 | var cmpData *compData[T] 185 | v, found := a.components[reflect.TypeOf(t)] 186 | if !found { 187 | return nil 188 | } 189 | if v == nil { 190 | cmpData = newCompData[T](len(a.enitityIds)) 191 | a.components[reflect.TypeOf(t)] = cmpData 192 | } else { 193 | cmpData = v.(*compData[T]) 194 | } 195 | return &cmpData.arr[idx] 196 | } 197 | 198 | func setArcheTypeComponent[T any](a *ArcheType, entityId EntityId, value T) bool { 199 | idx, found := a.entityIdxMap[entityId] 200 | if !found { 201 | // entity is not added 202 | return false 203 | } 204 | return setArcheTypeComponentByIdx[T](a, idx, value) 205 | } 206 | 207 | func setArcheTypeComponentByIdx[T any](a *ArcheType, idx int, value T) bool { 208 | var cmpData *compData[T] 209 | v, found := a.components[reflect.TypeOf(value)] 210 | if !found { 211 | return false 212 | } 213 | if v == nil { 214 | cmpData = newCompData[T](len(a.enitityIds)) 215 | a.components[reflect.TypeOf(value)] = cmpData 216 | } else { 217 | cmpData = v.(*compData[T]) 218 | } 219 | cmpData.arr[idx] = value 220 | return true 221 | } 222 | 223 | type ArcheTypeAccessor struct { 224 | idx int 225 | entityId EntityId 226 | archeType *ArcheType 227 | } 228 | 229 | func (a *ArcheType) GetAccessor(entityId EntityId) *ArcheTypeAccessor { 230 | idx, found := a.entityIdxMap[entityId] 231 | if !found { 232 | return nil 233 | } 234 | return a.getAceessorByIdx(idx) 235 | } 236 | 237 | func (a *ArcheType) getAceessorByIdx(idx int) *ArcheTypeAccessor { 238 | if idx < 0 || idx >= len(a.enitityIds) { 239 | return nil 240 | } 241 | return &ArcheTypeAccessor{ 242 | idx: idx, 243 | entityId: a.enitityIds[idx], 244 | archeType: a, 245 | } 246 | } 247 | 248 | func (a *ArcheType) Foreach(fn func(accessor *ArcheTypeAccessor) error) error { 249 | for i := 0; i < len(a.enitityIds); i++ { 250 | err := fn(a.getAceessorByIdx(i)) 251 | if err != nil { 252 | return err 253 | } 254 | } 255 | return nil 256 | } 257 | 258 | func (acc *ArcheTypeAccessor) GetArcheType() *ArcheType { 259 | return acc.archeType 260 | } 261 | 262 | func (acc *ArcheTypeAccessor) GetEntityId() EntityId { 263 | return acc.entityId 264 | } 265 | 266 | func GetComponentByAccessor[T any](acc *ArcheTypeAccessor) *T { 267 | return getArcheTypeComponentByIdx[T](acc.archeType, acc.idx) 268 | } 269 | 270 | func SetComponentData[T any](acc *ArcheTypeAccessor, value T) bool { 271 | return setArcheTypeComponentByIdx[T](acc.archeType, acc.idx, value) 272 | } 273 | -------------------------------------------------------------------------------- /archetype_test.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type PositionComponent struct { 11 | X int 12 | Y int 13 | } 14 | 15 | type VelocityComponent struct { 16 | Vel int 17 | } 18 | 19 | func TestArcheType(t *testing.T) { 20 | a := newArcheType(reflect.TypeOf(PositionComponent{}), reflect.TypeOf(VelocityComponent{})) 21 | assert.NotNil(t, a) 22 | 23 | entityId := EntityId{ 24 | id: 1, 25 | version: 1, 26 | } 27 | a.addEntity(entityId) 28 | posComp := getArcheTypeComponent[PositionComponent](a, entityId) 29 | assert.NotNil(t, posComp) 30 | 31 | posComp.X = 100 32 | posComp.Y = 100 33 | 34 | entityId2 := EntityId{ 35 | id: 2, 36 | version: 2, 37 | } 38 | a.addEntity(entityId2) 39 | posComp2 := getArcheTypeComponent[PositionComponent](a, entityId2) 40 | assert.NotNil(t, posComp2) 41 | 42 | posComp2.X = 200 43 | posComp2.Y = 200 44 | 45 | posComp = getArcheTypeComponent[PositionComponent](a, entityId) 46 | assert.NotNil(t, posComp) 47 | 48 | assert.Equal(t, 100, posComp.X) 49 | assert.Equal(t, 100, posComp.Y) 50 | 51 | posComp2 = getArcheTypeComponent[PositionComponent](a, entityId2) 52 | assert.NotNil(t, posComp) 53 | 54 | assert.Equal(t, 200, posComp2.X) 55 | assert.Equal(t, 200, posComp2.Y) 56 | 57 | setArcheTypeComponent[VelocityComponent](a, entityId, VelocityComponent{ 58 | Vel: 100, 59 | }) 60 | 61 | velComp := getArcheTypeComponent[VelocityComponent](a, entityId) 62 | assert.NotNil(t, velComp) 63 | assert.Equal(t, 100, velComp.Vel) 64 | } 65 | -------------------------------------------------------------------------------- /deferredactions.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "reflect" 5 | "slices" 6 | "sync" 7 | ) 8 | 9 | type entityAction interface { 10 | modifyTypes(types, added, removed *[]reflect.Type) 11 | apply(entityId EntityId, a *ArcheType) 12 | } 13 | 14 | type deferredActions struct { 15 | r *Registry 16 | 17 | entityActions map[EntityId][]entityAction 18 | addSystemActions []*System 19 | addObserverActions []*Observer 20 | 21 | mx sync.Mutex 22 | } 23 | 24 | func newDeferredActions(r *Registry) *deferredActions { 25 | return &deferredActions{ 26 | r: r, 27 | entityActions: make(map[EntityId][]entityAction), 28 | } 29 | } 30 | 31 | type createEntityAction struct{} 32 | 33 | func (a *createEntityAction) modifyTypes(types, added, removed *[]reflect.Type) {} 34 | func (a *createEntityAction) apply(entityId EntityId, archeType *ArcheType) {} 35 | 36 | type removeEntityAction struct{} 37 | 38 | func (a *removeEntityAction) modifyTypes(types, added, removed *[]reflect.Type) {} 39 | func (a *removeEntityAction) apply(entityId EntityId, archeType *ArcheType) {} 40 | 41 | type addComponentAction[T any] struct { 42 | val T 43 | } 44 | 45 | func (a *addComponentAction[T]) modifyTypes(types, added, removed *[]reflect.Type) { 46 | var ty reflect.Type = reflect.TypeOf(a.val) 47 | *types = append(*types, ty) 48 | *added = append(*added, ty) 49 | } 50 | 51 | func (a *addComponentAction[T]) apply(entityId EntityId, archeType *ArcheType) { 52 | setArcheTypeComponent[T](archeType, entityId, a.val) 53 | } 54 | 55 | type removeComponentAction[T any] struct{} 56 | 57 | func (a *removeComponentAction[T]) modifyTypes(types, added, removed *[]reflect.Type) { 58 | var t T 59 | var ty reflect.Type = reflect.TypeOf(t) 60 | *types = slices.DeleteFunc(*types, func(t reflect.Type) bool { 61 | return ty == t 62 | }) 63 | *removed = append(*removed, ty) 64 | } 65 | 66 | func (a *removeComponentAction[T]) apply(entityId EntityId, archeType *ArcheType) { 67 | // nothing to change 68 | } 69 | 70 | func (d *deferredActions) createEntity(entityId EntityId) { 71 | d.mx.Lock() 72 | defer d.mx.Unlock() 73 | 74 | _, found := d.entityActions[entityId] 75 | if found { 76 | // create entity should be called at first 77 | panic("createEntity should be called at first") 78 | } 79 | d.entityActions[entityId] = []entityAction{&createEntityAction{}} 80 | } 81 | 82 | func (d *deferredActions) removeEntity(entityId EntityId) { 83 | d.mx.Lock() 84 | defer d.mx.Unlock() 85 | 86 | d.entityActions[entityId] = []entityAction{&removeEntityAction{}} 87 | } 88 | 89 | func addComponentDeferredAction[T any](d *deferredActions, entityId EntityId, val T) { 90 | d.mx.Lock() 91 | defer d.mx.Unlock() 92 | 93 | actions, found := d.entityActions[entityId] 94 | if !found { 95 | d.entityActions[entityId] = []entityAction{&addComponentAction[T]{val: val}} 96 | } 97 | // check if it is already removed 98 | if len(actions) == 1 { 99 | if _, ok := (actions)[0].(*removeEntityAction); ok { 100 | // it is already removed 101 | return 102 | } 103 | } 104 | 105 | actions = append(actions, &addComponentAction[T]{val: val}) 106 | d.entityActions[entityId] = actions 107 | } 108 | 109 | func removeComponentDeferredAction[T any](d *deferredActions, entityId EntityId) { 110 | d.mx.Lock() 111 | defer d.mx.Unlock() 112 | 113 | actions, found := d.entityActions[entityId] 114 | if !found { 115 | d.entityActions[entityId] = []entityAction{&removeComponentAction[T]{}} 116 | } 117 | // check if it is already removed 118 | if len(actions) == 1 { 119 | if _, ok := (actions)[0].(*removeEntityAction); ok { 120 | // it is already removed 121 | return 122 | } 123 | } 124 | 125 | actions = append(actions, &removeComponentAction[T]{}) 126 | d.entityActions[entityId] = actions 127 | } 128 | 129 | func (d *deferredActions) addSystem(sys *System) { 130 | d.mx.Lock() 131 | defer d.mx.Unlock() 132 | 133 | d.addSystemActions = append(d.addSystemActions, sys) 134 | } 135 | 136 | func (d *deferredActions) addObserver(o *Observer) { 137 | d.mx.Lock() 138 | defer d.mx.Unlock() 139 | 140 | d.addObserverActions = append(d.addObserverActions, o) 141 | } 142 | 143 | func (d *deferredActions) process() error { 144 | for _, o := range d.addObserverActions { 145 | d.r.addObserverSync(o) 146 | } 147 | d.addObserverActions = d.addObserverActions[:0] 148 | 149 | var err error 150 | for entityId, actions := range d.entityActions { 151 | if len(actions) == 0 { 152 | continue 153 | } 154 | if len(actions) == 1 { 155 | if _, ok := actions[0].(*removeEntityAction); ok { 156 | err = d.r.removeEntitySync(entityId) 157 | if err != nil { 158 | return err 159 | } 160 | } 161 | } 162 | 163 | err = d.r.processEntityActionSync(entityId, actions) 164 | if err != nil { 165 | return err 166 | } 167 | delete(d.entityActions, entityId) 168 | } 169 | 170 | for _, sys := range d.addSystemActions { 171 | d.r.addSystemSync(sys) 172 | } 173 | d.addSystemActions = d.addSystemActions[:0] 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /entityid.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | type EntityId struct { 4 | id uint32 5 | version uint32 6 | } 7 | 8 | func (e EntityId) NotNil() bool { 9 | return e.id != 0 && e.version != 0 10 | } 11 | -------------------------------------------------------------------------------- /example/helloecsgo/.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | helloecsgo 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /example/helloecsgo/helloecsgo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/kongbong/ecsgo" 9 | ) 10 | 11 | type Position struct { 12 | X float32 13 | Y float32 14 | } 15 | 16 | type Velocity struct { 17 | X float32 18 | Y float32 19 | } 20 | 21 | type HP struct { 22 | Hp float32 23 | MaxHp float32 24 | } 25 | 26 | type EnemyTag struct{} 27 | 28 | func main() { 29 | registry := ecsgo.NewRegistry() 30 | 31 | sys1 := registry.AddSystem("VelocitySystem", 0, func(ctx *ecsgo.ExecutionContext) error { 32 | qr := ctx.GetQueryResult(0) 33 | log.Println("This system should have not any archtype", qr.GetArcheTypeCount()) 34 | return nil 35 | }) 36 | q1 := sys1.NewQuery() 37 | ecsgo.AddReadWriteComponent[Velocity](q1) 38 | ecsgo.AddExcludeComponent[EnemyTag](q1) 39 | 40 | o := registry.AddObserver("AddVelocityObserver", func(ctx *ecsgo.ObserverContext) error { 41 | vel := ecsgo.GetComponentObserver[Velocity](ctx) 42 | log.Println("This is one time called system", ctx.GetEntityId(), vel) 43 | return nil 44 | }) 45 | ecsgo.AddComponentToObserver[Velocity](o) 46 | 47 | sys2 := registry.AddSystem("VelocitySystem2", 0, func(ctx *ecsgo.ExecutionContext) error { 48 | qr := ctx.GetQueryResult(0) 49 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 50 | vel := ecsgo.GetComponentByAccessor[Velocity](accessor) 51 | log.Println("VelocitySystem2", accessor.GetEntityId(), vel) 52 | return nil 53 | }) 54 | return nil 55 | }) 56 | q2 := sys2.NewQuery() 57 | ecsgo.AddExcludeComponent[HP](q2) 58 | ecsgo.AddReadonlyComponent[Velocity](q2) 59 | 60 | sys3 := registry.AddSystem("PositionAndVelocity", 0, func(ctx *ecsgo.ExecutionContext) error { 61 | qr := ctx.GetQueryResult(0) 62 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 63 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 64 | vel := ecsgo.GetComponentByAccessor[Velocity](accessor) 65 | log.Println("Position, Velocity system", accessor.GetEntityId(), pos, vel, ctx.GetDeltaTime()) 66 | pos.X++ 67 | pos.Y++ 68 | vel.X++ 69 | vel.Y++ 70 | return nil 71 | }) 72 | return nil 73 | }) 74 | q3 := sys3.NewQuery() 75 | ecsgo.AddReadWriteComponent[Position](q3) 76 | ecsgo.AddReadWriteComponent[Velocity](q3) 77 | ecsgo.AddReadonlyComponent[EnemyTag](q3) 78 | 79 | entity := registry.CreateEntity() 80 | ecsgo.AddComponent(registry, entity, Position{10, 10}) 81 | ecsgo.AddComponent(registry, entity, Velocity{20, 20}) 82 | ecsgo.AddComponent(registry, entity, EnemyTag{}) 83 | 84 | ctx := context.Background() 85 | for i := 0; i < 10; i++ { 86 | registry.Tick(time.Second, ctx) 87 | time.Sleep(time.Second) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /example/snake/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "image/color" 7 | "log" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/hajimehoshi/ebiten/v2" 12 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 13 | "github.com/hajimehoshi/ebiten/v2/inpututil" 14 | "github.com/hajimehoshi/ebiten/v2/vector" 15 | "github.com/kongbong/ecsgo" 16 | ) 17 | 18 | const ( 19 | screenWidth = 640 20 | screenHeight = 480 21 | gridSize = 10 22 | xNumInScreen = screenWidth / gridSize 23 | yNumInScreen = screenHeight / gridSize 24 | ) 25 | 26 | func main() { 27 | ebiten.SetWindowSize(screenWidth, screenHeight) 28 | ebiten.SetWindowTitle("Snake (Ebiten Demo)") 29 | g := newGame() 30 | if err := ebiten.RunGame(g); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | 35 | type EbitenGame struct { 36 | registry *ecsgo.Registry 37 | renderInfo RenderInfo 38 | ctx context.Context 39 | } 40 | 41 | func newGame() *EbitenGame { 42 | g := &EbitenGame{} 43 | g.ctx = context.Background() 44 | g.Reset() 45 | return g 46 | } 47 | 48 | var GlobalGameState ecsgo.EntityId 49 | 50 | func (g *EbitenGame) Reset() { 51 | g.registry = ecsgo.NewRegistry() 52 | 53 | sys1 := g.registry.AddSystem("directionSystem", 5, inputProcess) 54 | q := sys1.NewQuery() 55 | ecsgo.AddReadWriteComponent[Direction](q) 56 | 57 | sys2 := g.registry.AddSystem("MoveSystem", 4, move) 58 | q = sys2.NewQuery() 59 | ecsgo.AddReadWriteComponent[Direction](q) 60 | ecsgo.AddReadWriteComponent[Position](q) 61 | ecsgo.AddReadonlyComponent[Next](q) 62 | ecsgo.AddReadonlyComponent[Head](q) 63 | q2 := sys2.NewQuery() 64 | ecsgo.AddReadWriteComponent[GameState](q2) 65 | q3 := sys2.NewQuery() 66 | ecsgo.AddReadWriteComponent[Position](q3) 67 | ecsgo.AddReadonlyComponent[Next](q3) 68 | 69 | sys3 := g.registry.AddSystem("checkCollisionSystem", 3, checkCollision) 70 | q = sys3.NewQuery() 71 | ecsgo.AddReadonlyComponent[Position](q) 72 | ecsgo.AddReadWriteComponent[Collision](q) 73 | 74 | sys4 := g.registry.AddSystem("processCollision", 2, processCollsion) 75 | q = sys4.NewQuery() 76 | ecsgo.AddReadonlyComponent[Direction](q) 77 | ecsgo.AddReadWriteComponent[Collision](q) 78 | ecsgo.AddReadWriteComponent[Position](q) 79 | ecsgo.AddReadWriteComponent[Head](q) 80 | ecsgo.AddReadWriteComponent[Next](q) 81 | q2 = sys4.NewQuery() 82 | ecsgo.AddReadWriteComponent[GameState](q2) 83 | q3 = sys4.NewQuery() 84 | ecsgo.AddReadWriteComponent[Collision](q3) 85 | ecsgo.AddReadWriteComponent[Position](q3) 86 | ecsgo.AddOptionalReadWriteComponent[Next](q) 87 | ecsgo.AddOptionalReadonlyComponent[Apple](q3) 88 | 89 | sys5 := g.registry.AddSystem("checkGameOverSystem", 1, g.checkGameOver) 90 | q = sys5.NewQuery() 91 | ecsgo.AddReadonlyComponent[GameState](q) 92 | 93 | sys6 := g.registry.AddSystem("setRenders", 0, g.SetRenders) 94 | q = sys6.NewQuery() 95 | ecsgo.AddReadWriteComponent[Position](q) 96 | ecsgo.AddReadWriteComponent[Color](q) 97 | 98 | GlobalGameState = g.registry.CreateEntity() 99 | ecsgo.AddComponent[GameState](g.registry, GlobalGameState, GameState{ 100 | Speed: 4, 101 | Level: 1, 102 | Score: 0, 103 | }) 104 | 105 | // Add Snake Head 106 | snake := g.registry.CreateEntity() 107 | ecsgo.AddComponent[Position](g.registry, snake, Position{ 108 | X: xNumInScreen / 2, 109 | Y: yNumInScreen / 2, 110 | }) 111 | ecsgo.AddComponent[Direction](g.registry, snake, Direction{ 112 | Dir: None, 113 | }) 114 | ecsgo.AddComponent[Color](g.registry, snake, Color{ 115 | Color: color.RGBA{0x80, 0xa0, 0xc0, 0xff}, 116 | }) 117 | ecsgo.AddComponent[Collision](g.registry, snake, Collision{}) // Placeholder 118 | ecsgo.AddComponent[Next](g.registry, snake, Next{}) // Placehodler 119 | ecsgo.AddComponent[Head](g.registry, snake, Head{}) 120 | 121 | // Add Apple 122 | apple := g.registry.CreateEntity() 123 | ecsgo.AddComponent[Position](g.registry, apple, Position{ 124 | X: rand.Intn(xNumInScreen - 1), 125 | Y: rand.Intn(yNumInScreen - 1), 126 | }) 127 | ecsgo.AddComponent[Color](g.registry, apple, Color{ 128 | Color: color.RGBA{0xFF, 0x00, 0x00, 0xff}, 129 | }) 130 | ecsgo.AddComponent[Collision](g.registry, apple, Collision{}) // Placeholder 131 | ecsgo.AddComponent[Apple](g.registry, apple, Apple{}) 132 | } 133 | 134 | func (g *EbitenGame) Update() error { 135 | g.registry.Tick(100*time.Millisecond, g.ctx) 136 | return nil 137 | } 138 | 139 | func (g *EbitenGame) Draw(screen *ebiten.Image) { 140 | for _, v := range g.renderInfo.objs { 141 | vector.DrawFilledRect(screen, float32(v.X*gridSize), float32(v.Y*gridSize), gridSize, gridSize, v.Color, false) 142 | } 143 | ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f Level: %d Score: %d Best Score: %d", 144 | ebiten.ActualFPS(), g.renderInfo.level, g.renderInfo.score, g.renderInfo.bestScore)) 145 | } 146 | 147 | func (g *EbitenGame) Layout(outsideWidth, outsideHeight int) (int, int) { 148 | return screenWidth, screenHeight 149 | } 150 | 151 | type Position struct { 152 | X int 153 | Y int 154 | } 155 | 156 | const ( 157 | None = iota 158 | Up 159 | Down 160 | Left 161 | Right 162 | ) 163 | 164 | type Direction struct { 165 | Dir int 166 | ElapsedTime int 167 | } 168 | 169 | type Collision struct { 170 | Other ecsgo.EntityId 171 | X int 172 | Y int 173 | } 174 | 175 | type Next struct { 176 | Next ecsgo.EntityId 177 | } 178 | 179 | type Color struct { 180 | Color color.RGBA 181 | } 182 | 183 | type GameState struct { 184 | Speed int 185 | Level int 186 | Score int 187 | GameOver bool 188 | } 189 | 190 | type Head struct { 191 | Last ecsgo.EntityId 192 | } 193 | 194 | type Apple struct{} 195 | type Body struct{} 196 | 197 | type RenderInfo struct { 198 | objs []RenderObj 199 | score int 200 | level int 201 | bestScore int 202 | } 203 | 204 | type RenderObj struct { 205 | X int 206 | Y int 207 | Color color.Color 208 | } 209 | 210 | // Process Input to set Dir 211 | func inputProcess(ctx *ecsgo.ExecutionContext) error { 212 | qr := ctx.GetQueryResult(0) 213 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 214 | dir := ecsgo.GetComponentByAccessor[Direction](accessor) 215 | if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) || inpututil.IsKeyJustPressed(ebiten.KeyA) { 216 | if dir.Dir != Right { 217 | dir.Dir = Left 218 | } 219 | } else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) || inpututil.IsKeyJustPressed(ebiten.KeyD) { 220 | if dir.Dir != Left { 221 | dir.Dir = Right 222 | } 223 | } else if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { 224 | if dir.Dir != Up { 225 | dir.Dir = Down 226 | } 227 | } else if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { 228 | if dir.Dir != Down { 229 | dir.Dir = Up 230 | } 231 | } 232 | return nil 233 | }) 234 | return nil 235 | } 236 | 237 | // check position if there are collision then set collision values 238 | func checkCollision(ctx *ecsgo.ExecutionContext) error { 239 | colliders := make(map[int]map[int]ecsgo.EntityId) 240 | qr := ctx.GetQueryResult(0) 241 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 242 | self := accessor.GetEntityId() 243 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 244 | if colliders[pos.X] == nil { 245 | colliders[pos.X] = make(map[int]ecsgo.EntityId) 246 | } 247 | 248 | selfCollision := ecsgo.GetComponentByAccessor[Collision](accessor) 249 | if other, ok := colliders[pos.X][pos.Y]; ok { 250 | otherCollision := ecsgo.GetComponent[Collision](ctx, other) 251 | selfCollision.Other = other 252 | selfCollision.X = pos.X 253 | selfCollision.Y = pos.Y 254 | 255 | otherCollision.Other = self 256 | otherCollision.X = pos.X 257 | otherCollision.Y = pos.Y 258 | } else { 259 | colliders[pos.X][pos.Y] = self 260 | *selfCollision = Collision{} 261 | } 262 | return nil 263 | }) 264 | return nil 265 | } 266 | 267 | func move(ctx *ecsgo.ExecutionContext) error { 268 | var gameState *GameState 269 | qr := ctx.GetQueryResult(1) 270 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 271 | gameState = ecsgo.GetComponentByAccessor[GameState](accessor) 272 | return nil 273 | }) 274 | 275 | qr = ctx.GetQueryResult(0) 276 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 277 | dir := ecsgo.GetComponentByAccessor[Direction](accessor) 278 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 279 | next := ecsgo.GetComponentByAccessor[Next](accessor) 280 | 281 | if dir.ElapsedTime >= gameState.Speed { 282 | lastX := pos.X 283 | lastY := pos.Y 284 | 285 | // Move 286 | if dir.Dir != None { 287 | switch dir.Dir { 288 | case Up: 289 | pos.Y-- 290 | case Down: 291 | pos.Y++ 292 | case Left: 293 | pos.X-- 294 | case Right: 295 | pos.X++ 296 | } 297 | if pos.X < 0 || pos.Y < 0 || pos.X >= xNumInScreen || pos.Y >= yNumInScreen { 298 | // GameOver 299 | gameState.GameOver = true 300 | } else { 301 | 302 | for next.Next.NotNil() { 303 | nextPos := ecsgo.GetComponent[Position](ctx, next.Next) 304 | nextPos.X, lastX = lastX, nextPos.X 305 | nextPos.Y, lastY = lastY, nextPos.Y 306 | next = ecsgo.GetComponent[Next](ctx, next.Next) 307 | } 308 | } 309 | } 310 | dir.ElapsedTime = 0 311 | } else { 312 | dir.ElapsedTime++ 313 | } 314 | return nil 315 | }) 316 | return nil 317 | } 318 | 319 | func processCollsion(ctx *ecsgo.ExecutionContext) error { 320 | var gameState *GameState 321 | qr := ctx.GetQueryResult(1) 322 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 323 | gameState = ecsgo.GetComponentByAccessor[GameState](accessor) 324 | return nil 325 | }) 326 | 327 | qr = ctx.GetQueryResult(0) 328 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 329 | collision := ecsgo.GetComponentByAccessor[Collision](accessor) 330 | dir := ecsgo.GetComponentByAccessor[Direction](accessor) 331 | if dir.ElapsedTime != 0 { 332 | // only check after move 333 | return nil 334 | } 335 | if collision != nil && collision.Other.NotNil() { 336 | if ecsgo.HasComponent[Apple](ctx, collision.Other) { 337 | // it is apple, eat 338 | gameState.Score++ 339 | 340 | if gameState.Score > 20 { 341 | gameState.Level = 3 342 | gameState.Speed = 2 343 | } else if gameState.Score > 10 { 344 | gameState.Level = 2 345 | gameState.Speed = 3 346 | } 347 | 348 | applePos := ecsgo.GetComponent[Position](ctx, collision.Other) 349 | applePos.X = rand.Intn(xNumInScreen - 1) 350 | applePos.Y = rand.Intn(yNumInScreen - 1) 351 | 352 | head := ecsgo.GetComponentByAccessor[Head](accessor) 353 | tail := head.Last 354 | 355 | // add snake body 356 | snakeBody := ctx.CreateEntity() 357 | head.Last = snakeBody 358 | if tail.NotNil() { 359 | tailPos := ecsgo.GetComponent[Position](ctx, tail) 360 | next := ecsgo.GetComponent[Next](ctx, tail) 361 | ecsgo.AddComponent[Position](ctx.GetResgiry(), snakeBody, Position{ 362 | X: tailPos.X, 363 | Y: tailPos.Y, 364 | }) 365 | next.Next = snakeBody 366 | } else { 367 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 368 | next := ecsgo.GetComponentByAccessor[Next](accessor) 369 | ecsgo.AddComponent[Position](ctx.GetResgiry(), snakeBody, Position{ 370 | X: pos.X, 371 | Y: pos.Y, 372 | }) 373 | next.Next = snakeBody 374 | } 375 | 376 | ecsgo.AddComponent[Color](ctx.GetResgiry(), snakeBody, Color{ 377 | Color: color.RGBA{0x90, 0xb0, 0xd0, 0xff}, 378 | }) 379 | ecsgo.AddComponent[Collision](ctx.GetResgiry(), snakeBody, Collision{}) // Placeholder 380 | ecsgo.AddComponent[Next](ctx.GetResgiry(), snakeBody, Next{}) // Placehodler 381 | ecsgo.AddComponent[Body](ctx.GetResgiry(), snakeBody, Body{}) 382 | 383 | } else if ecsgo.HasComponent[Body](ctx, collision.Other) { 384 | // collision with Body 385 | // Gameover 386 | gameState.GameOver = true 387 | } 388 | } 389 | return nil 390 | }) 391 | return nil 392 | } 393 | 394 | func (g *EbitenGame) SetRenders(ctx *ecsgo.ExecutionContext) error { 395 | 396 | g.renderInfo.objs = g.renderInfo.objs[:0] 397 | qr := ctx.GetQueryResult(0) 398 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 399 | pos := ecsgo.GetComponentByAccessor[Position](accessor) 400 | col := ecsgo.GetComponentByAccessor[Color](accessor) 401 | 402 | g.renderInfo.objs = append(g.renderInfo.objs, RenderObj{ 403 | X: pos.X, 404 | Y: pos.Y, 405 | Color: col.Color, 406 | }) 407 | return nil 408 | }) 409 | return nil 410 | } 411 | 412 | func (g *EbitenGame) checkGameOver(ctx *ecsgo.ExecutionContext) error { 413 | qr := ctx.GetQueryResult(0) 414 | qr.ForeachEntities(func(accessor *ecsgo.ArcheTypeAccessor) error { 415 | gameState := ecsgo.GetComponentByAccessor[GameState](accessor) 416 | if gameState.GameOver { 417 | g.Reset() 418 | } else { 419 | g.renderInfo.score = gameState.Score 420 | g.renderInfo.level = gameState.Level 421 | if gameState.Score > g.renderInfo.bestScore { 422 | g.renderInfo.bestScore = gameState.Score 423 | } 424 | } 425 | return nil 426 | }) 427 | return nil 428 | } 429 | -------------------------------------------------------------------------------- /executiongroup.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "sync" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "golang.org/x/sync/errgroup" 11 | ) 12 | 13 | type executionGroup struct { 14 | executeList []*System 15 | 16 | depRootNode *depTreeNode 17 | dirty bool 18 | } 19 | 20 | func newExecutionGroup() *executionGroup { 21 | return &executionGroup{} 22 | } 23 | 24 | func (e *executionGroup) addSystem(sys *System) *executionGroup { 25 | e.dirty = true 26 | e.executeList = append(e.executeList, sys) 27 | return e 28 | } 29 | 30 | // dependent node 31 | type depNode struct { 32 | sys *System 33 | edges []*depNode 34 | } 35 | 36 | func (e *executionGroup) build() error { 37 | if !e.dirty { 38 | return nil 39 | } 40 | e.dirty = false 41 | 42 | slices.SortFunc(e.executeList, func(a, b *System) int { 43 | if a.GetPriority() == b.GetPriority() { 44 | return b.getInterestComponentCount() - a.getInterestComponentCount() 45 | } 46 | return a.GetPriority() - b.GetPriority() 47 | }) 48 | 49 | // make dependency graph 50 | nodes := make([]*depNode, len(e.executeList)) 51 | for i, sys := range e.executeList { 52 | nodes[i] = &depNode{sys: sys} 53 | } 54 | 55 | // double loop to make dependency graph 56 | for i := 0; i < len(nodes); i++ { 57 | for j := i + 1; j < len(nodes); j++ { 58 | if nodes[i].sys.dependent(nodes[j].sys) { 59 | nodes[i].edges = append(nodes[i].edges, nodes[j]) 60 | } 61 | } 62 | } 63 | 64 | // change dependency graph to dependency tree 65 | var err error 66 | e.depRootNode, err = changeToDependencyTree(nodes) 67 | return errors.Errorf("failed to change to dependency tree: %v", err) 68 | } 69 | 70 | // dependency tree node 71 | type depTreeNode struct { 72 | sys *System 73 | 74 | waitCount int 75 | wg sync.WaitGroup 76 | edges []*depTreeNode 77 | } 78 | 79 | func (n *depTreeNode) addEdge(edge *depTreeNode) { 80 | edge.wg.Add(1) 81 | edge.waitCount++ 82 | n.edges = append(n.edges, edge) 83 | } 84 | 85 | func changeToDependencyTree(nodes []*depNode) (*depTreeNode, error) { 86 | root := &depTreeNode{} 87 | 88 | resolved := make(map[*depNode]bool) 89 | depNodeMap := make(map[*depNode]*depTreeNode) 90 | 91 | for { 92 | if len(nodes) == 0 { 93 | return root, nil 94 | } 95 | 96 | unresolved := make(map[*depNode]bool) 97 | leastDepNode, err := depResolve(nodes[0], resolved, unresolved) 98 | if err != nil { 99 | return nil, errors.Errorf("failed to resolve dependency: %v", err) 100 | } 101 | 102 | treeNode := &depTreeNode{ 103 | sys: leastDepNode.sys, 104 | } 105 | depNodeMap[leastDepNode] = treeNode 106 | 107 | // inverse node 108 | if len(leastDepNode.edges) == 0 { 109 | root.addEdge(treeNode) 110 | } else { 111 | for _, edge := range leastDepNode.edges { 112 | edgeTreeNode := depNodeMap[edge] 113 | edgeTreeNode.addEdge(treeNode) 114 | } 115 | } 116 | 117 | // remove leastDepNode from node list 118 | nodes = slices.DeleteFunc(nodes, func(n *depNode) bool { 119 | return n == leastDepNode 120 | }) 121 | } 122 | } 123 | 124 | func depResolve(node *depNode, resolved, unresolved map[*depNode]bool) (*depNode, error) { 125 | unresolved[node] = true 126 | for _, edge := range node.edges { 127 | if !resolved[edge] { 128 | if unresolved[edge] { 129 | return nil, errors.Errorf("circular dependency detected: %v -> %v", node.sys, edge.sys) 130 | } 131 | return depResolve(edge, resolved, unresolved) 132 | } 133 | } 134 | resolved[node] = true 135 | delete(unresolved, node) 136 | return node, nil 137 | } 138 | 139 | func (e *executionGroup) execute(deltaTime time.Duration, ctx context.Context) error { 140 | err := e.build() 141 | if err != nil { 142 | return errors.Errorf("failed to build dependency tree %v", err) 143 | } 144 | errs, ctx := errgroup.WithContext(ctx) 145 | 146 | err = runTree(e.depRootNode, deltaTime, &sync.Map{}, errs, ctx) 147 | if err != nil { 148 | return errors.Errorf("failed to run dependency tree %v", err) 149 | } 150 | return errs.Wait() 151 | } 152 | 153 | // runTree - run dependency tree parallely 154 | func runTree(node *depTreeNode, deltaTime time.Duration, visited *sync.Map, errs *errgroup.Group, ctx context.Context) error { 155 | if ctx.Err() != nil { 156 | return ctx.Err() 157 | } 158 | 159 | node.wg.Wait() 160 | // reset wait group count for next round 161 | node.wg.Add(node.waitCount) 162 | 163 | var err error 164 | if node.sys != nil { 165 | err = node.sys.execute(deltaTime) 166 | } 167 | 168 | for _, edge := range node.edges { 169 | edge.wg.Done() 170 | } 171 | 172 | // shold return error after edge node waitGroup done for not starving edge 173 | if err != nil { 174 | return err 175 | } 176 | 177 | for _, edge := range node.edges { 178 | _, loaded := visited.LoadOrStore(edge, true) 179 | if !loaded { 180 | e := edge 181 | errs.Go(func() error { 182 | return runTree(e, deltaTime, visited, errs, ctx) 183 | }) 184 | } 185 | } 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /executiongroup_test.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type TestComponent1 struct { 14 | X int 15 | Y int 16 | } 17 | 18 | type TestComponent2 struct { 19 | V float64 20 | } 21 | 22 | type TestComponent3 struct { 23 | str string 24 | vec []int 25 | } 26 | 27 | type TestComponent4 struct { 28 | f1 float64 29 | f2 float64 30 | } 31 | 32 | func TestExecutionGroup(t *testing.T) { 33 | eg := newExecutionGroup() 34 | 35 | var executed [5]bool 36 | sys1 := newSystem(nil, "sys1", 0, func(ctx *ExecutionContext) error { 37 | executed[0] = true 38 | assert.True(t, executed[1]) 39 | assert.True(t, executed[2]) 40 | assert.Equal(t, 1, ctx.GetQueryResultCount()) 41 | qr := ctx.GetQueryResult(0) 42 | qr.ForeachEntities(func(accessor *ArcheTypeAccessor) error { 43 | id := accessor.GetEntityId() 44 | assert.Equal(t, uint32(1), id.id) 45 | assert.Equal(t, uint32(1), id.version) 46 | t1 := GetComponentByAccessor[TestComponent1](accessor) 47 | assert.NotNil(t, t1) 48 | t1.X = 200 49 | t1.Y = 200 50 | t2 := GetComponentByAccessor[TestComponent2](accessor) 51 | assert.NotNil(t, t2) 52 | assert.Equal(t, 3.1415, t2.V) 53 | t3 := GetComponentByAccessor[TestComponent3](accessor) 54 | assert.Nil(t, t3) 55 | var testT3 TestComponent3 56 | success := SetComponentData(accessor, testT3) 57 | assert.False(t, success) 58 | return nil 59 | }) 60 | t.Log("sys1 executed") 61 | time.Sleep(time.Duration(rand.Int31n(100)) * time.Millisecond) 62 | return nil 63 | }) 64 | q := sys1.NewQuery() 65 | AddReadWriteComponent[TestComponent1](q) 66 | AddOptionalReadWriteComponent[TestComponent2](q) 67 | 68 | sys2 := newSystem(nil, "sys2", 0, func(ctx *ExecutionContext) error { 69 | executed[1] = true 70 | assert.False(t, executed[0]) 71 | assert.Equal(t, 1, ctx.GetQueryResultCount()) 72 | qr := ctx.GetQueryResult(0) 73 | qr.ForeachEntities(func(accessor *ArcheTypeAccessor) error { 74 | id := accessor.GetEntityId() 75 | assert.Equal(t, uint32(1), id.id) 76 | assert.Equal(t, uint32(1), id.version) 77 | 78 | t2 := GetComponentByAccessor[TestComponent2](accessor) 79 | assert.NotNil(t, t2) 80 | t2.V = 3.1415 81 | return nil 82 | }) 83 | t.Log("sys2 executed") 84 | time.Sleep(time.Duration(rand.Int31n(100)) * time.Millisecond) 85 | return nil 86 | }) 87 | q2 := sys2.NewQuery() 88 | AddReadWriteComponent[TestComponent2](q2) 89 | 90 | sys3 := newSystem(nil, "sys3", 0, func(ctx *ExecutionContext) error { 91 | executed[2] = true 92 | assert.False(t, executed[4]) 93 | assert.Equal(t, 1, ctx.GetQueryResultCount()) 94 | qr := ctx.GetQueryResult(0) 95 | qr.ForeachEntities(func(accessor *ArcheTypeAccessor) error { 96 | id := accessor.GetEntityId() 97 | assert.Equal(t, uint32(2), id.id) 98 | assert.Equal(t, uint32(1), id.version) 99 | t3 := GetComponentByAccessor[TestComponent3](accessor) 100 | assert.NotNil(t, t3) 101 | t3.str = "TestTest" 102 | t3.vec = append(t3.vec, 1, 2, 3, 4, 5) 103 | return nil 104 | }) 105 | t.Log("sys3 executed") 106 | time.Sleep(time.Duration(rand.Int31n(100)) * time.Millisecond) 107 | return nil 108 | }) 109 | q3 := sys3.NewQuery() 110 | AddReadWriteComponent[TestComponent3](q3) 111 | AddExcludeComponent[TestComponent1](q3) 112 | 113 | sys4 := newSystem(nil, "sys4", 0, func(ctx *ExecutionContext) error { 114 | executed[3] = true 115 | assert.False(t, executed[4]) 116 | assert.Equal(t, 1, ctx.GetQueryResultCount()) 117 | qr := ctx.GetQueryResult(0) 118 | qr.ForeachEntities(func(accessor *ArcheTypeAccessor) error { 119 | id := accessor.GetEntityId() 120 | assert.Equal(t, uint32(2), id.id) 121 | assert.Equal(t, uint32(1), id.version) 122 | t4 := GetComponentByAccessor[TestComponent4](accessor) 123 | assert.NotNil(t, t4) 124 | t4.f1 = 1234.1234 125 | t4.f2 = 3.141516 126 | return nil 127 | }) 128 | t.Log("sys4 executed") 129 | time.Sleep(time.Duration(rand.Int31n(100)) * time.Millisecond) 130 | return nil 131 | }) 132 | q4 := sys4.NewQuery() 133 | AddReadWriteComponent[TestComponent4](q4) 134 | AddExcludeComponent[TestComponent1](q4) 135 | 136 | sys5 := newSystem(nil, "sys5", 0, func(ctx *ExecutionContext) error { 137 | executed[4] = true 138 | assert.True(t, executed[3]) 139 | assert.Equal(t, 1, ctx.GetQueryResultCount()) 140 | qr := ctx.GetQueryResult(0) 141 | qr.ForeachEntities(func(accessor *ArcheTypeAccessor) error { 142 | id := accessor.GetEntityId() 143 | assert.Equal(t, uint32(2), id.id) 144 | assert.Equal(t, uint32(1), id.version) 145 | t3 := GetComponentByAccessor[TestComponent3](accessor) 146 | assert.NotNil(t, t3) 147 | assert.Equal(t, "TestTest", t3.str) 148 | assert.Equal(t, []int{1, 2, 3, 4, 5}, t3.vec) 149 | t4 := GetComponentByAccessor[TestComponent4](accessor) 150 | assert.NotNil(t, t4) 151 | assert.Equal(t, 1234.1234, t4.f1) 152 | assert.Equal(t, 3.141516, t4.f2) 153 | return nil 154 | }) 155 | t.Log("sys5 executed") 156 | time.Sleep(time.Duration(rand.Int31n(100)) * time.Millisecond) 157 | return nil 158 | }) 159 | q5 := sys5.NewQuery() 160 | AddReadWriteComponent[TestComponent3](q5) 161 | AddReadWriteComponent[TestComponent4](q5) 162 | 163 | eg.addSystem(sys1) 164 | eg.addSystem(sys2) 165 | eg.addSystem(sys3) 166 | eg.addSystem(sys4) 167 | eg.addSystem(sys5) 168 | 169 | eg.build() 170 | 171 | var t1 TestComponent1 172 | var t2 TestComponent2 173 | var t3 TestComponent3 174 | var t4 TestComponent4 175 | a1 := newArcheType(reflect.TypeOf(t1), reflect.TypeOf(t2)) 176 | a2 := newArcheType(reflect.TypeOf(t3), reflect.TypeOf(t4)) 177 | 178 | a1.addEntity(EntityId{ 179 | id: 1, 180 | version: 1, 181 | }) 182 | a2.addEntity(EntityId{ 183 | id: 2, 184 | version: 1, 185 | }) 186 | 187 | added := sys1.addArcheTypeIfInterest(a1) 188 | assert.True(t, added) 189 | added = sys2.addArcheTypeIfInterest(a1) 190 | assert.True(t, added) 191 | added = sys3.addArcheTypeIfInterest(a1) 192 | assert.False(t, added) 193 | added = sys4.addArcheTypeIfInterest(a1) 194 | assert.False(t, added) 195 | added = sys5.addArcheTypeIfInterest(a1) 196 | assert.False(t, added) 197 | 198 | added = sys1.addArcheTypeIfInterest(a2) 199 | assert.False(t, added) 200 | added = sys2.addArcheTypeIfInterest(a2) 201 | assert.False(t, added) 202 | added = sys3.addArcheTypeIfInterest(a2) 203 | assert.True(t, added) 204 | added = sys4.addArcheTypeIfInterest(a2) 205 | assert.True(t, added) 206 | added = sys5.addArcheTypeIfInterest(a2) 207 | assert.True(t, added) 208 | 209 | // dependency tree should be 210 | // root 211 | // / \ \ 212 | // 2 3 4 213 | // | \ / 214 | // 1 5 215 | eg.execute(time.Second, context.Background()) 216 | for i := 0; i < 5; i++ { 217 | assert.True(t, executed[i]) 218 | executed[i] = false 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kongbong/ecsgo 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.6.6 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.8.4 9 | golang.org/x/sync v0.6.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/ebitengine/purego v0.6.0 // indirect 15 | github.com/jezek/xgb v1.1.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect 18 | golang.org/x/image v0.12.0 // indirect 19 | golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect 20 | golang.org/x/sys v0.12.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/ebitengine/purego v0.6.0 h1:Yo9uBc1x+ETQbfEaf6wcBsjrQfCEnh/gaGUg7lguEJY= 4 | github.com/ebitengine/purego v0.6.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 5 | github.com/hajimehoshi/ebiten/v2 v2.6.6 h1:E5X87Or4VwKZIKjeC9+Vr4ComhZAz9h839myF4Q21kc= 6 | github.com/hajimehoshi/ebiten/v2 v2.6.6/go.mod h1:gKgQI26zfoSb6j5QbrEz2L6nuHMbAYwrsXa5qsGrQKo= 7 | github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= 8 | github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 17 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 18 | golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg= 19 | golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= 20 | golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= 21 | golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= 22 | golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 h1:Q6NT8ckDYNcwmi/bmxe+XbiDMXqMRW1xFBtJ+bIpie4= 23 | golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4= 24 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 25 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 26 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 28 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 29 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 30 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 34 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 35 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 36 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 42 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 44 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 45 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 48 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 49 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 50 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 53 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 54 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 55 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 59 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /observer.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import "reflect" 4 | 5 | type ObserverContext struct { 6 | registry *Registry 7 | entityId EntityId 8 | archeType *ArcheType 9 | addedComponents []reflect.Type 10 | removedComponents []reflect.Type 11 | } 12 | 13 | type ObserverFunc func(ctx *ObserverContext) error 14 | 15 | type Observer struct { 16 | registry *Registry 17 | name string 18 | fn ObserverFunc 19 | 20 | addComponents map[reflect.Type]bool 21 | removeComponents map[reflect.Type]bool 22 | } 23 | 24 | func newObserver(registry *Registry, name string, fn ObserverFunc) *Observer { 25 | return &Observer{ 26 | registry: registry, 27 | name: name, 28 | fn: fn, 29 | } 30 | } 31 | 32 | func (o *Observer) GetName() string { 33 | return o.name 34 | } 35 | 36 | func AddComponentToObserver[T any](o *Observer) { 37 | var t T 38 | if o.addComponents == nil { 39 | o.addComponents = make(map[reflect.Type]bool) 40 | } 41 | o.addComponents[reflect.TypeOf(t)] = true 42 | } 43 | 44 | func RemoveComponentFromObserver[T any](o *Observer) { 45 | var t T 46 | if o.removeComponents == nil { 47 | o.removeComponents = make(map[reflect.Type]bool) 48 | } 49 | o.removeComponents[reflect.TypeOf(t)] = true 50 | } 51 | 52 | func (o *Observer) executeIfInterest(entityId EntityId, archeType *ArcheType, addedComponents, removedComponents []reflect.Type) error { 53 | interested, interestedAdd, interestedRemove := o.interestedIn(addedComponents, removedComponents) 54 | if interested { 55 | return o.execute(entityId, archeType, interestedAdd, interestedRemove) 56 | } 57 | return nil 58 | } 59 | 60 | func (o *Observer) interestedIn(addedComponents, removedComponents []reflect.Type) (interested bool, added []reflect.Type, removed []reflect.Type) { 61 | for _, t := range addedComponents { 62 | _, found := o.addComponents[t] 63 | if found { 64 | added = append(added, t) 65 | interested = true 66 | } 67 | } 68 | for _, t := range removedComponents { 69 | _, found := o.removeComponents[t] 70 | if found { 71 | removed = append(removed, t) 72 | interested = true 73 | } 74 | } 75 | return 76 | } 77 | 78 | func (o *Observer) execute(entityId EntityId, archeType *ArcheType, addedComponents, removedComponents []reflect.Type) error { 79 | return o.fn(&ObserverContext{ 80 | registry: o.registry, 81 | entityId: entityId, 82 | archeType: archeType, 83 | addedComponents: addedComponents, 84 | removedComponents: removedComponents, 85 | }) 86 | } 87 | 88 | func (ctx *ObserverContext) GetEntityId() EntityId { 89 | return ctx.entityId 90 | } 91 | 92 | func (ctx *ObserverContext) GetArcheType() *ArcheType { 93 | return ctx.archeType 94 | } 95 | 96 | func (ctx *ObserverContext) CreateEntity() EntityId { 97 | return ctx.registry.CreateEntity() 98 | } 99 | 100 | func GetComponentObserver[T any](ctx *ObserverContext) *T { 101 | return getArcheTypeComponent[T](ctx.archeType, ctx.entityId) 102 | } 103 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Registry struct { 14 | eg *executionGroup 15 | deferredActions *deferredActions 16 | 17 | archeTypeList []*ArcheType 18 | systems []*System 19 | observers []*Observer 20 | entityArcheTypeMap map[EntityId]*ArcheType 21 | 22 | duringTick int32 23 | 24 | // for issue new id 25 | mx sync.Mutex 26 | lastId uint32 27 | tombstones []EntityId 28 | } 29 | 30 | func NewRegistry() *Registry { 31 | r := &Registry{ 32 | eg: newExecutionGroup(), 33 | entityArcheTypeMap: make(map[EntityId]*ArcheType), 34 | } 35 | r.deferredActions = newDeferredActions(r) 36 | return r 37 | } 38 | 39 | func (r *Registry) CreateEntity() EntityId { 40 | r.mx.Lock() 41 | 42 | var entityId EntityId 43 | lastIdx := len(r.tombstones) - 1 44 | if lastIdx >= 0 { 45 | lastId := r.tombstones[lastIdx] 46 | r.tombstones[lastIdx] = EntityId{} 47 | r.tombstones = r.tombstones[:lastIdx] 48 | lastId.version++ 49 | entityId = lastId 50 | } else { 51 | r.lastId++ 52 | entityId = EntityId{ 53 | id: r.lastId, 54 | version: 1, 55 | } 56 | } 57 | // setting archetype, currently it is just for checking issued or not 58 | r.entityArcheTypeMap[entityId] = nil 59 | r.mx.Unlock() 60 | 61 | r.deferredActions.createEntity(entityId) 62 | return entityId 63 | } 64 | 65 | func (r *Registry) RemoveEntity(entityId EntityId) { 66 | r.deferredActions.removeEntity(entityId) 67 | } 68 | 69 | func AddComponent[T any](r *Registry, entityId EntityId, val T) { 70 | addComponentDeferredAction[T](r.deferredActions, entityId, val) 71 | } 72 | 73 | func RemoveComponent[T any](r *Registry, entityId EntityId) { 74 | removeComponentDeferredAction[T](r.deferredActions, entityId) 75 | } 76 | 77 | func (r *Registry) AddSystem(name string, priority int, fn SystemFn) *System { 78 | s := newSystem(r, name, priority, fn) 79 | r.deferredActions.addSystem(s) 80 | return s 81 | } 82 | 83 | func (r *Registry) AddObserver(name string, fn ObserverFunc) *Observer { 84 | o := newObserver(r, name, fn) 85 | r.deferredActions.addObserver(o) 86 | return o 87 | } 88 | 89 | func (r *Registry) IsActiveEntity(entityId EntityId) bool { 90 | _, found := r.entityArcheTypeMap[entityId] 91 | return found 92 | } 93 | 94 | func (r *Registry) Tick(deltaTime time.Duration, ctx context.Context) error { 95 | atomic.StoreInt32(&r.duringTick, 1) 96 | defer func() { 97 | atomic.StoreInt32(&r.duringTick, 0) 98 | }() 99 | 100 | err := r.processDeferredActions() 101 | if err != nil { 102 | return err 103 | } 104 | err = r.eg.execute(deltaTime, ctx) 105 | if err != nil { 106 | return err 107 | } 108 | // processDeferred again that process deferred actions while processing Systems 109 | return r.processDeferredActions() 110 | } 111 | 112 | func (r *Registry) processDeferredActions() error { 113 | return r.deferredActions.process() 114 | } 115 | 116 | func (r *Registry) addObserverSync(o *Observer) { 117 | r.observers = append(r.observers, o) 118 | } 119 | 120 | func (r *Registry) addSystemSync(s *System) { 121 | r.systems = append(r.systems, s) 122 | for _, a := range r.archeTypeList { 123 | s.addArcheTypeIfInterest(a) 124 | } 125 | r.eg.addSystem(s) 126 | } 127 | 128 | func (r *Registry) removeEntitySync(entityId EntityId) error { 129 | a := r.entityArcheTypeMap[entityId] 130 | if a != nil { 131 | a.removeEntity(entityId) 132 | } 133 | delete(r.entityArcheTypeMap, entityId) 134 | // it is threadsafe so don't need to lock because it is only called on deferredActions 135 | r.tombstones = append(r.tombstones, entityId) 136 | 137 | // call observers 138 | removed := a.getComponentTypeList() 139 | for _, o := range r.observers { 140 | err := o.executeIfInterest(entityId, a, nil, removed) 141 | if err != nil { 142 | return nil 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | func (r *Registry) processEntityActionSync(entityId EntityId, actions []entityAction) error { 149 | if len(actions) == 0 { 150 | return nil 151 | } 152 | 153 | var types []reflect.Type 154 | var added []reflect.Type 155 | var removed []reflect.Type 156 | archeType := r.entityArcheTypeMap[entityId] 157 | if archeType != nil { 158 | types = archeType.getComponentTypeList() 159 | } 160 | 161 | for _, action := range actions { 162 | action.modifyTypes(&types, &added, &removed) 163 | } 164 | 165 | targetArcheType := r.getOrMakeArcheTypeSync(types) 166 | if targetArcheType != nil { 167 | targetArcheType.addEntity(entityId) 168 | 169 | if archeType != nil && archeType != targetArcheType { 170 | // move component data from old to new archeType 171 | origtypes := archeType.getComponentTypeList() 172 | for _, t := range origtypes { 173 | if targetArcheType.hasComponent(t) { 174 | err := archeType.copyDataToOtherArcheType(entityId, t, targetArcheType) 175 | if err != nil { 176 | return errors.Errorf("failed to move data from old to new archetype %v", err) 177 | } 178 | } 179 | } 180 | } 181 | 182 | for _, action := range actions { 183 | action.apply(entityId, targetArcheType) 184 | } 185 | } 186 | if archeType != nil { 187 | archeType.removeEntity(entityId) 188 | } 189 | r.entityArcheTypeMap[entityId] = targetArcheType 190 | 191 | // call observers 192 | for _, o := range r.observers { 193 | err := o.executeIfInterest(entityId, targetArcheType, added, removed) 194 | if err != nil { 195 | return err 196 | } 197 | } 198 | 199 | return nil 200 | } 201 | 202 | func (r *Registry) getOrMakeArcheTypeSync(types []reflect.Type) *ArcheType { 203 | if len(types) == 0 { 204 | return nil 205 | } 206 | for _, a := range r.archeTypeList { 207 | if a.equalComponents(types) { 208 | return a 209 | } 210 | } 211 | newArcheType := newArcheType(types...) 212 | r.archeTypeList = append(r.archeTypeList, newArcheType) 213 | r.onAddArcheType(newArcheType) 214 | return newArcheType 215 | } 216 | 217 | func (r *Registry) onAddArcheType(archeType *ArcheType) { 218 | for _, s := range r.systems { 219 | s.addArcheTypeIfInterest(archeType) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package ecsgo 2 | 3 | import ( 4 | "reflect" 5 | "slices" 6 | "time" 7 | ) 8 | 9 | type ExecutionContext struct { 10 | registry *Registry 11 | deltaTime time.Duration 12 | 13 | queryResults []*QueryResult 14 | } 15 | 16 | type QueryResult struct { 17 | query *Query 18 | archeTypeList []*ArcheType 19 | } 20 | 21 | // Component Query 22 | type Query struct { 23 | includeComponents []reflect.Type 24 | excludeComponents []reflect.Type 25 | optionalComponents []reflect.Type 26 | readonlyComponents []reflect.Type 27 | atleastOneComponents [][]reflect.Type 28 | 29 | interestedArcheTypeList []*ArcheType 30 | } 31 | 32 | type SystemFn func(ctx *ExecutionContext) error 33 | 34 | type System struct { 35 | registry *Registry 36 | name string 37 | priority int 38 | fn SystemFn 39 | 40 | // query 41 | queries []*Query 42 | } 43 | 44 | func newSystem(registry *Registry, name string, priority int, fn SystemFn) *System { 45 | return &System{ 46 | registry: registry, 47 | name: name, 48 | priority: priority, 49 | fn: fn, 50 | } 51 | } 52 | 53 | func (s *System) execute(deltaTime time.Duration) error { 54 | ctx := &ExecutionContext{ 55 | registry: s.registry, 56 | deltaTime: deltaTime, 57 | } 58 | for _, q := range s.queries { 59 | qr := &QueryResult{ 60 | query: q, 61 | archeTypeList: q.interestedArcheTypeList, 62 | } 63 | ctx.queryResults = append(ctx.queryResults, qr) 64 | } 65 | return s.fn(ctx) 66 | } 67 | 68 | func (s *System) GetName() string { 69 | return s.name 70 | } 71 | 72 | func (s *System) GetPriority() int { 73 | return s.priority 74 | } 75 | 76 | func (s *System) NewQuery() *Query { 77 | q := &Query{} 78 | s.queries = append(s.queries, q) 79 | return q 80 | } 81 | 82 | func AddReadWriteComponent[T any](q *Query) { 83 | var t T 84 | ty := reflect.TypeOf(t) 85 | if q.hasComponent(ty) { 86 | // already added 87 | return 88 | } 89 | q.includeComponents = append(q.includeComponents, ty) 90 | } 91 | 92 | func AddReadonlyComponent[T any](q *Query) { 93 | var t T 94 | ty := reflect.TypeOf(t) 95 | if q.hasComponent(ty) { 96 | // already added 97 | return 98 | } 99 | q.includeComponents = append(q.includeComponents, ty) 100 | q.readonlyComponents = append(q.readonlyComponents, ty) 101 | } 102 | 103 | func AddExcludeComponent[T any](q *Query) { 104 | var t T 105 | ty := reflect.TypeOf(t) 106 | if q.hasComponent(ty) { 107 | // already added 108 | return 109 | } 110 | q.excludeComponents = append(q.excludeComponents, ty) 111 | } 112 | 113 | func AddOptionalReadWriteComponent[T any](q *Query) { 114 | var t T 115 | ty := reflect.TypeOf(t) 116 | if q.hasComponent(ty) { 117 | // already added 118 | return 119 | } 120 | q.optionalComponents = append(q.optionalComponents, ty) 121 | } 122 | 123 | func AddOptionalReadonlyComponent[T any](q *Query) { 124 | var t T 125 | ty := reflect.TypeOf(t) 126 | if q.hasComponent(ty) { 127 | // already added 128 | return 129 | } 130 | q.optionalComponents = append(q.optionalComponents, ty) 131 | q.readonlyComponents = append(q.readonlyComponents, ty) 132 | } 133 | 134 | func (q *Query) AtLeastOneOfThem(tps []reflect.Type) { 135 | q.atleastOneComponents = append(q.atleastOneComponents, tps) 136 | } 137 | 138 | func (q *Query) AtLeastOneOfThemReadonly(tps []reflect.Type) { 139 | q.atleastOneComponents = append(q.atleastOneComponents, tps) 140 | q.readonlyComponents = append(q.readonlyComponents, tps...) 141 | } 142 | 143 | func (s *System) hasComponent(ty reflect.Type) bool { 144 | for _, q := range s.queries { 145 | if q.hasComponent(ty) { 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | 152 | func (q *Query) hasComponent(ty reflect.Type) bool { 153 | if q.isInterestComponent(ty) { 154 | return true 155 | } 156 | if slices.Contains(q.excludeComponents, ty) { 157 | return true 158 | } 159 | return false 160 | } 161 | 162 | func (s *System) isInterestComponent(ty reflect.Type) bool { 163 | for _, q := range s.queries { 164 | if q.isInterestComponent(ty) { 165 | return true 166 | } 167 | } 168 | return false 169 | } 170 | 171 | func (q *Query) isInterestComponent(ty reflect.Type) bool { 172 | if slices.Contains(q.includeComponents, ty) { 173 | return true 174 | } 175 | if slices.Contains(q.optionalComponents, ty) { 176 | return true 177 | } 178 | for _, atleast := range q.atleastOneComponents { 179 | if slices.Contains(atleast, ty) { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | 186 | func (s *System) getInterestComponentCount() int { 187 | cnt := 0 188 | for _, q := range s.queries { 189 | cnt += q.getInterestComponentCount() 190 | } 191 | return cnt 192 | } 193 | 194 | func (q *Query) getInterestComponentCount() int { 195 | cnt := len(q.includeComponents) + len(q.optionalComponents) 196 | for _, atleast := range q.atleastOneComponents { 197 | cnt += len(atleast) 198 | } 199 | return cnt 200 | } 201 | 202 | func (s *System) dependent(other *System) bool { 203 | for _, q := range s.queries { 204 | for _, otherQ := range other.queries { 205 | if q.dependent(otherQ) { 206 | return true 207 | } 208 | } 209 | } 210 | return false 211 | } 212 | 213 | func (q *Query) dependent(other *Query) bool { 214 | for _, t := range q.includeComponents { 215 | if other.isInterestComponent(t) { 216 | if !slices.Contains(q.readonlyComponents, t) || !slices.Contains(other.readonlyComponents, t) { 217 | return true 218 | } 219 | } 220 | } 221 | for _, t := range q.optionalComponents { 222 | if other.isInterestComponent(t) { 223 | if !slices.Contains(q.readonlyComponents, t) || !slices.Contains(other.readonlyComponents, t) { 224 | return true 225 | } 226 | } 227 | } 228 | for _, atleast := range q.atleastOneComponents { 229 | for _, t := range atleast { 230 | if other.isInterestComponent(t) { 231 | if !slices.Contains(q.readonlyComponents, t) || !slices.Contains(other.readonlyComponents, t) { 232 | return true 233 | } 234 | } 235 | } 236 | } 237 | return false 238 | } 239 | 240 | func (s *System) addArcheTypeIfInterest(archeType *ArcheType) bool { 241 | var added bool 242 | for _, q := range s.queries { 243 | if q.addArcheTypeIfInterest(archeType) { 244 | added = true 245 | } 246 | } 247 | return added 248 | } 249 | 250 | func (q *Query) addArcheTypeIfInterest(archeType *ArcheType) bool { 251 | for _, t := range q.includeComponents { 252 | if !archeType.hasComponent(t) { 253 | return false 254 | } 255 | } 256 | for _, t := range q.excludeComponents { 257 | if archeType.hasComponent(t) { 258 | return false 259 | } 260 | } 261 | for _, atleast := range q.atleastOneComponents { 262 | found := false 263 | for _, t := range atleast { 264 | if archeType.hasComponent(t) { 265 | found = true 266 | break 267 | } 268 | } 269 | if !found { 270 | return false 271 | } 272 | } 273 | q.interestedArcheTypeList = append(q.interestedArcheTypeList, archeType) 274 | return true 275 | } 276 | 277 | func (c *ExecutionContext) GetResgiry() *Registry { 278 | return c.registry 279 | } 280 | 281 | func (c *ExecutionContext) CreateEntity() EntityId { 282 | return c.registry.CreateEntity() 283 | } 284 | 285 | func (c *ExecutionContext) GetDeltaTime() time.Duration { 286 | return c.deltaTime 287 | } 288 | 289 | func (c *ExecutionContext) GetQueryResultCount() int { 290 | return len(c.queryResults) 291 | } 292 | 293 | func (c *ExecutionContext) GetQueryResult(idx int) *QueryResult { 294 | return c.queryResults[idx] 295 | } 296 | 297 | func (qr *QueryResult) GetArcheTypeCount() int { 298 | return len(qr.archeTypeList) 299 | } 300 | 301 | func (qr *QueryResult) GetArcheType(idx int) *ArcheType { 302 | return qr.archeTypeList[idx] 303 | } 304 | 305 | func (qr *QueryResult) ForeachEntities(fn func(accessor *ArcheTypeAccessor) error) error { 306 | for _, archeType := range qr.archeTypeList { 307 | if archeType.getEntityCount() == 0 { 308 | continue 309 | } 310 | err := archeType.Foreach(func(accessor *ArcheTypeAccessor) error { 311 | err := fn(accessor) 312 | if err != nil { 313 | return err 314 | } 315 | return nil 316 | }) 317 | if err != nil { 318 | return err 319 | } 320 | } 321 | return nil 322 | } 323 | 324 | func GetComponent[T any](c *ExecutionContext, entityId EntityId) *T { 325 | var t T 326 | for i := 0; i < c.GetQueryResultCount(); i++ { 327 | qr := c.GetQueryResult(i) 328 | for j := 0; j < qr.GetArcheTypeCount(); j++ { 329 | a := qr.GetArcheType(j) 330 | if a == nil { 331 | continue 332 | } 333 | if a.hasComponent(reflect.TypeOf(t)) { 334 | valT := getArcheTypeComponent[T](a, entityId) 335 | if valT != nil { 336 | return valT 337 | } 338 | } 339 | } 340 | } 341 | return nil 342 | } 343 | 344 | func HasComponent[T any](c *ExecutionContext, entityId EntityId) bool { 345 | var t T 346 | for i := 0; i < c.GetQueryResultCount(); i++ { 347 | qr := c.GetQueryResult(i) 348 | for j := 0; j < qr.GetArcheTypeCount(); j++ { 349 | a := qr.GetArcheType(j) 350 | if a == nil { 351 | continue 352 | } 353 | if a.hasComponent(reflect.TypeOf(t)) { 354 | if a.hasEntity(entityId) { 355 | return true 356 | } 357 | } 358 | } 359 | } 360 | return false 361 | } 362 | --------------------------------------------------------------------------------