├── LICENSE ├── README.md ├── cmd └── example │ ├── .gitignore │ └── main.go └── ecs.go /LICENSE: -------------------------------------------------------------------------------- 1 | Byte Arena ECS copyright 2017 Byte Arena org. 2 | 3 | This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. 4 | 5 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github.com/bytearena/ecs 2 | 3 | Go implementation of the Entity/Component/System paradigm. 4 | 5 | Think of it as a flexible, in-memory, queryable database of **game actors** (entities) linked with their respective **gameplay aspects** (components). 6 | 7 | You're left with writing the systems, (preferably stateless) functions that query the database to fetch the state of the game they're responsible of, and update it accordingly. 8 | 9 | TypeScript port: https://github.com/netgusto/ecs-typescript/blob/master/src/ecs.ts 10 | 11 | ## Installation 12 | 13 | ``` 14 | go get -u github.com/bytearena/ecs 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/bytearena/ecs" 26 | ) 27 | 28 | // The Walk component; always design components as simple containers 29 | // without logic (except for getters/setters if useful) 30 | type Walk struct { 31 | Direction string 32 | Distance float64 33 | } 34 | 35 | // The Talk component 36 | type Talk struct { 37 | Message string 38 | } 39 | 40 | func main() { 41 | 42 | // Initialize the ECS manager 43 | manager := ecs.NewManager() 44 | 45 | // Declare the components 46 | walk := manager.NewComponent() 47 | talk := manager.NewComponent() 48 | 49 | // Create 3 entities, and provide their components 50 | // Component data may be anything (interface{}) 51 | // Use pointers if you want to be able to mutate the data 52 | manager.NewEntity(). 53 | AddComponent(walk, &Walk{ 54 | Direction: "east", 55 | Distance: 3.14, 56 | }) 57 | 58 | manager.NewEntity(). 59 | AddComponent(talk, &Talk{ 60 | Message: "Wassup?", 61 | }) 62 | 63 | manager.NewEntity(). 64 | AddComponent(walk, &Walk{ 65 | Direction: "north", 66 | Distance: 12.4, 67 | }). 68 | AddComponent(talk, &Talk{ 69 | Message: "Fluctuat nec mergitur.", 70 | }) 71 | 72 | // Tags are masks that help identify entities that match the required components 73 | walkers := ecs.BuildTag(walk) 74 | talkers := ecs.BuildTag(talk) 75 | walkertalkers := ecs.BuildTag(walkers, talkers) 76 | 77 | // Process the walkers 78 | fmt.Println("\n# All the walkers walk :") 79 | for _, result := range manager.Query(walkers) { 80 | walkAspect := result.Components[walk].(*Walk) 81 | fmt.Println("I'm walking ", walkAspect.Distance, "km towards", walkAspect.Direction) 82 | } 83 | 84 | // Process the talkers 85 | fmt.Println("\n# All the talkers talk (and be mutated) :") 86 | for _, result := range manager.Query(talkers) { 87 | talkAspect := result.Components[talk].(*Talk) 88 | fmt.Println(talkAspect.Message, "Just sayin'.") 89 | 90 | // Here we mutate the component for this entity 91 | talkAspect.Message = "So I was like 'For real?' and he was like '" + talkAspect.Message + "'" 92 | } 93 | 94 | // Process the talkers/walkers 95 | fmt.Println("\n# All the talkers & walkers do their thing :") 96 | for _, result := range manager.Query(walkertalkers) { 97 | walkAspect := result.Components[walk].(*Walk) 98 | talkAspect := result.Components[talk].(*Talk) 99 | fmt.Println("I'm walking towards", walkAspect.Direction, ";", talkAspect.Message) 100 | } 101 | 102 | /////////////////////////////////////////////////////////////////////////// 103 | // Demonstrating views 104 | // To increase speed for repetitive queries, you can create cached views 105 | // for entities matching a given tag 106 | /////////////////////////////////////////////////////////////////////////// 107 | 108 | fmt.Println("\n# Demonstrating views") 109 | 110 | talkersView := manager.CreateView(talkers) 111 | 112 | //Add a new entity that can talk which has not been mutated 113 | manager.NewEntity(). 114 | AddComponent(talk, &Talk{ 115 | Message: "Ceci n'est pas une pipe", 116 | }) 117 | 118 | fmt.Println("\n# Should print 3 messages :") 119 | for _, result := range talkersView.Get() { 120 | talkAspect := result.Components[talk].(*Talk) 121 | fmt.Println(result.Entity.GetID(), "says", talkAspect.Message) 122 | } 123 | 124 | manager.DisposeEntities(manager.Query(talkers).Entities()...) 125 | 126 | fmt.Println("\n# Talkers have been disposed; should not print any message below :") 127 | for _, result := range talkersView.Get() { 128 | talkAspect := result.Components[talk].(*Talk) 129 | fmt.Println(result.Entity.GetID(), "says", talkAspect.Message) 130 | } 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /cmd/example/.gitignore: -------------------------------------------------------------------------------- 1 | example -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytearena/ecs" 7 | ) 8 | 9 | // The Walk component; always design components as simple containers 10 | // without logic (except for getters/setters if useful) 11 | type Walk struct { 12 | Direction string 13 | Distance float64 14 | } 15 | 16 | // The Talk component 17 | type Talk struct { 18 | Message string 19 | } 20 | 21 | func main() { 22 | 23 | // Initialize the ECS manager 24 | manager := ecs.NewManager() 25 | 26 | // Declare the components 27 | walk := manager.NewComponent() 28 | talk := manager.NewComponent() 29 | 30 | // Create 3 entities, and provide their components 31 | // Component data may be anything (interface{}) 32 | // Use pointers if you want to be able to mutate the data 33 | manager.NewEntity(). 34 | AddComponent(walk, &Walk{ 35 | Direction: "east", 36 | Distance: 3.14, 37 | }) 38 | 39 | manager.NewEntity(). 40 | AddComponent(talk, &Talk{ 41 | Message: "Wassup?", 42 | }) 43 | 44 | manager.NewEntity(). 45 | AddComponent(walk, &Walk{ 46 | Direction: "north", 47 | Distance: 12.4, 48 | }). 49 | AddComponent(talk, &Talk{ 50 | Message: "Fluctuat nec mergitur.", 51 | }) 52 | 53 | // Tags are masks that help identify entities that match the required components 54 | walkers := ecs.BuildTag(walk) 55 | talkers := ecs.BuildTag(talk) 56 | walkertalkers := ecs.BuildTag(walkers, talkers) 57 | 58 | // Process the walkers 59 | fmt.Println("\n# All the walkers walk :") 60 | for _, result := range manager.Query(walkers) { 61 | walkAspect := result.Components[walk].(*Walk) 62 | fmt.Println("I'm walking ", walkAspect.Distance, "km towards", walkAspect.Direction) 63 | } 64 | 65 | // Process the talkers 66 | fmt.Println("\n# All the talkers talk (and be mutated) :") 67 | for _, result := range manager.Query(talkers) { 68 | talkAspect := result.Components[talk].(*Talk) 69 | fmt.Println(talkAspect.Message, "Just sayin'.") 70 | 71 | // Here we mutate the component for this entity 72 | talkAspect.Message = "So I was like 'For real?' and he was like '" + talkAspect.Message + "'" 73 | } 74 | 75 | // Process the talkers/walkers 76 | fmt.Println("\n# All the talkers & walkers do their thing :") 77 | for _, result := range manager.Query(walkertalkers) { 78 | walkAspect := result.Components[walk].(*Walk) 79 | talkAspect := result.Components[talk].(*Talk) 80 | fmt.Println("I'm walking towards", walkAspect.Direction, ";", talkAspect.Message) 81 | } 82 | 83 | /////////////////////////////////////////////////////////////////////////// 84 | // Demonstrating views 85 | // To increase speed for repetitive queries, you can create cached views 86 | // for entities matching a given tag 87 | /////////////////////////////////////////////////////////////////////////// 88 | 89 | fmt.Println("\n# Demonstrating views") 90 | 91 | talkersView := manager.CreateView(talkers) 92 | 93 | //Add a new entity that can talk which has not been mutated 94 | manager.NewEntity(). 95 | AddComponent(talk, &Talk{ 96 | Message: "Ceci n'est pas une pipe", 97 | }) 98 | 99 | fmt.Println("\n# Should print 3 messages :") 100 | for _, result := range talkersView.Get() { 101 | talkAspect := result.Components[talk].(*Talk) 102 | fmt.Println(result.Entity.GetID(), "says", talkAspect.Message) 103 | } 104 | 105 | manager.DisposeEntities(manager.Query(talkers).Entities()...) 106 | 107 | fmt.Println("\n# Talkers have been disposed; should not print any message below :") 108 | for _, result := range talkersView.Get() { 109 | talkAspect := result.Components[talk].(*Talk) 110 | fmt.Println(result.Entity.GetID(), "says", talkAspect.Message) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ecs.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | type EntityID uint32 10 | 11 | func (id EntityID) String() string { 12 | return strconv.Itoa(int(id)) 13 | } 14 | 15 | type ComponentID uint32 16 | 17 | type Tag struct { 18 | flags uint64 // limited to 64 components 19 | inverse bool 20 | } 21 | 22 | func (tag Tag) matches(smallertag Tag) bool { 23 | res := tag.flags&smallertag.flags == smallertag.flags 24 | 25 | if smallertag.inverse { 26 | return !res 27 | } 28 | 29 | return res 30 | } 31 | 32 | func (tag *Tag) binaryORInPlace(othertag Tag) *Tag { 33 | tag.flags |= othertag.flags 34 | return tag 35 | } 36 | 37 | func (tag *Tag) binaryNOTInPlace(othertag Tag) *Tag { 38 | tag.flags ^= othertag.flags 39 | return tag 40 | } 41 | 42 | func (tag Tag) clone() Tag { 43 | return tag 44 | } 45 | 46 | func (tag Tag) Inverse(values ...bool) Tag { 47 | 48 | clone := tag.clone() 49 | inverse := true 50 | if len(values) > 0 { 51 | inverse = values[0] 52 | } 53 | 54 | clone.inverse = inverse 55 | return clone 56 | } 57 | 58 | type View struct { 59 | tag Tag 60 | entities QueryResultCollection 61 | lock *sync.RWMutex 62 | } 63 | 64 | type QueryResultCollection []*QueryResult 65 | 66 | func (coll QueryResultCollection) Entities() []*Entity { 67 | res := make([]*Entity, len(coll)) 68 | for i, qr := range coll { 69 | res[i] = qr.Entity 70 | } 71 | 72 | return res 73 | } 74 | 75 | func (v *View) Get() QueryResultCollection { 76 | v.lock.RLock() 77 | defer v.lock.RUnlock() 78 | return v.entities 79 | } 80 | 81 | func (v *View) add(entity *Entity) { 82 | v.lock.Lock() 83 | v.entities = append(v.entities, entity.manager.GetEntityByID( 84 | entity.ID, 85 | v.tag, 86 | )) 87 | v.lock.Unlock() 88 | } 89 | 90 | func (v *View) remove(entity *Entity) { 91 | v.lock.RLock() 92 | for i, qr := range v.entities { 93 | if qr.Entity.ID == entity.ID { 94 | maxbound := len(v.entities) - 1 95 | v.entities[maxbound], v.entities[i] = v.entities[i], v.entities[maxbound] 96 | v.entities = v.entities[:maxbound] 97 | break 98 | } 99 | } 100 | v.lock.RUnlock() 101 | } 102 | 103 | type Manager struct { 104 | lock *sync.RWMutex 105 | entityIdInc uint32 106 | componentNumInc uint32 // limited to 64 107 | 108 | entities []*Entity 109 | entitiesByID map[EntityID]*Entity 110 | components []*Component 111 | views []*View 112 | } 113 | 114 | type Component struct { 115 | id ComponentID 116 | tag Tag 117 | datalock *sync.RWMutex 118 | data map[EntityID]interface{} 119 | destructor func(entity *Entity, data interface{}) 120 | } 121 | 122 | func (component *Component) SetDestructor(destructor func(entity *Entity, data interface{})) { 123 | component.destructor = destructor 124 | } 125 | 126 | func (component *Component) GetID() ComponentID { 127 | return component.id 128 | } 129 | 130 | func (manager *Manager) CreateView(tagelements ...interface{}) *View { 131 | 132 | tag := BuildTag(tagelements...) 133 | 134 | view := &View{ 135 | tag: tag, 136 | lock: &sync.RWMutex{}, 137 | } 138 | 139 | entities := manager.Query(tag) 140 | view.entities = make(QueryResultCollection, len(entities)) 141 | manager.lock.Lock() 142 | for i, entityresult := range entities { 143 | view.entities[i] = entityresult 144 | } 145 | manager.views = append(manager.views, view) 146 | manager.lock.Unlock() 147 | 148 | return view 149 | } 150 | 151 | type Entity struct { 152 | ID EntityID 153 | tag Tag 154 | manager *Manager 155 | } 156 | 157 | func (entity *Entity) GetID() EntityID { 158 | return entity.ID 159 | } 160 | 161 | func NewManager() *Manager { 162 | return &Manager{ 163 | entityIdInc: 0, 164 | componentNumInc: 0, 165 | entitiesByID: make(map[EntityID]*Entity), 166 | lock: &sync.RWMutex{}, 167 | views: make([]*View, 0), 168 | } 169 | } 170 | 171 | func BuildTag(elements ...interface{}) Tag { 172 | 173 | tag := Tag{} 174 | 175 | for _, element := range elements { 176 | switch typedelement := element.(type) { 177 | case *Component: 178 | { 179 | tag.binaryORInPlace(typedelement.tag) 180 | } 181 | case Tag: 182 | { 183 | tag.binaryORInPlace(typedelement) 184 | } 185 | default: 186 | { 187 | panic("Invalid type passed to BuildTag; accepts only <*Component> and types.") 188 | } 189 | } 190 | } 191 | 192 | return tag 193 | } 194 | 195 | func (manager *Manager) NewEntity() *Entity { 196 | 197 | nextid := ComponentID(atomic.AddUint32(&manager.componentNumInc, 1)) 198 | id := nextid - 1 // to start at 0 199 | 200 | entity := &Entity{ 201 | ID: EntityID(id), 202 | manager: manager, 203 | } 204 | 205 | manager.lock.Lock() 206 | manager.entities = append(manager.entities, entity) 207 | manager.entitiesByID[entity.ID] = entity 208 | manager.lock.Unlock() 209 | 210 | return entity 211 | } 212 | 213 | func (manager *Manager) NewComponent() *Component { 214 | 215 | if manager.componentNumInc >= 63 { 216 | panic("Component overflow (limited to 64)") 217 | } 218 | 219 | nextid := ComponentID(atomic.AddUint32(&manager.componentNumInc, 1)) 220 | id := nextid - 1 // to start at 0 221 | 222 | tag := Tag{ 223 | flags: (1 << id), // set bit on position corresponding to component number 224 | inverse: false, 225 | } 226 | 227 | component := &Component{ 228 | id: id, 229 | tag: tag, 230 | data: make(map[EntityID]interface{}), 231 | datalock: &sync.RWMutex{}, 232 | } 233 | 234 | manager.lock.Lock() 235 | manager.components = append(manager.components, component) 236 | manager.lock.Unlock() 237 | 238 | return component 239 | } 240 | 241 | func (manager *Manager) GetEntityByID(id EntityID, tagelements ...interface{}) *QueryResult { 242 | 243 | manager.lock.RLock() 244 | res, ok := manager.entitiesByID[id] 245 | 246 | if !ok { 247 | manager.lock.RUnlock() 248 | return nil 249 | } 250 | 251 | tag := BuildTag(tagelements...) 252 | 253 | components := manager.fetchComponentsForEntity(res, tag) 254 | manager.lock.RUnlock() 255 | 256 | if components == nil { 257 | return nil 258 | } 259 | 260 | return &QueryResult{ 261 | Entity: res, 262 | Components: components, 263 | } 264 | 265 | } 266 | 267 | func (entity Entity) Matches(tag Tag) bool { 268 | return entity.tag.matches(tag) 269 | } 270 | 271 | func (entity *Entity) AddComponent(component *Component, componentdata interface{}) *Entity { 272 | component.datalock.Lock() 273 | component.data[entity.ID] = componentdata 274 | component.datalock.Unlock() 275 | 276 | component.datalock.RLock() 277 | 278 | tagbefore := entity.tag 279 | entity.tag.binaryORInPlace(component.tag) 280 | 281 | for _, view := range entity.manager.views { 282 | 283 | if !tagbefore.matches(view.tag) && entity.tag.matches(view.tag) { 284 | view.add(entity) 285 | } 286 | } 287 | 288 | component.datalock.RUnlock() 289 | return entity 290 | } 291 | 292 | func (entity *Entity) RemoveComponent(component *Component) *Entity { 293 | if component.destructor != nil { 294 | if data, ok := component.data[entity.ID]; ok { 295 | component.destructor(entity, data) 296 | } 297 | } 298 | 299 | component.datalock.Lock() 300 | delete(component.data, entity.ID) 301 | tagbefore := entity.tag 302 | entity.tag.binaryNOTInPlace(component.tag) 303 | 304 | for _, view := range entity.manager.views { 305 | if tagbefore.matches(view.tag) && !entity.tag.matches(view.tag) { 306 | view.remove(entity) 307 | } 308 | } 309 | 310 | component.datalock.Unlock() 311 | return entity 312 | } 313 | 314 | func (entity Entity) HasComponent(component *Component) bool { 315 | return entity.tag.matches(component.tag) 316 | } 317 | 318 | func (entity Entity) GetComponentData(component *Component) (interface{}, bool) { 319 | component.datalock.RLock() 320 | data, ok := component.data[entity.ID] 321 | component.datalock.RUnlock() 322 | 323 | return data, ok 324 | } 325 | 326 | func (manager *Manager) DisposeEntities(entities ...*Entity) { 327 | for _, entity := range entities { 328 | manager.DisposeEntity(entity) 329 | } 330 | } 331 | 332 | func (manager *Manager) DisposeEntity(entity interface{}) { 333 | 334 | var typedentity *Entity 335 | 336 | switch typeditem := entity.(type) { 337 | case *QueryResult: 338 | { 339 | typedentity = typeditem.Entity 340 | } 341 | case QueryResult: 342 | { 343 | typedentity = typeditem.Entity 344 | } 345 | case *Entity: 346 | { 347 | typedentity = typeditem 348 | } 349 | default: 350 | { 351 | panic("Invalid type passed to DisposeEntity; accepts only <*QueryResult>, and <*Entity> types.") 352 | } 353 | } 354 | 355 | if typedentity == nil { 356 | return 357 | } 358 | 359 | manager.lock.Lock() 360 | for _, component := range manager.components { 361 | if typedentity.HasComponent(component) { 362 | typedentity.RemoveComponent(component) 363 | } 364 | } 365 | delete(manager.entitiesByID, typedentity.ID) 366 | manager.lock.Unlock() 367 | } 368 | 369 | type QueryResult struct { 370 | Entity *Entity 371 | Components map[*Component]interface{} 372 | } 373 | 374 | func (manager *Manager) fetchComponentsForEntity(entity *Entity, tag Tag) map[*Component]interface{} { 375 | 376 | if !entity.tag.matches(tag) { 377 | return nil 378 | } 379 | 380 | componentMap := make(map[*Component]interface{}) 381 | 382 | for _, component := range manager.components { 383 | if tag.matches(component.tag) { 384 | data, ok := entity.GetComponentData(component) 385 | if !ok { 386 | return nil // if one of the required components is not set, return nothing ! 387 | } 388 | 389 | componentMap[component] = data 390 | } 391 | 392 | // fmt.Printf("-------------\n") 393 | // fmt.Printf("%16b : %s\n", int64(tag), "tag") 394 | // fmt.Printf("%16b : %s\n", int64(component.tag), "component.tag") 395 | // fmt.Printf("%16b : %s\n", int64(entity.tag), "entity.tag") 396 | // fmt.Printf("//////////////////\n") 397 | } 398 | 399 | return componentMap 400 | } 401 | 402 | func (manager *Manager) Query(tag Tag) QueryResultCollection { 403 | 404 | matches := make(QueryResultCollection, 0) 405 | 406 | manager.lock.RLock() 407 | for _, entity := range manager.entities { 408 | if entity.tag.matches(tag) { 409 | 410 | componentMap := make(map[*Component]interface{}) 411 | 412 | for _, component := range manager.components { 413 | if tag.matches(component.tag) { 414 | data, _ := entity.GetComponentData(component) 415 | componentMap[component] = data 416 | } 417 | } 418 | 419 | matches = append(matches, &QueryResult{ 420 | Entity: entity, 421 | Components: componentMap, 422 | }) 423 | 424 | } 425 | } 426 | manager.lock.RUnlock() 427 | 428 | return matches 429 | } 430 | --------------------------------------------------------------------------------