├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── arch.go ├── bench └── main.go ├── bench_test.go ├── benchmark_physics_test.go ├── benchmark_test.go ├── bundle.go ├── cod_encode.go ├── command.go ├── command_test.go ├── component.go ├── dcr.go ├── deprecated.go ├── ecs.go ├── entity.go ├── event.go ├── examples └── basic │ └── main.go ├── filter.go ├── go.mod ├── go.sum ├── hashmap.go ├── hook.go ├── inject.go ├── internal ├── gen │ ├── main.go │ ├── old_view.tgo │ └── view.tgo └── intmap │ ├── LICENSE │ ├── README.md │ ├── intintmap_test.go │ ├── map64.go │ └── map64_test.go ├── list.go ├── mask.go ├── mask_test.go ├── name.go ├── name_test.go ├── query_test.go ├── storage.go ├── system.go ├── system_test.go ├── view_gen.go ├── world.go └── world_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | go-version: ['1.21'] 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - uses: actions/checkout@v3 20 | 21 | - name: make All 22 | run: make all 23 | 24 | - name: Update coverage report 25 | uses: ncruces/go-coverage-report@v0 26 | with: 27 | report: true 28 | chart: true 29 | amend: true 30 | if: | 31 | matrix.os == 'ubuntu-latest' && 32 | github.event_name == 'push' 33 | continue-on-error: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 unitoftime 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fmt build test benchmark 2 | 3 | fmt: 4 | go fmt ./... 5 | 6 | gen: 7 | go run ./internal/gen/ 8 | 9 | build: fmt 10 | go build -v ./... 11 | 12 | test: fmt 13 | # go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 14 | go test -v -coverprofile=coverage.out -covermode=count ./... 15 | 16 | benchmark: 17 | go test -v -bench=. ./... 18 | 19 | coverage: 20 | go tool cover -html=coverage.out 21 | 22 | bench2: 23 | go test -run=nothingplease -bench=BenchmarkAddEntity -benchmem -memprofile mem.pprof -cpuprofile cpu.pprof 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/unitoftime/ecs.svg)](https://pkg.go.dev/github.com/unitoftime/ecs) 2 | [![Build](https://github.com/unitoftime/ecs/actions/workflows/build.yml/badge.svg)](https://github.com/unitoftime/ecs/actions/workflows/build.yml) 3 | [![Go Coverage](https://github.com/unitoftime/ecs/wiki/coverage.svg)](https://raw.githack.com/wiki/unitoftime/ecs/coverage.html) 4 | 5 | 6 | This is an ecs library I wrote for doing game development in Go. I'm actively using it and its pretty stable, but I do find bugs every once in a while. I might vary the APIs in the future if native iterators are added. 7 | 8 | ### Overview 9 | Conceptually you can imagine an ECS as one big table, where an `Id` column associates an *Entity Id* with various other component columns. Kind of like this: 10 | 11 | | Id | Position | Rotation | Size | 12 | |:--:|:--------:|:--------:|:----:| 13 | | 0 | {1, 1} | 3.14 | 11 | 14 | | 1 | {2, 2} | 6.28 | 22 | 15 | 16 | We use an archetype-based storage mechanism. Which simply means we have a specific table for a specific component layout. This means that if you add or remove components it can be somewhat expensive, because we have to copy the entire entity to the new table. 17 | 18 | ## Basic Full Example 19 | You can find a fairly comprehensive example here: 20 | - [Basic Example](https://github.com/unitoftime/ecs/tree/master/examples/basic) 21 | 22 | 23 | ### Basic Usage 24 | Import the library: `import "github.com/unitoftime/ecs"` 25 | 26 | Create Components like you normally would: 27 | ``` 28 | type Position struct { 29 | X, Y float64 30 | } 31 | 32 | type Rotation float64 33 | ``` 34 | 35 | Create a `World` to store all of your data 36 | ``` 37 | world := ecs.NewWorld() 38 | ``` 39 | 40 | Create an entity and add components to it 41 | ``` 42 | id := world.NewId() 43 | ecs.Write(world, id, 44 | ecs.C(Position{1, 1}), 45 | ecs.C(Rotation(3.14)), 46 | // Note: Try to reduce the number of write calls by packing as many components as you can in 47 | ) 48 | 49 | // Side-Note: I'm trying to get rid of the `ecs.C(...)` boxing, but I couldn't figure out how when 50 | // I first wrote the ECS. I'll try to get back to fixing that because ideally you 51 | // shouldn't have to worry about it. For now though, you have to box your components 52 | // to the `ecs.Component` interface type before passing them in, so `ecs.C(...)` 53 | // does that for you. 54 | ``` 55 | 56 | Create a View, by calling `QueryN`: 57 | ``` 58 | query := ecs.Query2[Position, Rotation](world) 59 | ``` 60 | 61 | Iterate on the query. You basically pass in a lambda, and internally the library calls it for every entity in the world which has all of the components specified. Notably your lambda takes pointer values which represent a pointer to the internally stored component. So modifying these pointers will modify the entity's data. 62 | ``` 63 | query.MapId(func(id ecs.Id, pos *Position, rot *Rotation) { 64 | pos.X += 1 65 | pos.Y += 1 66 | 67 | rot += 0.01 68 | }) 69 | ``` 70 | 71 | There are several map functions you can use, each with varying numbers of parameters. I support up to `Map12`. They all look like this: 72 | ``` 73 | ecs.MapN(world, func(id ecs.Id, a *ComponentA, /*... */, n *ComponentN) { 74 | // Do your work 75 | }) 76 | ``` 77 | 78 | ### Advanced queries 79 | You can also filter your queries for more advanced usage: 80 | ``` 81 | // Returns a view of Position and Velocity, but only if the entity also has the `Rotation` component. 82 | query := ecs.Query2[Position, Velocity](world, ecs.With(Rotation)) 83 | 84 | // Returns a view of Position and Velocity, but if velocity is missing on the entity, will just return nil during the `MapId(...)`. You must do nil checks for all components included in the `Optional()`! 85 | query := ecs.Query2[Position, Velocity](world, ecs.Optional(Velocity)) 86 | ``` 87 | 88 | ### Commands 89 | 90 | Commands will eventually replace `ecs.Write(...)` once I figure out how their usage will work. Commands essentially buffer some work on the ECS so that the work can be executed later on. You can use them in loop safe ways by calling `Execute()` after your loop has completed. Right now they work like this: 91 | ``` 92 | world := NewWorld() 93 | cmd := NewCommandQueue(world) 94 | 95 | cmd.SpawnEmpty(). 96 | Insert(ecs.C(Position{1, 2, 3})). 97 | Insert(ecs.C(Velocity{1, 2, 3})) 98 | 99 | cmd.Execute() 100 | ``` 101 | 102 | ### Still In Progress 103 | - [ ] Improving iterator performance: See: https://github.com/golang/go/discussions/54245 104 | 105 | ### Videos 106 | Hopefully, eventually I can have some automated test-bench that runs and measures performance, but for now you'll just have to refer to my second video and hopefully trust me. Of course, you can run the benchmark in the `bench` folder to measure how long frames take on your computer. 107 | 108 | 1. How it works: https://youtu.be/71RSWVyOMEY 109 | 2. Simulation Performance: https://youtu.be/i2gWDOgg50k 110 | -------------------------------------------------------------------------------- /arch.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // This is the identifier for entities in the world 8 | // 9 | //cod:struct 10 | type Id uint32 11 | 12 | type archetypeId uint32 13 | 14 | type entLoc struct { 15 | archId archetypeId 16 | index uint32 17 | } 18 | 19 | // Provides generic storage for all archetypes 20 | type archEngine struct { 21 | generation int 22 | 23 | lookup []*lookupList // Indexed by archetypeId 24 | compStorage []storage // Indexed by componentId 25 | dcr *componentRegistry 26 | 27 | // TODO: Optimization: Hook loops can be improved by tracking a slice of CompId for each type of hook. Then when I Track components on that finalizeSlice, I can just loop over the list of CompId which will only be as long as the number of hooks that the user has added 28 | onAddHooks []Handler // A list of hooks to execute for onAdd events. Indexed by componentId 29 | finalizeOnAdd []CompId // The temporary list of components to run the onAdd hooks 30 | 31 | // TODO: You could unify hooks with observers by making initial ranges of EventId 32 | // [0, maxComponent) -> Add event per component 33 | // [maxComponent, 2*maxComponent) -> Remove event per component 34 | // etc... 35 | } 36 | 37 | func newArchEngine() *archEngine { 38 | return &archEngine{ 39 | generation: 1, // Start at 1 so that anyone with the default int value will always realize they are in the wrong generation 40 | 41 | lookup: make([]*lookupList, 0, DefaultAllocation), 42 | compStorage: make([]storage, maxComponentId+1), 43 | dcr: newComponentRegistry(), 44 | 45 | onAddHooks: make([]Handler, maxComponentId+1), 46 | } 47 | } 48 | 49 | func (e *archEngine) print() { 50 | // fmt.Printf("%+v\n", *e) 51 | for i := range e.lookup { 52 | fmt.Printf(" id: %+v\n", e.lookup[i].id) 53 | fmt.Printf(" holes: %+v\n", e.lookup[i].holes) 54 | fmt.Printf(" mask: %+v\n", e.lookup[i].mask) 55 | fmt.Printf(" components: %+v\n", e.lookup[i].components) 56 | fmt.Printf("--------------------------------------------------------------------------------\n") 57 | } 58 | 59 | for i := range e.compStorage { 60 | fmt.Printf("css: %d: %+v\n", i, e.compStorage[i]) 61 | } 62 | } 63 | 64 | func (e *archEngine) newArchetypeId(archMask archetypeMask, components []CompId) archetypeId { 65 | e.generation++ // Increment the generation 66 | 67 | archId := archetypeId(len(e.lookup)) 68 | e.lookup = append(e.lookup, 69 | &lookupList{ 70 | id: make([]Id, 0, DefaultAllocation), 71 | holes: make([]int, 0, DefaultAllocation), 72 | mask: archMask, 73 | components: components, 74 | }, 75 | ) 76 | 77 | return archId 78 | } 79 | 80 | func (e *archEngine) getGeneration() int { 81 | return e.generation 82 | } 83 | 84 | func (e *archEngine) count(anything ...any) int { 85 | comps := make([]CompId, len(anything)) 86 | for i, c := range anything { 87 | comps[i] = name(c) 88 | } 89 | 90 | archIds := make([]archetypeId, 0) 91 | archIds = e.FilterList(archIds, comps) 92 | 93 | total := 0 94 | for _, archId := range archIds { 95 | lookup := e.lookup[archId] 96 | if lookup == nil { 97 | panic(fmt.Sprintf("Couldnt find archId in archEngine lookup table: %d", archId)) 98 | } 99 | 100 | // Each id represents an entity that holds the requested component(s) 101 | // Each hole represents a deleted entity that used to hold the requested component(s) 102 | total = total + len(lookup.id) - len(lookup.holes) 103 | } 104 | return total 105 | } 106 | 107 | func (e *archEngine) getArchetypeId(mask archetypeMask) archetypeId { 108 | return e.dcr.getArchetypeId(e, mask) 109 | } 110 | 111 | // Returns replaces archIds with a list of archids that match the compId list 112 | func (e *archEngine) FilterList(archIds []archetypeId, comp []CompId) []archetypeId { 113 | // Idea 3: Loop through every registered archMask to see if it matches 114 | // Problem - Forces you to check every arch mask, even if the 115 | // The good side is that you dont need to deduplicate your list, and you dont need to allocate 116 | requiredArchMask := buildArchMaskFromId(comp...) 117 | 118 | archIds = archIds[:0] 119 | for archId := range e.dcr.revArchMask { 120 | if requiredArchMask.contains(e.dcr.revArchMask[archId]) { 121 | archIds = append(archIds, archetypeId(archId)) 122 | } 123 | } 124 | return archIds 125 | 126 | //-------------------------------------------------------------------------------- 127 | // Idea 2: Loop through every archMask that every componentId points to 128 | // // TODO: could I maybe do something more optimal with archetypeMask? Something like this could work. 129 | // requiredArchMask := buildArchMaskFromId(comp...) 130 | 131 | // archCount := make(map[archetypeId]struct{}) 132 | 133 | // archIds = archIds[:0] 134 | // for _, compId := range comp { 135 | // for _, archId := range e.dcr.archSet[compId] { 136 | // archMask, ok := e.dcr.revArchMask[archId] 137 | // if !ok { 138 | // panic("AAA") 139 | // continue 140 | // } // TODO: This shouldn't happen? 141 | // if requiredArchMask.contains(archMask) { 142 | // archCount[archId] = struct{}{} 143 | // } 144 | // } 145 | // } 146 | 147 | // for archId := range archCount { 148 | // archIds = append(archIds, archId) 149 | // } 150 | // return archIds 151 | } 152 | 153 | func getStorage[T any](e *archEngine) *componentStorage[T] { 154 | var val T 155 | n := name(val) 156 | return getStorageByCompId[T](e, n) 157 | } 158 | 159 | // Note: This will panic if the wrong compId doesn't match the generic type 160 | func getStorageByCompId[T any](e *archEngine, compId CompId) *componentStorage[T] { 161 | ss := e.compStorage[compId] 162 | if ss == nil { 163 | ss = &componentStorage[T]{ 164 | slice: newMap[archetypeId, *componentList[T]](DefaultAllocation), 165 | } 166 | e.compStorage[compId] = ss 167 | } 168 | storage := ss.(*componentStorage[T]) 169 | 170 | return storage 171 | } 172 | 173 | func (e *archEngine) getOrAddLookupIndex(archId archetypeId, id Id) int { 174 | lookup := e.lookup[archId] 175 | 176 | index := lookup.addToEasiestHole(id) 177 | return index 178 | } 179 | 180 | // Writes all of the components to the archetype. 181 | // Internally requires that the id is not added to the archetype 182 | func (e *archEngine) spawn(archId archetypeId, id Id, comp ...Component) int { 183 | lookup := e.lookup[archId] 184 | // TODO: Doesn't cleanup holes? 185 | index := lookup.addToEasiestHole(id) 186 | loc := entLoc{archId, uint32(index)} 187 | e.writeIndex(loc, id, comp...) 188 | 189 | // All components are added 190 | e.finalizeOnAdd = markComponents(e.finalizeOnAdd, comp...) 191 | 192 | return index 193 | } 194 | 195 | func (e *archEngine) writeIndex(loc entLoc, id Id, comp ...Component) { 196 | // Loop through all components and add them to individual component slices 197 | wd := W{ 198 | engine: e, 199 | archId: loc.archId, 200 | index: int(loc.index), 201 | } 202 | for i := range comp { 203 | comp[i].CompWrite(wd) 204 | } 205 | } 206 | 207 | // Allocates a slot for the supplied archId 208 | func (e *archEngine) allocate(archId archetypeId, id Id) int { 209 | // Add to lookup list 210 | index := e.getOrAddLookupIndex(archId, id) 211 | 212 | // for compId registered to archId 213 | lookup := e.lookup[archId] 214 | for _, compId := range lookup.components { 215 | s := e.getStorage(compId) 216 | s.Allocate(archId, index) 217 | } 218 | return index 219 | } 220 | 221 | func (e *archEngine) getStorage(compId CompId) storage { 222 | ss := e.compStorage[compId] 223 | if ss == nil { 224 | ss = newComponentStorage(compId) 225 | e.compStorage[compId] = ss 226 | } 227 | return ss 228 | } 229 | 230 | func writeArch[T any](e *archEngine, archId archetypeId, index int, store *componentStorage[T], val T) { 231 | cSlice := store.GetSlice(archId) 232 | cSlice.Write(index, val) 233 | } 234 | 235 | // Returns the archetypeId of where the entity ends up 236 | func (e *archEngine) rewriteArch(loc entLoc, id Id, comp ...Component) entLoc { 237 | // Calculate the new mask based on the bitwise or of the old and added masks 238 | lookup := e.lookup[loc.archId] 239 | oldMask := lookup.mask 240 | addMask := buildArchMask(comp...) 241 | newMask := oldMask.bitwiseOr(addMask) 242 | 243 | if oldMask == newMask { 244 | // Case 1: Archetype and index stays the same. 245 | // This means that we only need to write the newly added components because we wont be moving the base entity data 246 | e.writeIndex(loc, id, comp...) 247 | return loc 248 | } else { 249 | // 1. Move Archetype Data 250 | newLoc := e.moveArchetype(loc, newMask, id) 251 | 252 | // 2. Write new componts to new archetype/index location 253 | e.writeIndex(newLoc, id, comp...) 254 | 255 | // Mark all new components 256 | e.finalizeOnAdd = markNewComponents(e.finalizeOnAdd, oldMask, comp...) 257 | 258 | return newLoc 259 | } 260 | } 261 | 262 | // Moves an entity from one archetype to another, copying all of the data from the old archetype to the new one 263 | func (e *archEngine) moveArchetype(oldLoc entLoc, newMask archetypeMask, id Id) entLoc { 264 | newArchId := e.dcr.getArchetypeId(e, newMask) 265 | newIndex := e.allocate(newArchId, id) 266 | newLoc := entLoc{newArchId, uint32(newIndex)} 267 | 268 | oldLookup := e.lookup[oldLoc.archId] 269 | 270 | for _, compId := range oldLookup.components { 271 | store := e.compStorage[compId] 272 | store.moveArchetype(oldLoc, newLoc) 273 | } 274 | 275 | e.TagForDeletion(oldLoc, id) 276 | 277 | return entLoc{newArchId, uint32(newIndex)} 278 | } 279 | 280 | // Moves an entity from one archetype to another, copying all of the data required by the new archetype 281 | func (e *archEngine) moveArchetypeDown(oldLoc entLoc, newMask archetypeMask, id Id) entLoc { 282 | newArchId := e.dcr.getArchetypeId(e, newMask) 283 | newIndex := e.allocate(newArchId, id) 284 | 285 | newLoc := entLoc{newArchId, uint32(newIndex)} 286 | newLookup := e.lookup[newArchId] 287 | for _, compId := range newLookup.components { 288 | store := e.compStorage[compId] 289 | store.moveArchetype(oldLoc, newLoc) //oldArchId, oldIndex, newArchId, newIndex) 290 | } 291 | 292 | e.TagForDeletion(oldLoc, id) 293 | 294 | return newLoc 295 | } 296 | 297 | // This creates a "hole" in the archetype at the specified Id 298 | // Once we get enough holes, we can re-pack the entire slice 299 | // TODO - How many holes before we repack? How many holes to pack at a time? 300 | func (e *archEngine) TagForDeletion(loc entLoc, id Id) { 301 | lookup := e.lookup[loc.archId] 302 | if lookup == nil { 303 | panic("Archetype doesn't have lookup list") 304 | } 305 | 306 | // This indicates that the index needs to be cleaned up and should be skipped in any list processing 307 | lookup.id[loc.index] = InvalidEntity 308 | 309 | // This is used to track the current list of indices that need to be cleaned 310 | lookup.holes = append(lookup.holes, int(loc.index)) 311 | } 312 | 313 | // func (e *archEngine) CleanupHoles(archId archetypeId) { 314 | // lookup := e.lookup[archId] 315 | // if lookup == nil { 316 | // panic("Archetype doesn't have lookup list") 317 | // } 318 | 319 | // for _, index := range lookup.holes { 320 | // // Pop all holes off the end of the archetype 321 | // for { 322 | // lastIndex := len(lookup.id) - 1 323 | // if lastIndex < 0 { 324 | // break 325 | // } // Break if the index we are trying to pop off is -1 326 | // lastId := lookup.id[lastIndex] 327 | // if lastId == InvalidEntity { 328 | // // If the last id is a hole, then slice it off 329 | // lookup.id = lookup.id[:lastIndex] 330 | // for n := range e.compStorage { 331 | // if e.compStorage[n] != nil { 332 | // e.compStorage[n].Delete(archId, lastIndex) 333 | // } 334 | // } 335 | 336 | // continue // Try again 337 | // } 338 | 339 | // break 340 | // } 341 | 342 | // // Check bounds because we may have popped past our original index 343 | // if index >= len(lookup.id) { 344 | // continue 345 | // } 346 | 347 | // // Swap lastIndex (which is not a hole) with index (which is a hole) 348 | // lastIndex := len(lookup.id) - 1 349 | // lastId := lookup.id[lastIndex] 350 | // if lastId == InvalidEntity { 351 | // panic("Bug: This shouldn't happen") 352 | // } 353 | 354 | // // TODO: To fix this, you need to bubble the index swap up to the entLoc map. You probably want to relocate how the "CleanupHoles" gets called. I kinda feel like it shouldn't get executed on write? 355 | 356 | // lookup.id[index] = lastId 357 | // lookup.id = lookup.id[:lastIndex] 358 | // lookup.index.Put(lastId, index) 359 | // for n := range e.compStorage { 360 | // if e.compStorage[n] != nil { 361 | // e.compStorage[n].Delete(archId, index) 362 | // } 363 | // } 364 | // } 365 | 366 | // // Clear holes slice 367 | // lookup.holes = lookup.holes[:0] 368 | // } 369 | 370 | // This is a defragment operation which tries to repack entities closer together 371 | // You wont usually need to do this, but if you delete a lot of entities of one archetype and dont plan 372 | // to add them back, then you can run this to repack 373 | func (w *World) CleanupHoles() { 374 | for lookupIdx, lookup := range w.engine.lookup { 375 | archId := archetypeId(lookupIdx) 376 | 377 | for _, index := range lookup.holes { 378 | // Pop all holes off the end of the archetype 379 | for { 380 | lastIndex := len(lookup.id) - 1 381 | if lastIndex < 0 { 382 | break // Break if the index we are trying to pop off is -1 383 | } 384 | lastId := lookup.id[lastIndex] 385 | if lastId == InvalidEntity { 386 | // If the last id is a hole, then slice it off 387 | lookup.id = lookup.id[:lastIndex] 388 | for n := range w.engine.compStorage { 389 | if w.engine.compStorage[n] != nil { 390 | w.engine.compStorage[n].Delete(archId, lastIndex) 391 | } 392 | } 393 | 394 | continue // Try again 395 | } 396 | 397 | break 398 | } 399 | 400 | // Check bounds because we may have popped past our original index 401 | if index >= len(lookup.id) { 402 | continue 403 | } 404 | 405 | // Swap lastIndex (which is not a hole) with index (which is a hole) 406 | lastIndex := len(lookup.id) - 1 407 | lastId := lookup.id[lastIndex] 408 | if lastId == InvalidEntity { 409 | panic("Bug: This shouldn't happen") 410 | } 411 | 412 | // Update id list 413 | lookup.id[index] = lastId 414 | lookup.id = lookup.id[:lastIndex] 415 | 416 | // Update entity location for this id 417 | newEntLoc := entLoc{archId, uint32(index)} // lookup.index.Put(lastId, index) 418 | w.arch.Put(lastId, newEntLoc) 419 | for n := range w.engine.compStorage { 420 | if w.engine.compStorage[n] != nil { 421 | w.engine.compStorage[n].Delete(archId, index) 422 | } 423 | } 424 | } 425 | 426 | // Clear holes slice 427 | lookup.holes = lookup.holes[:0] 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // TODO - Add ballast and investigate GC pressure? 4 | // TODO - Disable GC: GOCG=-1 go run . 5 | // TODO - manual runtime.GC() 6 | 7 | // Baseline: 8/3/23 8 | // go run . ecs-slow 10000 1 9 | // Iter Time 10 | // 0 0.548914251 11 | // 1 0.563671395 12 | // 2 0.567264819 13 | // 3 0.576352863 14 | // 4 0.571570715 15 | 16 | // Baseline: 9/2/23 (intmap replacement) 17 | // Iter Time 18 | // 0 0.478192213 19 | // 1 0.447929508 20 | // 2 0.449512679 21 | // 3 0.45052541 22 | // 4 0.453497259 23 | 24 | import ( 25 | "fmt" 26 | "log" 27 | "math/rand" 28 | "strconv" 29 | "time" 30 | 31 | "flag" 32 | "os" 33 | "runtime" 34 | "runtime/pprof" 35 | 36 | "github.com/unitoftime/ecs" 37 | ) 38 | 39 | var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") 40 | var memprofile = flag.String("memprofile", "", "write memory profile to `file`") 41 | 42 | type Vec2 struct { 43 | X, Y float64 44 | } 45 | type Position Vec2 46 | type Velocity Vec2 47 | type Collider struct { 48 | Radius float64 49 | } 50 | type Count struct { 51 | Count int32 52 | } 53 | 54 | const iterations = 5 55 | const maxPosition = 100.0 56 | const maxSpeed = 10.0 57 | const maxCollider = 1.0 58 | 59 | func main() { 60 | // Create a large heap allocation of 10 GiB 61 | 62 | flag.Parse() 63 | if *cpuprofile != "" { 64 | f, err := os.Create(*cpuprofile) 65 | if err != nil { 66 | log.Fatal("could not create CPU profile: ", err) 67 | } 68 | defer f.Close() // error handling omitted for example 69 | go func() { 70 | if err := pprof.StartCPUProfile(f); err != nil { 71 | log.Fatal("could not start CPU profile: ", err) 72 | } 73 | }() 74 | defer pprof.StopCPUProfile() 75 | } 76 | 77 | program := os.Args[1] 78 | size, err := strconv.Atoi(os.Args[2]) 79 | if err != nil { 80 | panic(err) 81 | } 82 | colLimitArg, err := strconv.Atoi(os.Args[3]) 83 | if err != nil { 84 | panic(err) 85 | } 86 | collisionLimit := int32(colLimitArg) 87 | 88 | // ballast := make([]byte, 10<<30) 89 | 90 | fmt.Println("Iter", "Time") 91 | switch program { 92 | case "native": 93 | benchNativeComponents(size, collisionLimit) 94 | case "native-split": 95 | benchNativeSplit(size, collisionLimit) 96 | case "ecs-slow": 97 | benchPhysicsAlt(size, collisionLimit) 98 | case "ecs": 99 | benchPhysicsOptimized(size, collisionLimit) 100 | default: 101 | fmt.Printf("Invalid Program name %s\n", program) 102 | fmt.Println("Available Options") 103 | fmt.Println("physics - Runs a physics simulation") 104 | } 105 | 106 | // fmt.Println(len(ballast)) 107 | 108 | if *memprofile != "" { 109 | f, err := os.Create(*memprofile) 110 | if err != nil { 111 | log.Fatal("could not create memory profile: ", err) 112 | } 113 | defer f.Close() // error handling omitted for example 114 | runtime.GC() // get up-to-date statistics 115 | if err := pprof.WriteHeapProfile(f); err != nil { 116 | log.Fatal("could not write memory profile: ", err) 117 | } 118 | } 119 | } 120 | 121 | func createWorld(size int) *ecs.World { 122 | world := ecs.NewWorld() 123 | 124 | for i := 0; i < size; i++ { 125 | id := world.NewId() 126 | 127 | ecs.Write(world, id, 128 | ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 129 | ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 130 | ecs.C(Collider{ 131 | Radius: maxCollider * rand.Float64(), 132 | }), 133 | ecs.C(Count{}), 134 | ) 135 | } 136 | return world 137 | } 138 | 139 | func moveCircles(query *ecs.View2[Position, Velocity], fixedTime float64, maxPosition float64) { 140 | query.MapSlices(func(ids []ecs.Id, pos []Position, vel []Velocity) { 141 | if len(ids) != len(pos) || len(ids) != len(vel) { 142 | panic("ERR") 143 | } 144 | for i := range ids { 145 | if ids[i] == ecs.InvalidEntity { 146 | continue 147 | } 148 | pos[i].X += vel[i].X * fixedTime 149 | pos[i].Y += vel[i].Y * fixedTime 150 | 151 | // Bump into the bounding rect 152 | if pos[i].X <= 0 || pos[i].X >= maxPosition { 153 | vel[i].X = -vel[i].X 154 | } 155 | if pos[i].Y <= 0 || pos[i].Y >= maxPosition { 156 | vel[i].Y = -vel[i].Y 157 | } 158 | } 159 | }) 160 | } 161 | 162 | func checkCollisions(world *ecs.World, 163 | query *ecs.View3[Position, Collider, Count], 164 | innerQuery *ecs.View2[Position, Collider], 165 | collisionLimit int32, deathCount *int) { 166 | 167 | query.MapSlices(func(aId []ecs.Id, aPos []Position, aCol []Collider, aCnt []Count) { 168 | innerQuery.MapSlices(func(bId []ecs.Id, bPos []Position, bCol []Collider) { 169 | if len(aId) != len(aPos) || len(aId) != len(aCol) { 170 | panic("ERR") 171 | } 172 | if len(bId) != len(bPos) || len(bId) != len(bCol) { 173 | panic("ERR") 174 | } 175 | for i := range aId { 176 | if aId[i] == ecs.InvalidEntity { 177 | continue 178 | } 179 | aPos_i := &aPos[i] 180 | aCol_i := &aCol[i] 181 | for j := range bId { 182 | if bId[i] == ecs.InvalidEntity { 183 | continue 184 | } 185 | bPos_j := &bPos[j] 186 | bCol_j := &bCol[j] 187 | if aId[i] == bId[j] { 188 | continue 189 | } // Skip if entity is the same 190 | 191 | dx := aPos_i.X - bPos_j.X 192 | dy := aPos_i.Y - bPos_j.Y 193 | distSq := (dx * dx) + (dy * dy) 194 | 195 | dr := aCol_i.Radius + bCol_j.Radius 196 | drSq := dr * dr 197 | 198 | if drSq > distSq { 199 | aCnt[i].Count++ 200 | } 201 | 202 | // Kill and spawn one 203 | // TODO move to outer loop? 204 | if collisionLimit > 0 && aCnt[i].Count > collisionLimit { 205 | success := ecs.Delete(world, aId[i]) 206 | if success { 207 | *deathCount++ 208 | break 209 | } 210 | } 211 | } 212 | } 213 | }) 214 | }) 215 | } 216 | 217 | func benchPhysicsOptimized(size int, collisionLimit int32) { 218 | world := createWorld(size) 219 | 220 | fixedTime := (15 * time.Millisecond).Seconds() 221 | 222 | moveQuery := ecs.Query2[Position, Velocity](world) 223 | posColCntQuery := ecs.Query3[Position, Collider, Count](world) 224 | posColQuery := ecs.Query2[Position, Collider](world) 225 | 226 | start := time.Now() 227 | dt := time.Since(start) 228 | for iterCount := 0; iterCount < iterations; iterCount++ { 229 | start = time.Now() 230 | 231 | // ecs.ExecuteSystem2(world, func(query *ecs.Query2[Position, Velocity]) { 232 | // moveCircles(query, fixedTime, maxPosition) 233 | // }) 234 | 235 | // deathCount := 0 236 | // ecs.ExecuteSystem2(world, func(query *ecs.Query2[Position, Collider]) { 237 | // checkCollisions(world, query, collisionLimit, &deathCount) 238 | // }) 239 | 240 | { 241 | moveCircles(moveQuery, fixedTime, maxPosition) 242 | } 243 | 244 | deathCount := 0 245 | { 246 | checkCollisions(world, posColCntQuery, posColQuery, collisionLimit, &deathCount) 247 | } 248 | 249 | // fmt.Println("DeathCount:", deathCount) 250 | 251 | // Spawn new entities, one per each entity we deleted 252 | for i := 0; i < deathCount; i++ { 253 | id := world.NewId() 254 | ent := ecs.NewEntity( 255 | ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 256 | ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 257 | ecs.C(Collider{ 258 | Radius: maxCollider * rand.Float64(), 259 | }), 260 | ecs.C(Count{}), 261 | ) 262 | ent.Write(world, id) 263 | } 264 | 265 | // world.Print(0) 266 | 267 | dt = time.Since(start) 268 | fmt.Println(iterCount, dt.Seconds()) 269 | } 270 | 271 | // query := ecs.Query1[Count](world) 272 | // query.MapId(func(id ecs.Id, count *Count) { 273 | // fmt.Println(id, count.Count) 274 | // }) 275 | } 276 | 277 | /* 278 | 974 1031 279 | 975 625 280 | 976 787 281 | 977 208 282 | 978 1601 283 | 979 1243 284 | 980 167 285 | 981 108 286 | 982 1040 287 | 983 500 288 | 984 637 289 | 985 1011 290 | 986 830 291 | 987 1247 292 | 988 901 293 | 989 1597 294 | 990 418 295 | 991 767 296 | 992 951 297 | 993 1252 298 | 994 948 299 | 995 194 300 | 996 290 301 | 997 181 302 | 998 1276 303 | 999 858 304 | 1000 789 305 | 1001 638 306 | */ 307 | 308 | // func benchPhysics(size int, collisionLimit int32) { 309 | // world := createWorld(size) 310 | 311 | // start := time.Now() 312 | // dt := time.Since(start) 313 | // fixedTime := (15 * time.Millisecond).Seconds() 314 | // for i := 0; i < iterations; i++ { 315 | // start = time.Now() 316 | 317 | // // Update positions 318 | // ecs.Map2(world, func(id ecs.Id, position *Position, velocity *Velocity) { 319 | // position.X += velocity.X * fixedTime 320 | // position.Y += velocity.Y * fixedTime 321 | 322 | // // Bump into the bounding rect 323 | // if position.X <= 0 || position.X >= maxPosition { 324 | // velocity.X = -velocity.X 325 | // } 326 | // if position.Y <= 0 || position.Y >= maxPosition { 327 | // velocity.Y = -velocity.Y 328 | // } 329 | // }) 330 | 331 | // // Check collisions, increment the count if a collision happens 332 | // deathCount := 0 333 | // ecs.Map3(world, func(aId ecs.Id, aPos *Position, aCol *Collider, aCnt *Count) { 334 | // ecs.Map2(world, func(bId ecs.Id, bPos *Position, bCol *Collider) { 335 | // if aId == bId { return } // Skip if entity is the same 336 | 337 | // dx := aPos.X - bPos.X 338 | // dy := aPos.Y - bPos.Y 339 | // distSq := (dx * dx) + (dy * dy) 340 | 341 | // dr := aCol.Radius + bCol.Radius 342 | // drSq := dr * dr 343 | 344 | // if drSq > distSq { 345 | // aCnt.Count++ 346 | // } 347 | 348 | // // Kill and spawn one 349 | // // TODO move to outer loop? 350 | // if collisionLimit > 0 && aCnt.Count > collisionLimit { 351 | // success := ecs.Delete(world, aId) 352 | // if success { 353 | // deathCount++ 354 | // return 355 | // } 356 | // } 357 | // }) 358 | // }) 359 | 360 | // // Spawn new entities, one per each entity we deleted 361 | // for i := 0; i < deathCount; i++ { 362 | // id := world.NewId() 363 | // ent := ecs.NewEntity( 364 | // ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 365 | // ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 366 | // ecs.C(Collider{ 367 | // Radius: maxCollider * rand.Float64(), 368 | // }), 369 | // ecs.C(Count{}), 370 | // ) 371 | // ecs.WriteEntity(world, id, ent) 372 | // } 373 | 374 | // // world.Print(0) 375 | 376 | // dt = time.Since(start) 377 | // fmt.Println(i, dt.Seconds()) 378 | // } 379 | 380 | // // ecs.Map(world, func(id ecs.Id, collider *Collider) { 381 | // // fmt.Println(id, collider.Count) 382 | // // }) 383 | // } 384 | 385 | func benchPhysicsAlt(size int, collisionLimit int32) { 386 | world := createWorld(size) 387 | 388 | posVelQuery := ecs.Query2[Position, Velocity](world) 389 | posColQuery := ecs.Query2[Position, Collider](world) 390 | posColCntQuery := ecs.Query3[Position, Collider, Count](world) 391 | 392 | // TODO - maybe one day 393 | // posVelSystem := ecs.NewSystemFunc(world *ecs.World, func(query ecs.View2[Position, Velocity])) 394 | 395 | start := time.Now() 396 | dt := time.Since(start) 397 | fixedTime := (15 * time.Millisecond).Seconds() 398 | for i := 0; i < iterations; i++ { 399 | start = time.Now() 400 | 401 | // Update positions 402 | posVelQuery.MapId(func(id ecs.Id, position *Position, velocity *Velocity) { 403 | // ecs.Map2(world, func(id ecs.Id, position *Position, velocity *Velocity) { 404 | position.X += velocity.X * fixedTime 405 | position.Y += velocity.Y * fixedTime 406 | 407 | // Bump into the bounding rect 408 | if position.X <= 0 || position.X >= maxPosition { 409 | velocity.X = -velocity.X 410 | } 411 | if position.Y <= 0 || position.Y >= maxPosition { 412 | velocity.Y = -velocity.Y 413 | } 414 | }) 415 | 416 | // Check collisions, increment the count if a collision happens 417 | deathCount := 0 418 | posColCntQuery.MapId(func(aId ecs.Id, aPos *Position, aCol *Collider, aCnt *Count) { 419 | posColQuery.MapId(func(bId ecs.Id, bPos *Position, bCol *Collider) { 420 | if aId == bId { 421 | return 422 | } // Skip if entity is the same 423 | 424 | dx := aPos.X - bPos.X 425 | dy := aPos.Y - bPos.Y 426 | distSq := (dx * dx) + (dy * dy) 427 | 428 | dr := aCol.Radius + bCol.Radius 429 | drSq := dr * dr 430 | 431 | if drSq > distSq { 432 | aCnt.Count++ 433 | } 434 | 435 | // Kill and spawn one 436 | // TODO move to outer loop? 437 | if collisionLimit > 0 && aCnt.Count > collisionLimit { 438 | success := ecs.Delete(world, aId) 439 | if success { 440 | deathCount++ 441 | return 442 | } 443 | } 444 | }) 445 | }) 446 | 447 | // Spawn new entities, one per each entity we deleted 448 | for i := 0; i < deathCount; i++ { 449 | id := world.NewId() 450 | ent := ecs.NewEntity( 451 | ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 452 | ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 453 | ecs.C(Collider{ 454 | Radius: maxCollider * rand.Float64(), 455 | }), 456 | ecs.C(Count{}), 457 | ) 458 | ent.Write(world, id) 459 | } 460 | 461 | // world.Print(0) 462 | 463 | dt = time.Since(start) 464 | fmt.Println(i, dt.Seconds()) 465 | } 466 | 467 | // query := ecs.Query1[Count](world) 468 | // query.MapId(func(id ecs.Id, count *Count) { 469 | // fmt.Println(id, count.Count) 470 | // }) 471 | } 472 | 473 | /* 474 | func benchPhysicsOptimized(size int, collisionLimit int32) { 475 | iterations := 1000 476 | 477 | world := ecs.NewWorld() 478 | maxSpeed := 10.0 479 | maxPosition := 100.0 480 | maxCollider := 1.0 481 | 482 | for i := 0; i < size; i++ { 483 | id := world.NewId() 484 | ent := ecs.NewEntity( 485 | ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 486 | ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 487 | ecs.C(Collider{ 488 | Radius: maxCollider * rand.Float64(), 489 | Count: 0, 490 | }), 491 | ) 492 | ecs.WriteEntity(world, id, ent) 493 | } 494 | 495 | loopCounter := 0 496 | fixedTime := (15 * time.Millisecond).Seconds() 497 | 498 | start := time.Now() 499 | dt := time.Since(start) 500 | for iterCount := 0; iterCount < iterations; iterCount++ { 501 | start = time.Now() 502 | 503 | { 504 | // view := ecs.ViewAll2[Position, Velocity](world) 505 | // for iter := view.Iterate(); iter.Ok(); { 506 | // _, pos, vel := iter.Next() 507 | // // fmt.Println("0", iter) 508 | // pos.X += vel.X * fixedTime 509 | // pos.Y += vel.Y * fixedTime 510 | 511 | // // Bump into the bounding rect 512 | // if pos.X <= 0 || pos.X >= maxPosition { 513 | // vel.X = -vel.X 514 | // } 515 | // if pos.Y <= 0 || pos.Y >= maxPosition { 516 | // vel.Y = -vel.Y 517 | // } 518 | // loopCounter++ 519 | // } 520 | 521 | 522 | view := ecs.ViewAll2[Position, Velocity](world) 523 | for view.Ok() { 524 | _, pos, vel := view.IterChunkClean() 525 | if len(pos) != len(vel) { panic("ERR") } 526 | for j := range pos { 527 | pos[j].X += vel[j].X * fixedTime 528 | pos[j].Y += vel[j].Y * fixedTime 529 | 530 | // Bump into the bounding rect 531 | if pos[j].X <= 0 || pos[j].X >= maxPosition { 532 | vel[j].X = -vel[j].X 533 | } 534 | if pos[j].Y <= 0 || pos[j].Y >= maxPosition { 535 | vel[j].Y = -vel[j].Y 536 | } 537 | loopCounter++ 538 | } 539 | } 540 | } 541 | 542 | 543 | // deathCount := 0 544 | // view := ecs.ViewAll2[Position, Collider](world) 545 | // // view2 := ecs.ViewAll2[Position, Collider](world) 546 | // for iter := view.Iterate(); iter.Ok(); { 547 | // aId, aPos, aCol := iter.Next() 548 | // // fmt.Println("1", iter, aId, aPos, aCol) 549 | // // for view.Ok() { 550 | // // aId, aPos, aCol := view.Iter4() 551 | 552 | // for iter2 := view.Iterate(); iter2.Ok(); { 553 | // bId, bPos, bCol := iter2.Next() 554 | // // fmt.Println("2", iter2, bId, bPos, bCol) 555 | 556 | 557 | // // view2.Reset() 558 | // // for view2.Ok() { 559 | // // bId, bPos, bCol := view2.Iter4() 560 | 561 | // if aId == bId { continue } // Skip if entity is the same 562 | 563 | // dx := aPos.X - bPos.X 564 | // dy := aPos.Y - bPos.Y 565 | // distSq := (dx * dx) + (dy * dy) 566 | 567 | // dr := aCol.Radius + bCol.Radius 568 | // drSq := dr * dr 569 | 570 | // if drSq > distSq { 571 | // aCol.Count++ 572 | // } 573 | 574 | // // Kill and spawn one 575 | // // TODO move to outer loop? 576 | // if collisionLimit > 0 && aCol.Count > collisionLimit { 577 | // success := ecs.Delete(world, aId) 578 | // if success { 579 | // deathCount++ 580 | // break 581 | // } 582 | // } 583 | 584 | // loopCounter++ 585 | // } 586 | // } 587 | 588 | 589 | // !!!Fastest!!!! 590 | // Check collisions, increment the count if a collision happens 591 | deathCount := 0 592 | view := ecs.ViewAll2[Position, Collider](world) 593 | view2 := ecs.ViewAll2[Position, Collider](world) 594 | for view.Ok() { 595 | ids, pos, col := view.IterChunkClean() 596 | 597 | if len(ids) != len(pos) || len(ids) != len(col) { panic ("ERROR") } 598 | for j := range ids { 599 | aId := ids[j] 600 | aPos := &pos[j] 601 | aCol := &col[j] 602 | 603 | view2.Reset() 604 | for view2.Ok() { 605 | targIdList, targPosList, targCol := view2.IterChunkClean() 606 | 607 | if len(targIdList) != len(targPosList) || len(targIdList) != len(targCol) { panic ("ERROR") } 608 | for jj := range targIdList { 609 | bId := targIdList[jj] 610 | bPos := &targPosList[jj] 611 | bCol := &targCol[jj] 612 | 613 | if aId == bId { continue } // Skip if entity is the same 614 | 615 | dx := aPos.X - bPos.X 616 | dy := aPos.Y - bPos.Y 617 | distSq := (dx * dx) + (dy * dy) 618 | 619 | dr := aCol.Radius + bCol.Radius 620 | drSq := dr * dr 621 | 622 | if drSq > distSq { 623 | aCol.Count++ 624 | } 625 | 626 | // Kill and spawn one 627 | // TODO move to outer loop? 628 | if collisionLimit > 0 && aCol.Count > collisionLimit { 629 | success := ecs.Delete(world, aId) 630 | if success { 631 | deathCount++ 632 | break 633 | } 634 | } 635 | 636 | loopCounter++ 637 | } 638 | } 639 | } 640 | } 641 | 642 | // Spawn new entities, one per each entity we deleted 643 | for i := 0; i < deathCount; i++ { 644 | id := world.NewId() 645 | ent := ecs.NewEntity( 646 | ecs.C(Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()}), 647 | ecs.C(Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()}), 648 | ecs.C(Collider{ 649 | Radius: maxCollider * rand.Float64(), 650 | Count: 0, 651 | }), 652 | ) 653 | ecs.WriteEntity(world, id, ent) 654 | } 655 | 656 | // world.Print(0) 657 | 658 | dt = time.Since(start) 659 | fmt.Println(iterCount, dt, loopCounter) 660 | loopCounter = 0 661 | } 662 | 663 | ecs.Map(world, func(id ecs.Id, collider *Collider) { 664 | fmt.Println(id, collider.Count) 665 | }) 666 | } 667 | */ 668 | 669 | func benchNativeComponents(size int, collisionLimit int32) { 670 | // [uint64] 671 | // [{float64, float64}] 672 | // [{float64, float64}] 673 | // [{float64, int32}] 674 | 675 | // [uint64, uint64] 676 | // [{float64, float64}, {float64, float64}] 677 | // [{float64, float64}, {float64, float64}] 678 | // [{float64, int32}, {float64, int32}] 679 | ids := make([]ecs.Id, size, size) 680 | pos := make([]Position, size, size) 681 | vel := make([]Velocity, size, size) 682 | col := make([]Collider, size, size) 683 | cnt := make([]Count, size, size) 684 | 685 | for i := 0; i < size; i++ { 686 | ids[i] = ecs.Id(i + 2) 687 | pos[i] = Position{maxPosition * rand.Float64(), maxPosition * rand.Float64()} 688 | vel[i] = Velocity{maxSpeed * rand.Float64(), maxSpeed * rand.Float64()} 689 | col[i] = Collider{ 690 | Radius: maxCollider * rand.Float64(), 691 | } 692 | cnt[i] = Count{} 693 | } 694 | 695 | start := time.Now() 696 | dt := time.Since(start) 697 | fixedTime := (15 * time.Millisecond).Seconds() 698 | for iterCount := 0; iterCount < iterations; iterCount++ { 699 | start = time.Now() 700 | 701 | // Update positions 702 | for i := range ids { 703 | pos[i].X += vel[i].X * fixedTime 704 | pos[i].Y += vel[i].Y * fixedTime 705 | 706 | // Bump into the bounding rect 707 | if pos[i].X <= 0 || pos[i].X >= maxPosition { 708 | vel[i].X = -vel[i].X 709 | } 710 | if pos[i].Y <= 0 || pos[i].Y >= maxPosition { 711 | vel[i].Y = -vel[i].Y 712 | } 713 | } 714 | 715 | // Check collisions, increment the count if a collision happens 716 | deathCount := 0 717 | for i := range ids { 718 | aId := ids[i] 719 | aPos := &pos[i] 720 | aCol := &col[i] 721 | aCnt := &cnt[i] 722 | for j := range ids { 723 | bId := ids[j] 724 | bPos := &pos[j] 725 | bCol := &col[j] 726 | 727 | if aId == bId { 728 | continue 729 | } // Skip if entity is the same 730 | 731 | dx := aPos.X - bPos.X 732 | dy := aPos.Y - bPos.Y 733 | distSq := (dx * dx) + (dy * dy) 734 | 735 | dr := aCol.Radius + bCol.Radius 736 | drSq := dr * dr 737 | 738 | if drSq > distSq { 739 | aCnt.Count++ 740 | } 741 | 742 | // Kill and spawn one 743 | // TODO move to outer loop? 744 | if collisionLimit > 0 && aCnt.Count > collisionLimit { 745 | deathCount++ 746 | } 747 | } 748 | } 749 | 750 | dt = time.Since(start) 751 | fmt.Println(iterCount, dt.Seconds()) 752 | } 753 | 754 | // for i := range ids { 755 | // fmt.Println(ids[i], cnt[i].Count) 756 | // } 757 | } 758 | 759 | // struct myStruct { 760 | // X float64 761 | // Y float64 762 | // } 763 | 764 | // myarray []myStruct 765 | 766 | // myArrayX []float64 767 | // myArrayY []float64 768 | 769 | // [uint64] 770 | // [{float64, float64}] 771 | // [{float64, float64}] 772 | // [{float64, int32}] 773 | 774 | // Holes [bool] [true] ... 775 | // Id [uint64] [uint64] ... 776 | // PosX [float64] [float64] ... 777 | // PosY [float64] [float64] ... 778 | // VelX [float64] [float64] ... 779 | // VelY [float64] [float64] ... 780 | // ColRadius [float64] [float64] ... 781 | // ColCount [int32] [int32] ... 782 | 783 | // Test with this new memory layout 784 | // [uint64] 785 | // PosX [float64] 786 | // PosY [float64] 787 | // VelX [float64] 788 | // VelY [float64] 789 | // ColRadius [float64] 790 | // ColCount [int32] 791 | func benchNativeSplit(size int, collisionLimit int32) { 792 | ids := make([]ecs.Id, size, size) 793 | posX := make([]float64, size, size) 794 | posY := make([]float64, size, size) 795 | velX := make([]float64, size, size) 796 | velY := make([]float64, size, size) 797 | col := make([]float64, size, size) 798 | cnt := make([]int32, size, size) 799 | 800 | for i := 0; i < size; i++ { 801 | ids[i] = ecs.Id(i + 2) 802 | posX[i] = maxPosition * rand.Float64() 803 | posY[i] = maxPosition * rand.Float64() 804 | velX[i] = maxSpeed * rand.Float64() 805 | velY[i] = maxSpeed * rand.Float64() 806 | col[i] = maxCollider * rand.Float64() 807 | cnt[i] = 0 808 | } 809 | 810 | fixedTime := 0.015 811 | for iterCount := 0; iterCount < iterations; iterCount++ { 812 | start := time.Now() 813 | // Update positions 814 | for i := range ids { 815 | posX[i] += velX[i] * fixedTime 816 | posY[i] += velY[i] * fixedTime 817 | 818 | // Bump into the bounding rect 819 | if posX[i] <= 0 || posX[i] >= maxPosition { 820 | velX[i] = -velX[i] 821 | } 822 | if posY[i] <= 0 || posY[i] >= maxPosition { 823 | velY[i] = -velY[i] 824 | } 825 | } 826 | 827 | // Check collisions, increment the count if a collision happens 828 | deathCount := 0 829 | for i := range ids { 830 | aId := ids[i] 831 | aPosX := &posX[i] 832 | aPosY := &posY[i] 833 | aCol := &col[i] 834 | for j := range ids { 835 | bId := ids[j] 836 | bPosX := &posX[j] 837 | bPosY := &posY[j] 838 | bCol := &col[j] 839 | 840 | if aId == bId { 841 | continue 842 | } // Skip if entity is the same 843 | 844 | dx := *aPosX - *bPosX 845 | dy := *aPosY - *bPosY 846 | distSq := (dx * dx) + (dy * dy) 847 | 848 | dr := *aCol + *bCol 849 | drSq := dr * dr 850 | 851 | if drSq > distSq { 852 | cnt[i]++ 853 | } 854 | 855 | if collisionLimit > 0 && cnt[i] > collisionLimit { 856 | deathCount++ 857 | } 858 | } 859 | } 860 | 861 | dt := time.Since(start) 862 | fmt.Println(iterCount, dt.Seconds()) 863 | } 864 | 865 | // for i := range ids { 866 | // fmt.Println(ids[i], cnt[i]) 867 | // } 868 | } 869 | 870 | // [ id, id , id ] 871 | // [ pos, pos, pos ] 872 | // [ vel, vel, ] 873 | // [ col, col, col ] 874 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "testing" 4 | 5 | func TestMainRewriteArch(t *testing.T) { 6 | world := NewWorld() 7 | 8 | id := world.NewId() 9 | 10 | // --- position --- 11 | { 12 | val, ok := Read[position](world, id) 13 | check(t, !ok) 14 | compare(t, position{}, val) 15 | world.Write(id, C(position{1, 1, 1})) 16 | 17 | val, ok = Read[position](world, id) 18 | check(t, ok) 19 | compare(t, position{1, 1, 1}, val) 20 | } 21 | 22 | // --- Velocity --- 23 | { 24 | val, ok := Read[velocity](world, id) 25 | check(t, !ok) 26 | compare(t, velocity{}, val) 27 | world.Write(id, C(velocity{2, 2, 2})) 28 | val, ok = Read[velocity](world, id) 29 | check(t, ok) 30 | compare(t, velocity{2, 2, 2}, val) 31 | } 32 | 33 | // --- Acceleration --- 34 | { 35 | val, ok := Read[acceleration](world, id) 36 | check(t, !ok) 37 | compare(t, acceleration{}, val) 38 | world.Write(id, C(acceleration{3, 3, 3})) 39 | val, ok = Read[acceleration](world, id) 40 | check(t, ok) 41 | compare(t, acceleration{3, 3, 3}, val) 42 | } 43 | 44 | // --- Radius --- 45 | { 46 | val, ok := Read[radius](world, id) 47 | check(t, !ok) 48 | compare(t, radius{}, val) 49 | world.Write(id, C(radius{4})) 50 | val, ok = Read[radius](world, id) 51 | check(t, ok) 52 | compare(t, radius{4}, val) 53 | } 54 | } 55 | 56 | func BenchmarkMainRewriteArch(b *testing.B) { 57 | world := NewWorld() 58 | b.ResetTimer() 59 | 60 | for n := 0; n < b.N; n++ { 61 | id := world.NewId() 62 | world.Write(id, C(position{})) 63 | world.Write(id, C(velocity{})) 64 | world.Write(id, C(acceleration{})) 65 | world.Write(id, C(radius{})) 66 | } 67 | } 68 | 69 | // Old: BenchmarkMainRewriteDownArch-12 375435 3022 ns/op 864 B/op 31 allocs/op 70 | // New: BenchmarkMainRewriteDownArch-12 891117 1373 ns/op 360 B/op 8 allocs/op 71 | func BenchmarkMainRewriteDownArch(b *testing.B) { 72 | world := NewWorld() 73 | b.ResetTimer() 74 | 75 | for n := 0; n < b.N; n++ { 76 | id := world.NewId() 77 | world.Write(id, C(position{}), C(velocity{}), C(acceleration{}), C(radius{})) 78 | 79 | DeleteComponent(world, id, C(position{})) 80 | DeleteComponent(world, id, C(velocity{})) 81 | DeleteComponent(world, id, C(acceleration{})) 82 | DeleteComponent(world, id, C(radius{})) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "testing" 4 | 5 | var addEntSize = 1000 6 | 7 | func BenchmarkAddEntityWrite(b *testing.B) { 8 | world := NewWorld() 9 | 10 | b.ResetTimer() 11 | 12 | for n := 0; n < b.N; n++ { 13 | for i := 0; i < addEntSize; i++ { 14 | id := world.NewId() 15 | 16 | Write(world, id, 17 | C(position{1, 2, 3}), 18 | C(velocity{4, 5, 6}), 19 | C(acceleration{7, 8, 9}), 20 | C(radius{10}), 21 | ) 22 | } 23 | } 24 | } 25 | 26 | func BenchmarkAddEntitySingle(b *testing.B) { 27 | world := NewWorld() 28 | 29 | b.ResetTimer() 30 | 31 | for n := 0; n < b.N; n++ { 32 | for i := 0; i < addEntSize; i++ { 33 | ent := NewEntity( 34 | C(position{1, 2, 3}), 35 | C(velocity{4, 5, 6}), 36 | C(acceleration{7, 8, 9}), 37 | C(radius{10}), 38 | ) 39 | 40 | id := world.NewId() 41 | ent.Write(world, id) 42 | } 43 | } 44 | } 45 | 46 | func BenchmarkAddEntityMemCached(b *testing.B) { 47 | world := NewWorld() 48 | 49 | b.ResetTimer() 50 | 51 | ent := NewEntity() 52 | 53 | for n := 0; n < b.N; n++ { 54 | for i := 0; i < addEntSize; i++ { 55 | ent.Clear() 56 | ent.Add( 57 | C(position{1, 2, 3}), 58 | C(velocity{4, 5, 6}), 59 | C(acceleration{7, 8, 9}), 60 | C(radius{10}), 61 | ) 62 | id := world.NewId() 63 | ent.Write(world, id) 64 | } 65 | } 66 | } 67 | 68 | func BenchmarkAddEntityCached(b *testing.B) { 69 | world := NewWorld() 70 | 71 | b.ResetTimer() 72 | 73 | ent := NewEntity( 74 | C(position{1, 2, 3}), 75 | C(velocity{4, 5, 6}), 76 | C(acceleration{7, 8, 9}), 77 | C(radius{10}), 78 | ) 79 | 80 | for n := 0; n < b.N; n++ { 81 | for i := 0; i < addEntSize; i++ { 82 | id := world.NewId() 83 | ent.Write(world, id) 84 | } 85 | } 86 | } 87 | 88 | func BenchmarkAddTry2EntitySameCachedThenWrite(b *testing.B) { 89 | world := NewWorld() 90 | 91 | b.ResetTimer() 92 | 93 | query := Query4[position, velocity, acceleration, radius](world) 94 | ent := NewEntity( 95 | C(position{}), 96 | C(velocity{}), 97 | C(acceleration{}), 98 | C(radius{}), 99 | ) 100 | 101 | for n := 0; n < b.N; n++ { 102 | for i := 0; i < addEntSize; i++ { 103 | id := world.NewId() 104 | ent.Write(world, id) 105 | 106 | pos, vel, acc, rad := query.Read(id) 107 | *pos = position{1, 2, 3} 108 | *vel = velocity{4, 5, 6} 109 | *acc = acceleration{7, 8, 9} 110 | *rad = radius{10} 111 | } 112 | } 113 | } 114 | 115 | func BenchmarkCompareBaselineEntityWrite(b *testing.B) { 116 | world := NewWorld() 117 | 118 | b.ResetTimer() 119 | 120 | id := world.NewId() 121 | 122 | ent := NewEntity( 123 | C(position{1, 2, 3}), 124 | C(velocity{4, 5, 6}), 125 | C(acceleration{7, 8, 9}), 126 | C(radius{10}), 127 | ) 128 | 129 | for n := 0; n < b.N; n++ { 130 | for i := 0; i < addEntSize; i++ { 131 | ent.Write(world, id) 132 | } 133 | } 134 | } 135 | 136 | func BenchmarkCompareBaselineQueryWrite(b *testing.B) { 137 | world := NewWorld() 138 | 139 | query := Query4[position, velocity, acceleration, radius](world) 140 | 141 | b.ResetTimer() 142 | 143 | id := world.NewId() 144 | 145 | pp := position{1, 2, 3} 146 | vv := velocity{4, 5, 6} 147 | aa := acceleration{7, 8, 9} 148 | rr := radius{10} 149 | 150 | Write(world, id, C(pp), C(vv), C(aa), C(rr)) 151 | 152 | for n := 0; n < b.N; n++ { 153 | for i := 0; i < addEntSize; i++ { 154 | p, v, a, r := query.Read(id) 155 | *p = pp 156 | *v = vv 157 | *a = aa 158 | *r = rr 159 | } 160 | } 161 | } 162 | 163 | func BenchmarkCompare1(b *testing.B) { 164 | world := NewWorld() 165 | 166 | b.ResetTimer() 167 | 168 | id := world.NewId() 169 | 170 | for n := 0; n < b.N; n++ { 171 | for i := 0; i < addEntSize; i++ { 172 | ent := NewEntity( 173 | C(position{1, 2, 3}), 174 | C(velocity{4, 5, 6}), 175 | C(acceleration{7, 8, 9}), 176 | C(radius{10}), 177 | ) 178 | 179 | ent.Write(world, id) 180 | } 181 | } 182 | } 183 | 184 | func BenchmarkCompare2(b *testing.B) { 185 | world := NewWorld() 186 | 187 | b.ResetTimer() 188 | 189 | id := world.NewId() 190 | 191 | p := C(position{1, 2, 3}) 192 | v := C(velocity{4, 5, 6}) 193 | a := C(acceleration{7, 8, 9}) 194 | c := C(radius{10}) 195 | 196 | for n := 0; n < b.N; n++ { 197 | for i := 0; i < addEntSize; i++ { 198 | Write(world, id, p, v, a, c) 199 | } 200 | } 201 | } 202 | 203 | func BenchmarkAllocateBaseline(b *testing.B) { 204 | world := NewWorld() 205 | 206 | b.ResetTimer() 207 | 208 | for n := 0; n < b.N; n++ { 209 | for i := 0; i < addEntSize; i++ { 210 | id := world.NewId() 211 | 212 | ent := NewEntity( 213 | C(position{1, 2, 3}), 214 | C(velocity{4, 5, 6}), 215 | C(acceleration{7, 8, 9}), 216 | C(radius{10}), 217 | ) 218 | 219 | ent.Write(world, id) 220 | } 221 | } 222 | } 223 | 224 | // Note: Removed bc it uses internal code path: allocateMove -> causing it to potentially break hooks 225 | // func BenchmarkAllocateQuery(b *testing.B) { 226 | // world := NewWorld() 227 | 228 | // query := Query4[position, velocity, acceleration, radius](world) 229 | 230 | // b.ResetTimer() 231 | 232 | // mask := buildArchMask( 233 | // C(position{}), 234 | // C(velocity{}), 235 | // C(acceleration{}), 236 | // C(radius{}), 237 | // ) 238 | 239 | // for n := 0; n < b.N; n++ { 240 | // for i := 0; i < addEntSize; i++ { 241 | // id := world.NewId() 242 | // world.allocateMove(id, mask) 243 | 244 | // p, v, a, r := query.Read(id) 245 | // *p = position{1, 2, 3} 246 | // *v = velocity{4, 5, 6} 247 | // *a = acceleration{7, 8, 9} 248 | // *r = radius{10} 249 | // } 250 | // } 251 | // } 252 | 253 | // func BenchmarkAllocateQueryNoQuery(b *testing.B) { 254 | // world := NewWorld() 255 | 256 | // b.ResetTimer() 257 | 258 | // mask := buildArchMask( 259 | // C(position{}), 260 | // C(velocity{}), 261 | // C(acceleration{}), 262 | // C(radius{}), 263 | // ) 264 | // // archId := world.engine.getArchetypeId(mask) 265 | 266 | // for n := 0; n < b.N; n++ { 267 | // for i := 0; i < addEntSize; i++ { 268 | // id := world.NewId() 269 | // newLoc := world.allocateMove(id, mask) 270 | 271 | // // // Note: Slightly faster option. Actually, I'm not so sure 272 | // // p := positionId.getPtr(world.engine, archId, index) 273 | // // v := velocityId.getPtr(world.engine, archId, index) 274 | // // a := accelerationId.getPtr(world.engine, archId, index) 275 | // // r := radiusId.getPtr(world.engine, archId, index) 276 | // // *p = position{1, 2, 3} 277 | // // *v = velocity{4, 5, 6} 278 | // // *a = acceleration{7, 8, 9} 279 | // // *r = radius{10} 280 | 281 | // // positionId.writeVal(world.engine, archId, index, position{1, 2, 3}) 282 | // // velocityId.writeVal(world.engine, archId, index, velocity{1, 2, 3}) 283 | // // accelerationId.writeVal(world.engine, archId, index, acceleration{1, 2, 3}) 284 | // // radiusId.writeVal(world.engine, archId, index, radius{10}) 285 | 286 | // wd := W{ 287 | // engine: world.engine, 288 | // archId: newLoc.archId, 289 | // index: int(newLoc.index), 290 | // } 291 | // positionId.WriteVal(wd, position{1, 2, 3}) 292 | // velocityId.WriteVal(wd, velocity{1, 2, 3}) 293 | // accelerationId.WriteVal(wd, acceleration{1, 2, 3}) 294 | // radiusId.WriteVal(wd, radius{10}) 295 | 296 | // } 297 | // } 298 | // } 299 | 300 | // I think this is a good start. it basically makes it so you can just allocate archetypes and write them later rather than having to pass []component lists around everywhere 301 | // Maybe something with Write1(), Write2(), Write3(), Generation? ... <- you'd still have to call 'name' and lookup the mask every frame 302 | 303 | // func BenchmarkAllocateBundleTry2(b *testing.B) { 304 | // world := NewWorld() 305 | 306 | // bun := NewBundleTry2[position, velocity, acceleration, radius](world) 307 | 308 | // b.ResetTimer() 309 | 310 | // for n := 0; n < b.N; n++ { 311 | // for i := 0; i < addEntSize; i++ { 312 | // id := world.NewId() 313 | // bun.Write(id, 314 | // &position{1, 2, 3}, 315 | // &velocity{4, 5, 6}, 316 | // &acceleration{7, 8, 9}, 317 | // &radius{10}, 318 | // ) 319 | // } 320 | // } 321 | // } 322 | 323 | // Note: Was slow 324 | // func BenchmarkAllocateBundler(b *testing.B) { 325 | // world := NewWorld() 326 | 327 | // bun := &Bundler{} 328 | 329 | // b.ResetTimer() 330 | 331 | // for n := 0; n < b.N; n++ { 332 | // for i := 0; i < addEntSize; i++ { 333 | 334 | // bun.Clear() 335 | // WriteComponent(bun, position{1, 2, 3}) 336 | // WriteComponent(bun, velocity{4, 5, 6}) 337 | // WriteComponent(bun, acceleration{7, 8, 9}) 338 | // WriteComponent(bun, radius{10}) 339 | 340 | // id := world.NewId() 341 | // bun.Write(world, id) 342 | // } 343 | // } 344 | // } 345 | 346 | // func BenchmarkAllocateBundler2(b *testing.B) { 347 | // world := NewWorld() 348 | 349 | // bun := &Bundler{} 350 | 351 | // b.ResetTimer() 352 | 353 | // pos := C(position{1, 2, 3}) 354 | // vel := C(velocity{4, 5, 6}) 355 | // acc := C(acceleration{7, 8, 9}) 356 | // rad := C(radius{10}) 357 | 358 | // for n := 0; n < b.N; n++ { 359 | // for i := 0; i < addEntSize; i++ { 360 | // bun.Clear() 361 | 362 | // pos.Comp = position{1, 2, 3} 363 | 364 | // bun.Add(&pos) 365 | // bun.Add(&vel) 366 | // bun.Add(&acc) 367 | // bun.Add(&rad) 368 | 369 | // id := world.NewId() 370 | // bun.Write(world, id) 371 | // } 372 | // } 373 | // } 374 | 375 | // type outerBundle struct { 376 | // myBundle myBundle 377 | // } 378 | 379 | // func (m outerBundle) Unbundle(bun *Bundler) { 380 | // m.myBundle.Unbundle(bun) 381 | // } 382 | 383 | //-------------------------------------------------------------------------------- 384 | 385 | type myBundle struct { 386 | pos position 387 | vel velocity 388 | acc acceleration 389 | rad radius 390 | } 391 | 392 | // Note: This was interesting, but ended up being pretty slow 393 | // func (m myBundle) BundleSeq() iter.Seq[Component] { 394 | // return func(yield func(Component) bool) { 395 | // if !yield(positionId.With(m.pos)) { return } 396 | // if !yield(velocityId.With(m.vel)) { return } 397 | // if !yield(accelerationId.With(m.acc)) { return } 398 | // if !yield(radiusId.With(m.rad)) { return } 399 | // } 400 | // } 401 | 402 | // func (m myBundle) Unbundle(bun *Bundler) { 403 | // positionId.With(m.pos).Unbundle(bun) 404 | // velocityId.With(m.vel).Unbundle(bun) 405 | // accelerationId.With(m.acc).Unbundle(bun) 406 | // radiusId.With(m.rad).Unbundle(bun) 407 | 408 | // // positionId.UnbundleVal(bun, m.pos) 409 | // // velocityId.UnbundleVal(bun, m.vel) 410 | // // accelerationId.UnbundleVal(bun, m.acc) 411 | // // radiusId.UnbundleVal(bun, m.rad) 412 | // } 413 | 414 | func (m myBundle) CompWrite(wd W) { 415 | m.pos.CompWrite(wd) 416 | m.vel.CompWrite(wd) 417 | m.acc.CompWrite(wd) 418 | m.rad.CompWrite(wd) 419 | } 420 | 421 | func BenchmarkAllocateManual(b *testing.B) { 422 | world := NewWorld() 423 | 424 | bun := &Bundler{} 425 | 426 | b.ResetTimer() 427 | 428 | for n := 0; n < b.N; n++ { 429 | for i := 0; i < addEntSize; i++ { 430 | m := myBundle{ 431 | pos: position{1, 2, 3}, 432 | vel: velocity{1, 2, 3}, 433 | acc: acceleration{1, 2, 3}, 434 | rad: radius{1}, 435 | } 436 | 437 | bun.Clear() 438 | unbundle(m, bun) 439 | id := world.NewId() 440 | bun.Write(world, id) 441 | } 442 | } 443 | } 444 | 445 | // func BenchmarkAllocateBundle4(b *testing.B) { 446 | // world := NewWorld() 447 | 448 | // var myBundle2 = NewBundle4[position, velocity, acceleration, radius]() 449 | 450 | // bun := &Bundler{} 451 | 452 | // b.ResetTimer() 453 | 454 | // for n := 0; n < b.N; n++ { 455 | // for i := 0; i < addEntSize; i++ { 456 | // bun.Clear() 457 | 458 | // myBundle2.Unbundle(bun, 459 | // position{1, 2, 3}, 460 | // velocity{1, 2, 3}, 461 | // acceleration{1, 2, 3}, 462 | // radius{1}, 463 | // ) 464 | 465 | // id := world.NewId() 466 | // bun.Write(world, id) 467 | // } 468 | // } 469 | // } 470 | 471 | // func BenchmarkAllocateBundle4Direct(b *testing.B) { 472 | // world := NewWorld() 473 | 474 | // var myBundle2 = NewBundle4[position, velocity, acceleration, radius]() 475 | 476 | // b.ResetTimer() 477 | 478 | // for n := 0; n < b.N; n++ { 479 | // for i := 0; i < addEntSize; i++ { 480 | // id := world.NewId() 481 | // myBundle2.Write(world, id, 482 | // position{1, 2, 3}, 483 | // velocity{1, 2, 3}, 484 | // acceleration{1, 2, 3}, 485 | // radius{1}, 486 | // ) 487 | // } 488 | // } 489 | // } 490 | 491 | func BenchmarkAllocateNonBundle4Direct(b *testing.B) { 492 | world := NewWorld() 493 | 494 | b.ResetTimer() 495 | 496 | comps := []Component{ 497 | position{1, 2, 3}, 498 | velocity{1, 2, 3}, 499 | acceleration{1, 2, 3}, 500 | radius{1}, 501 | } 502 | 503 | for n := 0; n < b.N; n++ { 504 | for i := 0; i < addEntSize; i++ { 505 | id := world.NewId() 506 | world.Write(id, comps...) 507 | } 508 | } 509 | } 510 | 511 | // func BenchmarkAllocateCommands(b *testing.B) { 512 | // world := NewWorld() 513 | // cmd := NewCommandQueue(world) 514 | 515 | // b.ResetTimer() 516 | 517 | // for n := 0; n < b.N; n++ { 518 | // for i := 0; i < addEntSize; i++ { 519 | // cmd.Spawn(myBundle{ 520 | // pos: position{1, 2, 3}, 521 | // vel: velocity{1, 2, 3}, 522 | // acc: acceleration{1, 2, 3}, 523 | // rad: radius{1}, 524 | // }) 525 | // } 526 | // cmd.Execute() 527 | // } 528 | // } 529 | 530 | // func BenchmarkAllocateCommands2(b *testing.B) { 531 | // world := NewWorld() 532 | // cmd := NewCommandQueue(world) 533 | 534 | // b.ResetTimer() 535 | 536 | // for n := 0; n < b.N; n++ { 537 | // for i := 0; i < addEntSize; i++ { 538 | // CmdSpawn(cmd, myBundle{ 539 | // pos: position{1, 2, 3}, 540 | // vel: velocity{1, 2, 3}, 541 | // acc: acceleration{1, 2, 3}, 542 | // rad: radius{1}, 543 | // }) 544 | // } 545 | // cmd.Execute() 546 | // } 547 | // } 548 | 549 | func BenchmarkAllocateCommands3(b *testing.B) { 550 | world := NewWorld() 551 | cmd := NewCommandQueue(world) 552 | 553 | b.ResetTimer() 554 | 555 | for n := 0; n < b.N; n++ { 556 | for i := 0; i < addEntSize; i++ { 557 | cmd.SpawnEmpty().Insert( 558 | myBundle{ 559 | pos: position{1, 2, 3}, 560 | vel: velocity{1, 2, 3}, 561 | acc: acceleration{1, 2, 3}, 562 | rad: radius{1}, 563 | }) 564 | } 565 | // myBundle{ 566 | // pos: position{1, 2, 3}, 567 | // vel: velocity{1, 2, 3}, 568 | // acc: acceleration{1, 2, 3}, 569 | // rad: radius{1}, 570 | // }.Unbundle(entCmd.cmd.bundler) 571 | // } 572 | cmd.Execute() 573 | } 574 | } 575 | 576 | // func BenchmarkAllocateCommands4(b *testing.B) { 577 | // world := NewWorld() 578 | // cmd := NewCommandQueue() 579 | 580 | // b.ResetTimer() 581 | 582 | // for n := 0; n < b.N; n++ { 583 | // for i := 0; i < addEntSize; i++ { 584 | // cmd.SpawnEmpty(). 585 | // Insert(positionId.With(position{1, 2, 3})). 586 | // Insert(velocityId.With(velocity{1, 2, 3})). 587 | // Insert(accelerationId.With(acceleration{1, 2, 3})). 588 | // Insert(radiusId.With(radius{1})) 589 | // } 590 | // cmd.Execute(world) 591 | // } 592 | // } 593 | 594 | // func BenchmarkAllocateCommands5(b *testing.B) { 595 | // world := NewWorld() 596 | // cmd := NewCommandQueue(world) 597 | 598 | // b.ResetTimer() 599 | 600 | // for n := 0; n < b.N; n++ { 601 | // for i := 0; i < addEntSize; i++ { 602 | // cmd.SpawnEmpty().Add( 603 | // myBundle{ 604 | // pos: position{1, 2, 3}, 605 | // vel: velocity{1, 2, 3}, 606 | // acc: acceleration{1, 2, 3}, 607 | // rad: radius{1}, 608 | // }.BundleSeq()) 609 | // } 610 | // cmd.Execute() 611 | // } 612 | // } 613 | 614 | func BenchmarkAllocateCommands6(b *testing.B) { 615 | world := NewWorld() 616 | cmd := NewCommandQueue(world) 617 | 618 | b.ResetTimer() 619 | 620 | for n := 0; n < b.N; n++ { 621 | for i := 0; i < addEntSize; i++ { 622 | cmd.SpawnEmpty(). 623 | Insert(position{1, 2, 3}). 624 | Insert(velocity{1, 2, 3}). 625 | Insert(acceleration{1, 2, 3}). 626 | Insert(radius{1}) 627 | } 628 | cmd.Execute() 629 | } 630 | } 631 | -------------------------------------------------------------------------------- /bundle.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type Bundler struct { 4 | archMask archetypeMask // The current archetypeMask 5 | // TODO: Instead of set, you could just use the arch mask 6 | Set [maxComponentId]bool // The list of components that are being bundled 7 | Components [maxComponentId]Component // Component storage array for everything we've bundled 8 | maxComponentIdAdded CompId 9 | } 10 | 11 | func (b *Bundler) Clear() { 12 | b.archMask = blankArchMask 13 | b.Set = [maxComponentId]bool{} 14 | b.maxComponentIdAdded = 0 15 | // b.Components // Note: No need to clear because we only use set values 16 | } 17 | 18 | // func (bun *Bundler) Add(comp Component) { 19 | // compId := comp.id() 20 | // bun.archMask.addComponent(compId) 21 | // bun.Set[compId] = true 22 | // if bun.Components[compId] == nil { 23 | // bun.Components[compId] = comp.Clone() // Create an internal copy 24 | // } else { 25 | // comp.SetOther(bun.Components[compId]) 26 | // } 27 | 28 | // bun.maxComponentIdAdded = max(bun.maxComponentIdAdded, compId) 29 | // } 30 | 31 | func (bun *Bundler) Has(comp Component) bool { 32 | return bun.Set[comp.CompId()] 33 | } 34 | 35 | func readBundle[T Component](bun *Bundler) (T, bool) { 36 | var comp T 37 | compId := comp.CompId() 38 | 39 | if !bun.Set[compId] { 40 | return comp, false // Was not set 41 | } 42 | return bun.Components[compId].(*box[T]).val, true 43 | } 44 | 45 | // func (bun *Bundler) Read(comp Component) (Component, bool) { 46 | // compId := comp.CompId() 47 | // if !bun.Set[compId] { 48 | // return comp, false // Was not set 49 | // } 50 | // return bun.Components[compId], true 51 | // } 52 | 53 | func (bun *Bundler) Remove(compId CompId) { 54 | bun.archMask.removeComponent(compId) 55 | bun.Set[compId] = false 56 | } 57 | 58 | // func WriteComponent[T any](bun *Bundler, comp T) { 59 | // compId := nameTyped(comp) 60 | // bun.archMask.addComponent(compId) 61 | // bun.Set[compId] = true 62 | // if bun.Components[compId] == nil { 63 | // bun.Components[compId] = C(comp) 64 | // } else { 65 | // bun.Components[compId].Set(comp) 66 | // } 67 | 68 | // bun.maxComponentIdAdded = max(bun.maxComponentIdAdded, compId) 69 | // } 70 | 71 | func (b *Bundler) Write(world *World, id Id) { 72 | if b.archMask == blankArchMask { 73 | return // If the bundler is empty, just dont do anything 74 | } 75 | world.writeBundler(id, b) 76 | } 77 | -------------------------------------------------------------------------------- /cod_encode.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "github.com/unitoftime/cod/backend" 5 | ) 6 | 7 | func (t Id) CodEquals(tt Id) bool { 8 | return t == tt 9 | } 10 | 11 | func (t Id) EncodeCod(bs []byte) []byte { 12 | 13 | { 14 | value0 := uint32(t) 15 | 16 | bs = backend.WriteVarUint32(bs, value0) 17 | 18 | } 19 | return bs 20 | } 21 | 22 | func (t *Id) DecodeCod(bs []byte) (int, error) { 23 | var err error 24 | var n int 25 | var nOff int 26 | 27 | { 28 | var value0 uint32 29 | 30 | value0, nOff, err = backend.ReadVarUint32(bs[n:]) 31 | if err != nil { 32 | return 0, err 33 | } 34 | n += nOff 35 | 36 | *t = Id(value0) 37 | } 38 | 39 | return n, err 40 | } 41 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type onInsert interface { 4 | Component 5 | OnInsert(ent EntityCommand) 6 | } 7 | 8 | // type singleCmd interface { 9 | // apply(*World) 10 | // } 11 | 12 | // type spawnCmd struct { 13 | // bundler *Bundler 14 | // } 15 | // func (c spawnCmd) apply(world *World) { 16 | // id := world.NewId() 17 | // c.bundler.Write(world, id) 18 | // } 19 | 20 | type CmdType uint8 21 | 22 | const ( 23 | CmdTypeNone CmdType = iota 24 | CmdTypeSpawn 25 | CmdTypeWrite 26 | CmdTypeTrigger 27 | CmdTypeDelete 28 | // CmdTypeCustom 29 | ) 30 | 31 | type singleCmd struct { 32 | Type CmdType 33 | id Id 34 | bundler *Bundler 35 | world *World 36 | event Event 37 | } 38 | 39 | func (c *singleCmd) apply(world *World) { 40 | switch c.Type { 41 | case CmdTypeNone: 42 | // Do nothing, Command was probably cancelled 43 | case CmdTypeSpawn: 44 | if world.cmd.preWrite != nil { 45 | world.cmd.preWrite(EntityCommand{c}) 46 | } 47 | c.bundler.Write(world, c.id) // TODO: This could probably use a Spawn function which would be faster 48 | // if world.cmd.postWrite != nil { 49 | // world.cmd.postWrite(c.id) 50 | // } 51 | case CmdTypeWrite: 52 | if world.cmd.preWrite != nil { 53 | world.cmd.preWrite(EntityCommand{c}) 54 | } 55 | c.bundler.Write(world, c.id) 56 | // if world.cmd.postWrite != nil { 57 | // world.cmd.postWrite(c.id) 58 | // } 59 | case CmdTypeTrigger: 60 | world.Trigger(c.event, c.id) 61 | case CmdTypeDelete: 62 | if world.cmd.preDelete != nil { 63 | world.cmd.preDelete(c.id) 64 | } 65 | Delete(world, c.id) 66 | } 67 | } 68 | 69 | type EntityCommand struct { 70 | cmd *singleCmd 71 | } 72 | 73 | // func (e EntityCommand) Printout() { 74 | // fmt.Println("---") 75 | // for i := range e.cmd.bundler.Components { 76 | // if e.cmd.bundler.Set[i] { 77 | // fmt.Printf("+%v\n", e.cmd.bundler.Components[i]) 78 | // } 79 | // } 80 | // // fmt.Printf("+%v\n", e.cmd.bundler) 81 | // } 82 | 83 | func (e EntityCommand) Cancel() { 84 | e.cmd.Type = CmdTypeNone 85 | e.cmd.id = InvalidEntity 86 | } 87 | 88 | // Removes the supplied component type from this entity command. 89 | // TODO: Should this also remove it from the world? if it exists there? 90 | func (e EntityCommand) Remove(comp Component) { 91 | compId := comp.CompId() 92 | e.cmd.bundler.Remove(compId) 93 | } 94 | 95 | func (e EntityCommand) Empty() bool { 96 | return (e == EntityCommand{}) 97 | } 98 | 99 | func (e EntityCommand) Insert(bun Writer) EntityCommand { 100 | inserter, ok := bun.(onInsert) 101 | alreadyHas := false 102 | if ok { 103 | alreadyHas = e.cmd.bundler.Has(inserter) 104 | } 105 | 106 | unbundle(bun, e.cmd.bundler) 107 | 108 | // Only run OnInsert, if the writer supports it and we havent already inserted it to the bundler 109 | if ok && !alreadyHas { 110 | inserter.OnInsert(e) 111 | } 112 | 113 | return e 114 | } 115 | 116 | // Inserts the component if it is missing 117 | func (e EntityCommand) InsertIfMissing(bun Component) EntityCommand { 118 | if e.cmd.bundler.Has(bun) { 119 | return e 120 | } 121 | 122 | if e.cmd.world.hasCompId(e.Id(), bun.CompId()) { 123 | return e 124 | } 125 | 126 | unbundle(bun, e.cmd.bundler) 127 | return e 128 | } 129 | 130 | func (e EntityCommand) Id() Id { 131 | return e.cmd.id 132 | } 133 | 134 | // func (e EntityCommand) Remove(bun Bundle) EntityCommand { 135 | // bun.Unbundle(e.cmd.bundler) 136 | // return e 137 | // } 138 | 139 | // func (e EntityCommand) Add(seq iter.Seq[Component]) EntityCommand { 140 | // for c := range seq { 141 | // e.cmd.bundler.Add(c) 142 | // } 143 | // return e 144 | // } 145 | func ReadComp[T Component](e EntityCommand) (T, bool) { 146 | var t T 147 | // comp, ok := e.cmd.bundler.Read(t) 148 | // if ok { 149 | // box := comp.(*box[T]) 150 | // return box.val, true 151 | // } 152 | 153 | comp, ok := readBundle[T](e.cmd.bundler) 154 | if ok { 155 | return comp, true 156 | } 157 | return t, false 158 | } 159 | 160 | type CommandQueue struct { 161 | world *World 162 | preWrite func(EntityCommand) 163 | preDelete func(Id) 164 | commands []singleCmd 165 | 166 | currentBundlerIndex int 167 | bundlers []*Bundler 168 | } 169 | 170 | func NewCommandQueue(world *World) *CommandQueue { 171 | return &CommandQueue{ 172 | world: world, 173 | } 174 | } 175 | func (c *CommandQueue) Initialize(world *World) any { 176 | return NewCommandQueue(world) 177 | } 178 | 179 | func (c *CommandQueue) NextBundler() *Bundler { 180 | if c.currentBundlerIndex >= len(c.bundlers) { 181 | bundler := &Bundler{} 182 | c.bundlers = append(c.bundlers, bundler) 183 | c.currentBundlerIndex = len(c.bundlers) 184 | return bundler 185 | } else { 186 | bundler := c.bundlers[c.currentBundlerIndex] 187 | bundler.Clear() 188 | c.currentBundlerIndex++ 189 | return bundler 190 | } 191 | } 192 | 193 | func unbundle(bundle Writer, bundler *Bundler) { 194 | wd := W{bundler: bundler} 195 | bundle.CompWrite(wd) 196 | } 197 | 198 | func remove(bundle Writer, bundler *Bundler) { 199 | wd := W{bundler: bundler} 200 | bundle.CompWrite(wd) 201 | } 202 | 203 | // func CmdSpawn[T Writer](c *CommandQueue, ub T) { 204 | // bundler := c.NextBundler() 205 | // unbundle(ub, bundler) 206 | // // ub.Unbundle(bundler) 207 | // c.commands = append(c.commands, singleCmd{ 208 | // Type: CmdTypeSpawn, 209 | // id: c.world.NewId(), 210 | // bundler: bundler, 211 | // }) 212 | // } 213 | 214 | // func (c *CommandQueue) Spawn(bun Writer) { 215 | // entCmd := c.SpawnEmpty() 216 | // entCmd.Insert(bun) 217 | // } 218 | 219 | func (c *CommandQueue) SpawnEmpty() EntityCommand { 220 | bundler := c.NextBundler() 221 | 222 | c.commands = append(c.commands, singleCmd{ 223 | Type: CmdTypeSpawn, 224 | id: c.world.NewId(), 225 | bundler: bundler, 226 | world: c.world, 227 | }) 228 | return EntityCommand{ 229 | cmd: &(c.commands[len(c.commands)-1]), 230 | } 231 | } 232 | 233 | // // Pushes a command to delete the entity 234 | // func (c *CommandQueue) Delete(id Id) { 235 | // c.commands = append(c.commands, singleCmd{ 236 | // Type: CmdTypeDelete, 237 | // id: id, 238 | // }) 239 | // } 240 | 241 | func (c *CommandQueue) Write(id Id) EntityCommand { 242 | bundler := c.NextBundler() 243 | 244 | c.commands = append(c.commands, singleCmd{ 245 | Type: CmdTypeWrite, 246 | id: id, 247 | bundler: bundler, 248 | world: c.world, 249 | }) 250 | return EntityCommand{ 251 | cmd: &(c.commands[len(c.commands)-1]), 252 | } 253 | } 254 | 255 | func (c *CommandQueue) Trigger(event Event, ids ...Id) { 256 | // Special Case: no ids provided, so just trigger a single, unrelated 257 | if len(ids) == 0 { 258 | c.commands = append(c.commands, singleCmd{ 259 | Type: CmdTypeTrigger, 260 | id: InvalidEntity, 261 | event: event, 262 | world: c.world, 263 | }) 264 | 265 | return 266 | } 267 | 268 | for _, id := range ids { 269 | c.commands = append(c.commands, singleCmd{ 270 | Type: CmdTypeTrigger, 271 | id: id, 272 | event: event, 273 | world: c.world, 274 | }) 275 | } 276 | } 277 | 278 | // Adds a prewrite function to be executed before every write or spawn command is executed 279 | // Useful for ensuring entities are fully formed before pushing them into the ECS 280 | func (c *CommandQueue) SetPrewrite(lambda func(EntityCommand)) { 281 | c.preWrite = lambda 282 | } 283 | 284 | // // Adds a predelite function to be executed before every delete command is executed 285 | // // Useful for ensuring any external datastructures get cleaned up when an entity is deleted 286 | // func (c *CommandQueue) SetPredelete(lambda func(Id)) { 287 | // c.preDelete = lambda 288 | // } 289 | 290 | func (c *CommandQueue) Execute() { 291 | // Perform all commands 292 | // Note: We must check length every time in case calling one command adds more commands 293 | for i := 0; i < len(c.commands); i++ { 294 | c.commands[i].apply(c.world) 295 | } 296 | 297 | // Cleanup Queue 298 | c.commands = c.commands[:0] 299 | c.currentBundlerIndex = 0 300 | } 301 | 302 | // TODO: Maybe? 303 | // func (c *CommandQueue) ExecutePostWrite(postWrite func (ecs.Id)) { 304 | // // Perform all commands 305 | // for i := range c.commands { 306 | // c.commands[i].apply(c.world) 307 | // } 308 | // for i := range c.commands { 309 | // if c.commands[i].id == InvalidEntity { 310 | // continue 311 | // } 312 | // postWrite(c.commands[i].id) 313 | // } 314 | 315 | // // Cleanup Queue 316 | // c.commands = c.commands[:0] 317 | // c.currentBundlerIndex = 0 318 | // } 319 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestCommandSingleRewrite(t *testing.T) { 9 | world := NewWorld() 10 | cmd := NewCommandQueue(world) 11 | 12 | a := world.NewId() 13 | b := world.NewId() 14 | c := world.NewId() 15 | d := world.NewId() 16 | Write(world, a, C(position{}), C(velocity{})) 17 | Write(world, b, C(position{}), C(velocity{})) 18 | Write(world, c, C(position{}), C(velocity{})) 19 | Write(world, d, C(position{}), C(velocity{})) 20 | DeleteComponent(world, a, C(velocity{})) 21 | DeleteComponent(world, b, C(velocity{})) 22 | DeleteComponent(world, c, C(velocity{})) 23 | DeleteComponent(world, d, C(velocity{})) 24 | 25 | queryPos := Query1[position](world) 26 | queryVel := Query1[velocity](world) 27 | 28 | queryPos.MapId(func(id Id, pos *position) { 29 | cmd.Write(id).Insert(C(velocity{})) 30 | }) 31 | check(t, *queryPos.Read(a) == position{}) 32 | check(t, *queryPos.Read(b) == position{}) 33 | check(t, *queryPos.Read(c) == position{}) 34 | check(t, *queryPos.Read(d) == position{}) 35 | 36 | check(t, queryVel.Read(a) == nil) 37 | check(t, queryVel.Read(b) == nil) 38 | check(t, queryVel.Read(c) == nil) 39 | check(t, queryVel.Read(d) == nil) 40 | 41 | cmd.Execute() 42 | 43 | check(t, *queryPos.Read(a) == position{}) 44 | check(t, *queryPos.Read(b) == position{}) 45 | check(t, *queryPos.Read(c) == position{}) 46 | check(t, *queryPos.Read(d) == position{}) 47 | 48 | check(t, *queryVel.Read(a) == velocity{}) 49 | check(t, *queryVel.Read(b) == velocity{}) 50 | check(t, *queryVel.Read(c) == velocity{}) 51 | check(t, *queryVel.Read(d) == velocity{}) 52 | } 53 | 54 | func TestCommandWrites(t *testing.T) { 55 | world := NewWorld() 56 | commands := NewCommandQueue(world) 57 | 58 | type data struct { 59 | id Id 60 | pos position 61 | vel velocity 62 | acc acceleration 63 | rad radius 64 | } 65 | 66 | expected := make([]data, 1000) 67 | for i := range expected { 68 | ent := commands.SpawnEmpty() 69 | 70 | expected[i] = data{ 71 | id: ent.Id(), 72 | pos: position{1, rand.Float64() * 100, rand.Float64() * 100}, 73 | vel: velocity{2, rand.Float64() * 100, rand.Float64() * 100}, 74 | acc: acceleration{3, rand.Float64() * 100, rand.Float64() * 100}, 75 | rad: radius{rand.Float64() * 100}, 76 | } 77 | ent. 78 | Insert(expected[i].pos). 79 | Insert(expected[i].vel). 80 | Insert(expected[i].acc). 81 | Insert(expected[i].rad) 82 | } 83 | 84 | commands.Execute() 85 | 86 | for i := range expected { 87 | id := expected[i].id 88 | 89 | pos, ok := Read[position](world, id) 90 | check(t, ok) 91 | compare(t, pos, expected[i].pos) 92 | 93 | vel, ok := Read[velocity](world, id) 94 | check(t, ok) 95 | compare(t, vel, expected[i].vel) 96 | 97 | acc, ok := Read[acceleration](world, id) 98 | check(t, ok) 99 | compare(t, acc, expected[i].acc) 100 | 101 | rad, ok := Read[radius](world, id) 102 | check(t, ok) 103 | compare(t, rad, expected[i].rad) 104 | } 105 | } 106 | 107 | func TestWorldReadWriteNew(t *testing.T) { 108 | world := NewWorld() 109 | id := world.NewId() 110 | 111 | // Write position 112 | pos := position{1, 1, 1} 113 | Write(world, id, pos) 114 | 115 | // Check position and velocity 116 | posOut, ok := Read[position](world, id) 117 | check(t, ok) 118 | compare(t, posOut, pos) 119 | velOut, ok := Read[velocity](world, id) 120 | check(t, !ok) // We expect this to be false 121 | compare(t, velOut, velocity{0, 0, 0}) 122 | 123 | // Write velocity 124 | vel := velocity{2, 2, 2} 125 | Write(world, id, vel) 126 | 127 | // Check position and velocity 128 | posOut, ok = Read[position](world, id) 129 | check(t, ok) 130 | compare(t, posOut, pos) 131 | velOut, ok = Read[velocity](world, id) 132 | check(t, ok) 133 | compare(t, velOut, vel) 134 | 135 | compare(t, world.engine.count(position{}), 1) 136 | compare(t, world.engine.count(position{}, velocity{}), 1) 137 | compare(t, world.engine.count(position{}, velocity{}), 1) 138 | compare(t, world.engine.count(acceleration{}), 0) 139 | 140 | // count := 0 141 | // Map2(world, func(id Id, p *position, v *velocity) { 142 | // count++ 143 | // }) 144 | // compare(t, count, 1) 145 | 146 | // count = 0 147 | // view := ViewAll2[position, velocity](world) 148 | // for { 149 | // _, _, _, ok := view.Iter() 150 | // if !ok { break } 151 | // count++ 152 | // } 153 | // compare(t, count, 1) 154 | } 155 | 156 | func TestWorldReadMultiWriteNew(t *testing.T) { 157 | world := NewWorld() 158 | id := world.NewId() 159 | 160 | pos := position{1, 1, 1} 161 | vel := velocity{2, 2, 2} 162 | Write(world, id, pos, vel) 163 | 164 | // Check position and velocity 165 | posOut, ok := Read[position](world, id) 166 | check(t, ok) 167 | compare(t, posOut, pos) 168 | velOut, ok := Read[velocity](world, id) 169 | check(t, ok) 170 | compare(t, velOut, vel) 171 | 172 | // Write accel and size 173 | accel := acceleration{3, 3, 3} 174 | rad := radius{4} 175 | Write(world, id, accel, rad) 176 | 177 | // Check all 178 | posOut, ok = Read[position](world, id) 179 | check(t, ok) 180 | compare(t, posOut, pos) 181 | velOut, ok = Read[velocity](world, id) 182 | check(t, ok) 183 | compare(t, velOut, vel) 184 | accelOut, ok := Read[acceleration](world, id) 185 | check(t, ok) 186 | compare(t, accelOut, accel) 187 | radOut, ok := Read[radius](world, id) 188 | check(t, ok) 189 | compare(t, radOut, rad) 190 | } 191 | 192 | func TestCommandReadWrite(t *testing.T) { 193 | world := NewWorld() 194 | cmd := GetInjectable[*CommandQueue](world) 195 | 196 | { 197 | e := cmd.SpawnEmpty(). 198 | Insert(position{1, 2, 3}) 199 | 200 | p, ok := ReadComp[position](e) 201 | check(t, ok) 202 | compare(t, p, position{1, 2, 3}) 203 | 204 | v, ok := ReadComp[velocity](e) 205 | check(t, !ok) 206 | compare(t, v, velocity{}) 207 | 208 | e.Insert(velocity{4, 5, 6}) 209 | 210 | v, ok = ReadComp[velocity](e) 211 | check(t, ok) 212 | compare(t, v, velocity{4, 5, 6}) 213 | } 214 | 215 | cmd.Execute() 216 | 217 | { 218 | e := cmd.SpawnEmpty(). 219 | Insert(position{1, 2, 3}) 220 | 221 | p, ok := ReadComp[position](e) 222 | check(t, ok) 223 | compare(t, p, position{1, 2, 3}) 224 | 225 | v, ok := ReadComp[velocity](e) 226 | check(t, !ok) 227 | compare(t, v, velocity{}) 228 | 229 | e.Insert(velocity{4, 5, 6}) 230 | 231 | v, ok = ReadComp[velocity](e) 232 | check(t, ok) 233 | compare(t, v, velocity{4, 5, 6}) 234 | } 235 | 236 | cmd.Execute() 237 | } 238 | 239 | func TestCommandCancel(t *testing.T) { 240 | world := NewWorld() 241 | cmd := GetInjectable[*CommandQueue](world) 242 | 243 | e := cmd.SpawnEmpty(). 244 | Insert(position{1, 2, 3}) 245 | 246 | e.Cancel() 247 | 248 | cmd.Execute() 249 | 250 | p, ok := Read[position](world, e.Id()) 251 | check(t, !ok) 252 | compare(t, p, position{}) 253 | } 254 | 255 | type testEvent struct { 256 | val int 257 | } 258 | 259 | var _testEventId = NewEvent[testEvent]() 260 | 261 | func (p testEvent) EventId() EventId { 262 | return _testEventId 263 | } 264 | 265 | func TestCommandTrigger(t *testing.T) { 266 | world := NewWorld() 267 | cmd := GetInjectable[*CommandQueue](world) 268 | 269 | runCount := 0 270 | world.AddObserver( 271 | NewHandler(func(trigger Trigger[testEvent]) { 272 | compare(t, trigger.Data.val, 55) 273 | 274 | if runCount == 0 { 275 | compare(t, trigger.Id, 33) 276 | } else if runCount == 1 { 277 | compare(t, trigger.Id, 44) 278 | } else { 279 | compare(t, trigger.Id, InvalidEntity) 280 | } 281 | 282 | runCount++ 283 | })) 284 | 285 | cmd.Trigger(testEvent{55}, 33, 44) // Emit for id 33 then 44 286 | cmd.Execute() 287 | 288 | compare(t, runCount, 2) // Should run twice (33, 44) 289 | 290 | cmd.Trigger(testEvent{55}) // Emit for no specific entity 291 | cmd.Execute() 292 | 293 | compare(t, runCount, 3) // Should run once (nonspecific) 294 | } 295 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type CompId uint16 4 | 5 | func NewComp[T any]() comp[T] { 6 | var t T 7 | return Comp(t) 8 | } 9 | 10 | func Comp[T any](t T) comp[T] { 11 | compId := nameTyped[T](t) 12 | return comp[T]{ 13 | compId, 14 | } 15 | } 16 | 17 | type comp[T any] struct { 18 | compId CompId 19 | } 20 | 21 | func (c comp[T]) CompId() CompId { 22 | return c.compId 23 | } 24 | func (c comp[T]) newBox(val T) box[T] { 25 | return box[T]{ 26 | val: val, 27 | comp: c, 28 | } 29 | } 30 | 31 | type W struct { 32 | engine *archEngine 33 | archId archetypeId 34 | index int 35 | bundler *Bundler 36 | } 37 | 38 | type Writer interface { 39 | CompWrite(W) 40 | } 41 | 42 | type Component interface { 43 | Writer 44 | CompId() CompId 45 | } 46 | 47 | // This type is used to box a component with all of its type info so that it implements the component interface. I would like to get rid of this and simplify the APIs 48 | type box[T any] struct { 49 | val T 50 | comp[T] 51 | } 52 | 53 | // Creates the boxed component type 54 | func C[T any](val T) box[T] { 55 | comp := Comp(val) 56 | return comp.newBox(val) 57 | } 58 | 59 | func (c box[T]) CompWrite(wd W) { 60 | c.WriteVal(wd, c.val) 61 | } 62 | 63 | func (c box[T]) OnInsert(ent EntityCommand) { 64 | inserter, ok := any(c.val).(onInsert) 65 | if ok { 66 | inserter.OnInsert(ent) 67 | } 68 | } 69 | 70 | // func (c Box[T]) getPtr(engine *archEngine, archId archetypeId, index int) *T { 71 | // store := getStorageByCompId[T](engine, c.Id()) 72 | // slice := store.slice[archId] 73 | // return &slice.comp[index] 74 | // } 75 | 76 | // func (c box[T]) With(val T) box[T] { 77 | // c.val = val 78 | // return c 79 | // } 80 | 81 | // func (c box[T]) Unbundle(bun *Bundler) { 82 | // c.UnbundleVal(bun, c.comp) 83 | // } 84 | 85 | // func (c Box[T]) Unbundle(bun *Bundler) { 86 | // compId := c.compId 87 | // val := c.Comp 88 | // bun.archMask.addComponent(compId) 89 | // bun.Set[compId] = true 90 | // if bun.Components[compId] == nil { 91 | // // Note: We need a pointer so that we dont do an allocation every time we set it 92 | // c2 := c // Note: make a copy, so the bundle doesn't contain a pointer to the original 93 | // bun.Components[compId] = &c2 94 | // } else { 95 | // rwComp := bun.Components[compId].(*Box[T]) 96 | // rwComp.Comp = val 97 | // } 98 | 99 | // bun.maxComponentIdAdded = max(bun.maxComponentIdAdded, compId) 100 | // } 101 | 102 | func (c comp[T]) WriteVal(cw W, val T) { 103 | if cw.bundler != nil { 104 | c.UnbundleVal(cw.bundler, val) 105 | } else { 106 | store := getStorageByCompId[T](cw.engine, c.CompId()) 107 | writeArch(cw.engine, cw.archId, cw.index, store, val) 108 | } 109 | } 110 | 111 | // func (c Box[T]) writeVal(engine *archEngine, archId archetypeId, index int, val T) { 112 | // store := getStorageByCompId[T](engine, c.id()) 113 | // writeArch[T](engine, archId, index, store, val) 114 | // } 115 | 116 | func (c comp[T]) UnbundleVal(bun *Bundler, val T) { 117 | compId := c.compId 118 | bun.archMask.addComponent(compId) 119 | bun.Set[compId] = true 120 | if bun.Components[compId] == nil { 121 | // Note: We need a pointer so that we dont do an allocation every time we set it 122 | box := c.newBox(val) 123 | bun.Components[compId] = &box 124 | } else { 125 | rwComp := bun.Components[compId].(*box[T]) 126 | rwComp.val = val 127 | } 128 | 129 | bun.maxComponentIdAdded = max(bun.maxComponentIdAdded, compId) 130 | } 131 | -------------------------------------------------------------------------------- /dcr.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "fmt" 4 | 5 | // TODO: You should move to this (ie archetype graph (or bitmask?). maintain the current archetype node, then traverse to nodes (and add new ones) based on which components are added): https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9 6 | // Dynamic component Registry 7 | type componentRegistry struct { 8 | archSet [][]archetypeId // Contains the set of archetypeIds that have this component 9 | archMask map[archetypeMask]archetypeId // Contains a mapping of archetype bitmasks to archetypeIds 10 | 11 | revArchMask []archetypeMask // Contains the reverse mapping of archetypeIds to archetype masks. Indexed by archetypeId 12 | } 13 | 14 | func newComponentRegistry() *componentRegistry { 15 | r := &componentRegistry{ 16 | archSet: make([][]archetypeId, maxComponentId+1), // TODO: hardcoded to max component 17 | archMask: make(map[archetypeMask]archetypeId), 18 | revArchMask: make([]archetypeMask, 0), 19 | } 20 | return r 21 | } 22 | 23 | func (r *componentRegistry) getArchetypeId(engine *archEngine, mask archetypeMask) archetypeId { 24 | archId, ok := r.archMask[mask] 25 | if !ok { 26 | componentIds := mask.getComponentList() 27 | archId = engine.newArchetypeId(mask, componentIds) 28 | r.archMask[mask] = archId 29 | 30 | if int(archId) != len(r.revArchMask) { 31 | panic(fmt.Sprintf("ecs: archId must increment. Expected: %d, Got: %d", len(r.revArchMask), archId)) 32 | } 33 | r.revArchMask = append(r.revArchMask, mask) 34 | 35 | // Add this archetypeId to every component's archList 36 | for _, compId := range componentIds { 37 | r.archSet[compId] = append(r.archSet[compId], archId) 38 | } 39 | } 40 | return archId 41 | } 42 | 43 | // This is mostly for the without filter 44 | func (r *componentRegistry) archIdOverlapsMask(archId archetypeId, compArchMask archetypeMask) bool { 45 | archMaskToCheck := r.revArchMask[archId] 46 | 47 | resultArchMask := archMaskToCheck.bitwiseAnd(compArchMask) 48 | if resultArchMask != blankArchMask { 49 | // If the resulting arch mask is nonzero, it means that both the component mask and the base mask had the same bit set, which means the arch had one of the components 50 | return true 51 | } 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /deprecated.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "fmt" 4 | 5 | // Reads a specific component of the entity specified at id. 6 | // Returns true if the entity was found and had that component, else returns false. 7 | // Deprecated: This API is tentative, I'm trying to improve the QueryN construct so that it can capture this usecase. 8 | func Read[T any](world *World, id Id) (T, bool) { 9 | var ret T 10 | loc, ok := world.arch.Get(id) 11 | if !ok { 12 | return ret, false 13 | } 14 | 15 | return readArch[T](world.engine, loc, id) 16 | } 17 | 18 | // Reads a pointer to the component of the entity at the specified id. 19 | // Returns true if the entity was found and had that component, else returns false. 20 | // This pointer is short lived and can become invalid if any other entity changes in the world 21 | // Deprecated: This API is tentative, I'm trying to improve the QueryN construct so that it can capture this usecase. 22 | func ReadPtr[T any](world *World, id Id) *T { 23 | loc, ok := world.arch.Get(id) 24 | if !ok { 25 | return nil 26 | } 27 | 28 | return readPtrArch[T](world.engine, loc, id) 29 | } 30 | 31 | func (e *archEngine) ReadEntity(loc entLoc, id Id) *Entity { 32 | lookup := e.lookup[loc.archId] 33 | if lookup == nil { 34 | panic("Archetype doesn't have lookup list") 35 | } 36 | index := int(loc.index) 37 | 38 | ent := NewEntity() 39 | for n := range e.compStorage { 40 | if e.compStorage[n] != nil { 41 | e.compStorage[n].ReadToEntity(ent, loc.archId, index) 42 | } 43 | } 44 | return ent 45 | } 46 | 47 | func (e *archEngine) ReadRawEntity(loc entLoc, id Id) *RawEntity { 48 | lookup := e.lookup[loc.archId] 49 | if lookup == nil { 50 | panic("Archetype doesn't have lookup list") 51 | } 52 | 53 | ent := NewRawEntity() 54 | for n := range e.compStorage { 55 | if e.compStorage[n] != nil { 56 | e.compStorage[n].ReadToRawEntity(ent, loc.archId, int(loc.index)) 57 | } 58 | } 59 | return ent 60 | } 61 | 62 | func readArch[T any](e *archEngine, loc entLoc, id Id) (T, bool) { 63 | var ret T 64 | lookup := e.lookup[loc.archId] 65 | if lookup == nil { 66 | return ret, false // TODO: when could this possibly happen? 67 | } 68 | 69 | // Get the dynamic componentSliceStorage 70 | n := name(ret) 71 | ss := e.compStorage[n] 72 | if ss == nil { 73 | return ret, false 74 | } 75 | 76 | storage, ok := ss.(*componentStorage[T]) 77 | if !ok { 78 | panic(fmt.Sprintf("Wrong componentSliceStorage[T] type: %d != %d", name(ss), name(ret))) 79 | } 80 | 81 | // Get the underlying Archetype's componentSlice 82 | cSlice, ok := storage.slice.Get(loc.archId) 83 | if !ok { 84 | return ret, false 85 | } 86 | 87 | return cSlice.comp[loc.index], true 88 | } 89 | 90 | func readPtrArch[T any](e *archEngine, loc entLoc, id Id) *T { 91 | var ret T 92 | lookup := e.lookup[loc.archId] 93 | if lookup == nil { 94 | return nil 95 | } 96 | 97 | // Get the dynamic componentSliceStorage 98 | n := name(ret) 99 | ss := e.compStorage[n] 100 | if ss == nil { 101 | return nil 102 | } 103 | 104 | storage, ok := ss.(*componentStorage[T]) 105 | if !ok { 106 | panic(fmt.Sprintf("Wrong componentSliceStorage[T] type: %d != %d", name(ss), name(ret))) 107 | } 108 | 109 | // Get the underlying Archetype's componentSlice 110 | cSlice, ok := storage.slice.Get(loc.archId) 111 | if !ok { 112 | return nil 113 | } 114 | 115 | return &cSlice.comp[loc.index] 116 | } 117 | -------------------------------------------------------------------------------- /ecs.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | // This package provides ecs features. 4 | //go:generate go run ./internal/gen >> view_gen.go 5 | -------------------------------------------------------------------------------- /entity.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | // An Entity is essentially a map of components that is held external to a world. Useful for pulling full entities in and out of the world. 4 | // Deprecated: This type and its corresponding methods are tentative and might be replaced by something else. 5 | type Entity struct { 6 | // comp map[componentId]Component 7 | comp []Component 8 | } 9 | 10 | // Creates a new entity with the specified components 11 | func NewEntity(components ...Component) *Entity { 12 | return &Entity{ 13 | comp: components, 14 | } 15 | 16 | // c := make(map[componentId]Component) 17 | // for i := range components { 18 | // c[components[i].id()] = components[i] 19 | // } 20 | // return &Entity{ 21 | // comp: c, 22 | // } 23 | } 24 | 25 | // Returns the index that contains the same componentId or returns -1 26 | func (e *Entity) findIndex(compId CompId) int { 27 | for i := range e.comp { 28 | if compId == e.comp[i].CompId() { 29 | return i 30 | } 31 | } 32 | 33 | return -1 34 | } 35 | 36 | // Adds a component to an entity 37 | func (e *Entity) Add(components ...Component) { 38 | for i := range components { 39 | idx := e.findIndex(components[i].CompId()) 40 | if idx < 0 { 41 | e.comp = append(e.comp, components[i]) 42 | } else { 43 | e.comp[idx] = components[i] 44 | } 45 | } 46 | 47 | // for i := range components { 48 | // e.comp[components[i].id()] = components[i] 49 | // } 50 | } 51 | 52 | // Merges e2 on top of e (meaning that we will overwrite e with values from e2) 53 | func (e *Entity) Merge(e2 *Entity) { 54 | e.Add(e2.comp...) 55 | 56 | // for k, v := range e2.comp { 57 | // e.comp[k] = v 58 | // } 59 | } 60 | 61 | // Returns a list of the components held by the entity 62 | func (e *Entity) Comps() []Component { 63 | return e.comp 64 | 65 | // ret := make([]Component, 0, len(e.comp)) 66 | // for _, v := range e.comp { 67 | // ret = append(ret, v) 68 | // } 69 | // return ret 70 | } 71 | 72 | // Reads a specific component from the entity, returns false if the component doesn't exist 73 | func ReadFromEntity[T any](ent *Entity) (T, bool) { 74 | var t T 75 | n := name(t) 76 | idx := ent.findIndex(n) 77 | if idx < 0 { 78 | return t, false 79 | } 80 | 81 | icomp := ent.comp[idx] 82 | 83 | val, ok := icomp.(T) 84 | if ok { 85 | return val, true 86 | } 87 | 88 | return icomp.(box[T]).val, true 89 | 90 | // var t T 91 | // n := name(t) 92 | 93 | // icomp, ok := ent.comp[n] 94 | // if !ok { 95 | // return t, false 96 | // } 97 | // return icomp.(Box[T]).Comp, true 98 | } 99 | 100 | // Writes the entire entity to the world 101 | func (ent *Entity) Write(world *World, id Id) { 102 | world.Write(id, ent.comp...) 103 | 104 | // comps := ent.Comps() 105 | // world.Write(id, comps...) 106 | } 107 | 108 | // Reads the entire entity out of the world and into an *Entity object. Returns nil if the entity doesn't exist 109 | func ReadEntity(world *World, id Id) *Entity { 110 | entLoc, ok := world.arch.Get(id) 111 | if !ok { 112 | return nil 113 | } 114 | 115 | return world.engine.ReadEntity(entLoc, id) 116 | } 117 | 118 | // Deletes a component on this entity 119 | func (e *Entity) Delete(c Component) { 120 | compId := c.CompId() 121 | idx := e.findIndex(compId) 122 | if idx < 0 { 123 | return 124 | } 125 | 126 | // If index does exist, then cut it out 127 | e.comp[idx] = e.comp[len(e.comp)-1] 128 | e.comp = e.comp[:len(e.comp)-1] 129 | 130 | // delete(e.comp, c.id()) 131 | } 132 | 133 | // Clears the map, but retains the space 134 | func (e *Entity) Clear() { 135 | e.comp = e.comp[:0] 136 | 137 | // // Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler 138 | // for k := range e.comp { 139 | // delete(e.comp, k) 140 | // } 141 | } 142 | 143 | // TODO revisit this abstraction 144 | // type Copier interface { 145 | // Copy() interface{} 146 | // } 147 | 148 | // func (e Entity) Copy() Entity { 149 | // copy := BlankEntity() 150 | // for k,v := range e { 151 | // c, ok := v.(Copier) 152 | // if ok { 153 | // // fmt.Println("Copying:", k) 154 | // // If the component has a custom copy interface, then copy it 155 | // copy[k] = c.Copy() 156 | // } else { 157 | // // Else just copy the top level struct 158 | // copy[k] = v 159 | // } 160 | // } 161 | // return copy 162 | // } 163 | 164 | // A RawEntity is like an Entity, but every component is actually a pointer to the underlying component. I mostly use this to build inspector UIs that can directly modify an entity 165 | // Deprecated: This type and its corresponding methods are tentative and might be replaced by something else. 166 | type RawEntity struct { 167 | comp map[CompId]any 168 | } 169 | 170 | // Creates a new entity with the specified components 171 | func NewRawEntity(components ...any) *RawEntity { 172 | c := make(map[CompId]any) 173 | for i := range components { 174 | c[name(components[i])] = components[i] 175 | } 176 | return &RawEntity{ 177 | comp: c, 178 | } 179 | } 180 | 181 | // Adds a component to an entity 182 | func (e *RawEntity) Add(components ...any) { 183 | for i := range components { 184 | e.comp[name(components[i])] = components[i] 185 | } 186 | } 187 | 188 | // Merges e2 on top of e (meaning that we will overwrite e with values from e2) 189 | func (e *RawEntity) Merge(e2 *RawEntity) { 190 | for k, v := range e2.comp { 191 | e.comp[k] = v 192 | } 193 | } 194 | 195 | // Returns a list of the components held by the entity 196 | func (e *RawEntity) Comps() []Component { 197 | ret := make([]Component, 0, len(e.comp)) 198 | // for _, v := range e.comp { 199 | // ret = append(ret, v) 200 | // } 201 | for compId := range maxComponentId { 202 | v, ok := e.comp[CompId(compId)] 203 | if !ok { continue } 204 | ret = append(ret, v.(Component)) 205 | } 206 | return ret 207 | } 208 | 209 | // // Reads a specific component from the entity, returns false if the component doesn't exist 210 | // func ReadFromRawEntity[T any](ent *RawEntity) (T, bool) { 211 | // var t T 212 | // n := name(t) 213 | 214 | // icomp, ok := ent.comp[n] 215 | // if !ok { 216 | // return t, false 217 | // } 218 | // return icomp.(Box[T]).Comp, true 219 | // } 220 | 221 | // Writes the entire entity to the world 222 | // func (ent *RawEntity) Write(world *World, id Id) { 223 | // comps := ent.Comps() 224 | // world.Write(id, comps...) 225 | // } 226 | 227 | // Reads the entire entity out of the world and into an *RawEntity object. Returns nil if the entity doesn't exist. RawEntity is lik 228 | func ReadRawEntity(world *World, id Id) *RawEntity { 229 | loc, ok := world.arch.Get(id) 230 | if !ok { 231 | return nil 232 | } 233 | 234 | return world.engine.ReadRawEntity(loc, id) 235 | } 236 | 237 | // Deletes a component on this entity 238 | func (e *RawEntity) Delete(c Component) { 239 | delete(e.comp, name(c)) 240 | } 241 | 242 | // Clears the map, but retains the space 243 | func (e *RawEntity) Clear() { 244 | // Clearing Optimization: https://go.dev/doc/go1.11#performance-compiler 245 | for k := range e.comp { 246 | delete(e.comp, k) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "reflect" 4 | 5 | type EventId int 6 | 7 | var eventRegistryCounter EventId = 0 8 | var registeredEvents = make(map[reflect.Type]EventId, 0) 9 | 10 | // This function is not thread safe 11 | func NewEvent[T any]() EventId { 12 | var t T 13 | typeof := reflect.TypeOf(t) 14 | eventId, ok := registeredEvents[typeof] 15 | if !ok { 16 | eventId = eventRegistryCounter 17 | registeredEvents[typeof] = eventId 18 | eventRegistryCounter++ 19 | } 20 | return eventId 21 | } 22 | 23 | type Event interface { 24 | EventId() EventId 25 | } 26 | 27 | type Trigger[T Event] struct { 28 | Id Id // If set, it is the entity Id that this event was triggered on 29 | Data T 30 | } 31 | 32 | type Handler interface { 33 | Run(id Id, event any) 34 | EventTrigger() EventId 35 | } 36 | 37 | type handlerData[E Event] struct { 38 | lambda func(Trigger[E]) 39 | } 40 | 41 | func (h handlerData[E]) Run(id Id, event any) { 42 | e := event.(E) 43 | trigger := Trigger[E]{id, e} 44 | h.lambda(trigger) 45 | } 46 | 47 | func (h handlerData[E]) EventTrigger() EventId { 48 | var e E 49 | return e.EventId() 50 | } 51 | 52 | func NewHandler[E Event](f func(trigger Trigger[E])) handlerData[E] { 53 | return handlerData[E]{ 54 | lambda: f, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/unitoftime/ecs" 8 | ) 9 | 10 | // This example illustrates the primary use cases for the ecs 11 | 12 | type Name string 13 | 14 | type Position struct { 15 | X, Y, Z float64 16 | } 17 | 18 | type Velocity struct { 19 | X, Y, Z float64 20 | } 21 | 22 | type PrintMessage struct { 23 | Msg string 24 | } 25 | 26 | var PrintMessageEventId = ecs.NewEvent[PrintMessage]() 27 | 28 | func (p PrintMessage) EventId() ecs.EventId { 29 | return PrintMessageEventId 30 | } 31 | 32 | func main() { 33 | // Create a New World 34 | world := ecs.NewWorld() 35 | 36 | // You can manually spawn entities like this 37 | { 38 | cmd := ecs.NewCommandQueue(world) 39 | 40 | // Add entities 41 | cmd.SpawnEmpty(). 42 | Insert(ecs.C(Position{1, 2, 3})). 43 | Insert(ecs.C(Velocity{1, 2, 3})). 44 | Insert(ecs.C(Name("My First Entity"))) 45 | cmd.Execute() 46 | } 47 | 48 | // Adding Component hooks 49 | { 50 | world.SetHookOnAdd(ecs.C(Velocity{}), 51 | ecs.NewHandler(func(trigger ecs.Trigger[ecs.OnAdd]) { 52 | fmt.Println("Hook:", trigger) 53 | })) 54 | 55 | cmd := ecs.NewCommandQueue(world) 56 | cmd.SpawnEmpty(). 57 | Insert(ecs.C(Position{1, 2, 3})). 58 | Insert(ecs.C(Velocity{1, 2, 3})). 59 | Insert(ecs.C(Name("My First Entity"))) 60 | cmd.Execute() 61 | 62 | } 63 | 64 | // Adding Observers 65 | { 66 | cmd := ecs.NewCommandQueue(world) 67 | 68 | // You can add observer handlers which run as a result of triggered events 69 | world.AddObserver( 70 | ecs.NewHandler(func(trigger ecs.Trigger[PrintMessage]) { 71 | fmt.Println("Observer 1:", trigger.Data.Msg) 72 | })) 73 | world.AddObserver( 74 | ecs.NewHandler(func(trigger ecs.Trigger[PrintMessage]) { 75 | fmt.Println("Observer 2!", trigger.Data.Msg) 76 | })) 77 | 78 | cmd.Trigger(PrintMessage{"Hello"}) 79 | cmd.Trigger(PrintMessage{"World"}) 80 | 81 | cmd.Execute() 82 | } 83 | 84 | scheduler := ecs.NewScheduler(world) 85 | 86 | // Append physics systems, these run on a fixed time step, so dt will always be constant 87 | scheduler.AddSystems(ecs.StageFixedUpdate, 88 | // Comment out if you want to spawn a new entity every frame 89 | // ecs.NewSystem1(SpawnSystem), 90 | 91 | // Option A: Create a function that returns a system 92 | MoveSystemOption_A(world), 93 | 94 | // Option B: Use the dynamic injection to create a system for you 95 | ecs.NewSystem1(MoveSystemOption_B), 96 | 97 | ecs.NewSystem1(PrintSystem), 98 | 99 | ecs.NewSystem1(TriggerSystem), 100 | ) 101 | 102 | // Also, add render systems if you want, These run as fast as possible 103 | // scheduler.AppendRender() 104 | 105 | // This will block until the scheduler exits `scheduler.SetQuit(true)` 106 | scheduler.Run() 107 | } 108 | 109 | // Note: This system wasn't added to the scheduler, so that I wouldn't constantly spawn entities in the physics loop 110 | // But, you can rely on commands to get injected for you, just like a query. 111 | func SpawnSystem(dt time.Duration, commands *ecs.CommandQueue) { 112 | // Note: The scheduler will automatically call .Execute() the command queue 113 | cmd := commands.SpawnEmpty() 114 | 115 | name := Name(fmt.Sprintf("My Entity %d", cmd.Id())) 116 | cmd.Insert(ecs.C(Position{1, 2, 3})). 117 | Insert(ecs.C(Velocity{1, 2, 3})). 118 | Insert(ecs.C(name)) 119 | } 120 | 121 | // Option A: Define and return a system based on a closure 122 | // - Provides a bit more flexibility if you need to establish variables ahead of the system 123 | func MoveSystemOption_A(world *ecs.World) ecs.System { 124 | query := ecs.Query2[Position, Velocity](world) 125 | 126 | return ecs.NewSystem(func(dt time.Duration) { 127 | query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { 128 | sec := dt.Seconds() 129 | 130 | pos.X += vel.X * sec 131 | pos.Y += vel.Y * sec 132 | pos.Z += vel.Z * sec 133 | }) 134 | }) 135 | } 136 | 137 | // Option 2: Define a system and have all the queries created and injected for you 138 | // - Can be used for simpler systems that don't need to track much system-internal state 139 | // - Use the `ecs.NewSystemN(world, systemFunction)` syntax (Where N represents the number of required resources) 140 | func MoveSystemOption_B(dt time.Duration, query *ecs.View2[Position, Velocity]) { 141 | query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) { 142 | sec := dt.Seconds() 143 | 144 | pos.X += vel.X * sec 145 | pos.Y += vel.Y * sec 146 | pos.Z += vel.Z * sec 147 | }) 148 | } 149 | 150 | // A system that prints all entity names and their positions 151 | func PrintSystem(dt time.Duration, query *ecs.View2[Name, Position]) { 152 | query.MapId(func(id ecs.Id, name *Name, pos *Position) { 153 | fmt.Printf("%s: %v\n", *name, pos) 154 | }) 155 | } 156 | 157 | func TriggerSystem(dt time.Duration, cmd *ecs.CommandQueue) { 158 | cmd.Trigger(PrintMessage{"Hello"}) 159 | } 160 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "slices" 5 | ) 6 | 7 | // Optional - Lets you view even if component is missing (func will return nil) 8 | // With - Lets you add additional components that must be present 9 | // Without - Lets you add additional components that must not be present 10 | type Filter interface { 11 | Filter([]CompId) []CompId 12 | } 13 | 14 | type without struct { 15 | mask archetypeMask 16 | } 17 | 18 | // Creates a filter to ensure that entities will not have the specified components 19 | func Without(comps ...any) without { 20 | return without{ 21 | mask: buildArchMaskFromAny(comps...), 22 | } 23 | } 24 | func (w without) Filter(list []CompId) []CompId { 25 | return list // Dont filter anything. We need to exclude later on 26 | // return append(list, w.comps...) 27 | } 28 | 29 | type with struct { 30 | comps []CompId 31 | } 32 | 33 | // Creates a filter to ensure that entities have the specified components 34 | func With(comps ...any) with { 35 | ids := make([]CompId, len(comps)) 36 | for i := range comps { 37 | ids[i] = name(comps[i]) 38 | } 39 | return with{ 40 | comps: ids, 41 | } 42 | } 43 | 44 | func (w with) Filter(list []CompId) []CompId { 45 | return append(list, w.comps...) 46 | } 47 | 48 | type optional struct { 49 | comps []CompId 50 | } 51 | 52 | // Creates a filter to make the query still iterate even if a specific component is missing, in which case you'll get nil if the component isn't there when accessed 53 | func Optional(comps ...any) optional { 54 | ids := make([]CompId, len(comps)) 55 | for i := range comps { 56 | ids[i] = name(comps[i]) 57 | } 58 | 59 | return optional{ 60 | comps: ids, 61 | } 62 | } 63 | 64 | func (f optional) Filter(list []CompId) []CompId { 65 | for i := 0; i < len(list); i++ { 66 | for j := range f.comps { 67 | if list[i] == f.comps[j] { 68 | // If we have a match, we want to remove it from the list. 69 | list[i] = list[len(list)-1] 70 | list = list[:len(list)-1] 71 | 72 | // Because we just moved the last element to index i, we need to go back to process that element 73 | i-- 74 | break 75 | } 76 | } 77 | } 78 | return list 79 | } 80 | 81 | type filterList struct { 82 | comps []CompId 83 | withoutArchMask archetypeMask 84 | cachedArchetypeGeneration int // Denotes the world's archetype generation that was used to create the list of archIds. If the world has a new generation, we should probably regenerate 85 | archIds []archetypeId 86 | } 87 | 88 | func newFilterList(comps []CompId, filters ...Filter) filterList { 89 | var withoutArchMask archetypeMask 90 | for _, f := range filters { 91 | withoutFilter, isWithout := f.(without) 92 | if isWithout { 93 | withoutArchMask = withoutFilter.mask 94 | } else { 95 | comps = f.Filter(comps) 96 | } 97 | } 98 | 99 | return filterList{ 100 | comps: comps, 101 | withoutArchMask: withoutArchMask, 102 | archIds: make([]archetypeId, 0), 103 | } 104 | } 105 | func (f *filterList) regenerate(world *World) { 106 | if world.engine.getGeneration() != f.cachedArchetypeGeneration { 107 | f.archIds = world.engine.FilterList(f.archIds, f.comps) 108 | 109 | if f.withoutArchMask != blankArchMask { 110 | f.archIds = slices.DeleteFunc(f.archIds, func(archId archetypeId) bool { 111 | return world.engine.dcr.archIdOverlapsMask(archId, f.withoutArchMask) 112 | }) 113 | } 114 | 115 | f.cachedArchetypeGeneration = world.engine.getGeneration() 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unitoftime/ecs 2 | 3 | go 1.23 4 | 5 | require github.com/unitoftime/cod v0.0.0-20250419234656-6f109c444441 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/unitoftime/cod v0.0.0-20250419234656-6f109c444441 h1:VKmfg9J3dOPAJPZOzmSmoDIgkU+9kkRUPlah3QPZ+cQ= 2 | github.com/unitoftime/cod v0.0.0-20250419234656-6f109c444441/go.mod h1:9E3tPG/n6cN7k030C0lMWNe0q2f1Qh1BvyNdxISCKWE= 3 | -------------------------------------------------------------------------------- /hashmap.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "github.com/unitoftime/ecs/internal/intmap" 5 | ) 6 | 7 | type intkey interface { 8 | // comparable 9 | ~int | ~uint | ~int64 | ~uint64 | ~int32 | ~uint32 | ~int16 | ~uint16 | ~int8 | ~uint8 | ~uintptr 10 | } 11 | 12 | // This is useful for testing different map implementations in my workload 13 | type internalMap[K intkey, V any] struct { 14 | inner *intmap.Map[K, V] 15 | } 16 | 17 | func newMap[K intkey, V any](size int) *internalMap[K, V] { 18 | return &internalMap[K, V]{ 19 | intmap.New[K, V](0), 20 | } 21 | } 22 | func (m *internalMap[K, V]) Len() int { 23 | return m.inner.Len() 24 | } 25 | 26 | func (m *internalMap[K, V]) Get(k K) (V, bool) { 27 | return m.inner.Get(k) 28 | } 29 | 30 | func (m *internalMap[K, V]) Put(k K, val V) { 31 | m.inner.Put(k, val) 32 | } 33 | 34 | func (m *internalMap[K, V]) Delete(k K) { 35 | m.inner.Del(k) 36 | } 37 | 38 | func (m *internalMap[K, V]) Has(k K) bool { 39 | _, has := m.inner.Get(k) 40 | return has 41 | } 42 | 43 | //-------------------------------------------------------------------------------- 44 | // TODO: Move to generational Ids 45 | 46 | // This is useful for testing different map implementations in my workload 47 | type locMap struct { 48 | // inner *LocMapImpl 49 | inner *intmap.Map[Id, entLoc] 50 | } 51 | 52 | func newLocMap(size int) locMap { 53 | return locMap{ 54 | // NewLocMapImpl(size), 55 | intmap.New[Id, entLoc](0), 56 | } 57 | } 58 | func (m *locMap) Len() int { 59 | return m.inner.Len() 60 | } 61 | 62 | func (m *locMap) Get(k Id) (entLoc, bool) { 63 | return m.inner.Get(k) 64 | } 65 | 66 | func (m *locMap) Put(k Id, val entLoc) { 67 | m.inner.Put(k, val) 68 | } 69 | 70 | func (m *locMap) Delete(k Id) { 71 | m.inner.Del(k) 72 | } 73 | 74 | func (m *locMap) Has(k Id) bool { 75 | _, has := m.inner.Get(k) 76 | return has 77 | } 78 | 79 | // -------------------------------------------------------------------------------- 80 | // const fillFactor64 = 0.5 81 | 82 | // // Hashing Reference: https://gist.github.com/badboy/6267743 83 | // func phiMix64(x int) int { 84 | // // Note: With this, we are only just a bit faster than swissmap 85 | // h := x * (-1_640_531_527) // This is just the int32 version of the 0x9E3779B9 86 | // return h ^ (h >> 16) 87 | 88 | // // // TODO: track collision counts and compare before enabling this 89 | // // // Theory: Because ecs.Id is just incremented by 1 each time, it might be effective to just always take the next slot 90 | // // return x + x 91 | // } 92 | 93 | // type locPair struct { 94 | // K Id 95 | // V entLoc 96 | // } 97 | 98 | // // LocMapImpl is a hashmap where the keys are some any integer type. 99 | // type LocMapImpl struct { 100 | // data []locPair 101 | // size int 102 | 103 | // zeroVal entLoc // value of 'zero' key 104 | // hasZeroKey bool // do we have 'zero' key in the map? 105 | // } 106 | 107 | // // New creates a new map with keys being any integer subtype. 108 | // // The map can store up to the given capacity before reallocation and rehashing occurs. 109 | // func NewLocMapImpl(capacity int) *LocMapImpl { 110 | // return &LocMapImpl{ 111 | // data: make([]locPair, arraySize(capacity, fillFactor64)), 112 | // } 113 | // } 114 | 115 | // // Get returns the value if the key is found. 116 | // func (m *LocMapImpl) Get(key Id) (entLoc, bool) { 117 | // if key == InvalidEntity { 118 | // if m.hasZeroKey { 119 | // return m.zeroVal, true 120 | // } 121 | // var zero entLoc 122 | // return zero, false 123 | // } 124 | 125 | // idx := m.startIndex(key) 126 | // p := m.data[idx] 127 | 128 | // if p.K == InvalidEntity { // end of chain already 129 | // var zero entLoc 130 | // return zero, false 131 | // } 132 | // if p.K == key { // we check zero prior to this call 133 | // return p.V, true 134 | // } 135 | 136 | // // hash collision, seek next hash match, bailing on first empty 137 | // for { 138 | // idx = m.nextIndex(idx) 139 | // p = m.data[idx] 140 | // if p.K == InvalidEntity { 141 | // var zero entLoc 142 | // return zero, false 143 | // } 144 | // if p.K == key { 145 | // return p.V, true 146 | // } 147 | // } 148 | // } 149 | 150 | // // Put adds or updates key with value val. 151 | // func (m *LocMapImpl) Put(key Id, val entLoc) { 152 | // if key == InvalidEntity { 153 | // if !m.hasZeroKey { 154 | // m.size++ 155 | // } 156 | // m.zeroVal = val 157 | // m.hasZeroKey = true 158 | // return 159 | // } 160 | 161 | // idx := m.startIndex(key) 162 | // p := &m.data[idx] 163 | 164 | // if p.K == InvalidEntity { // end of chain already 165 | // p.K = key 166 | // p.V = val 167 | // if m.size >= m.sizeThreshold() { 168 | // m.rehash() 169 | // } else { 170 | // m.size++ 171 | // } 172 | // return 173 | // } else if p.K == key { // overwrite existing value 174 | // p.V = val 175 | // return 176 | // } 177 | 178 | // // hash collision, seek next empty or key match 179 | // for { 180 | // idx = m.nextIndex(idx) 181 | // p = &m.data[idx] 182 | 183 | // if p.K == InvalidEntity { 184 | // p.K = key 185 | // p.V = val 186 | // if m.size >= m.sizeThreshold() { 187 | // m.rehash() 188 | // } else { 189 | // m.size++ 190 | // } 191 | // return 192 | // } else if p.K == key { 193 | // p.V = val 194 | // return 195 | // } 196 | // } 197 | // } 198 | 199 | // // Clear removes all items from the map, but keeps the internal buffers for reuse. 200 | // func (m *LocMapImpl) Clear() { 201 | // var zero entLoc 202 | // m.hasZeroKey = false 203 | // m.zeroVal = zero 204 | 205 | // // compiles down to runtime.memclr() 206 | // for i := range m.data { 207 | // m.data[i] = locPair{} 208 | // } 209 | 210 | // m.size = 0 211 | // } 212 | 213 | // func (m *LocMapImpl) rehash() { 214 | // oldData := m.data 215 | // m.data = make([]locPair, 2*len(m.data)) 216 | 217 | // // reset size 218 | // if m.hasZeroKey { 219 | // m.size = 1 220 | // } else { 221 | // m.size = 0 222 | // } 223 | 224 | // forEach64(oldData, m.Put) 225 | // // for _, p := range oldData { 226 | // // if p.K != InvalidEntity { 227 | // // m.Put(p.K, p.V) 228 | // // } 229 | // // } 230 | // } 231 | 232 | // // Len returns the number of elements in the map. 233 | // func (m *LocMapImpl) Len() int { 234 | // return m.size 235 | // } 236 | 237 | // func (m *LocMapImpl) sizeThreshold() int { 238 | // return int(math.Floor(float64(len(m.data)) * fillFactor64)) 239 | // } 240 | 241 | // func (m *LocMapImpl) startIndex(key Id) int { 242 | // return phiMix64(int(key)) & (len(m.data) - 1) 243 | // } 244 | 245 | // func (m *LocMapImpl) nextIndex(idx int) int { 246 | // return (idx + 1) & (len(m.data) - 1) 247 | // } 248 | 249 | // func forEach64(pairs []locPair, f func(k Id, v entLoc)) { 250 | // for _, p := range pairs { 251 | // if p.K != InvalidEntity { 252 | // f(p.K, p.V) 253 | // } 254 | // } 255 | // } 256 | 257 | // // Del deletes a key and its value, returning true iff the key was found 258 | // func (m *LocMapImpl) Del(key Id) bool { 259 | // if key == InvalidEntity { 260 | // if m.hasZeroKey { 261 | // m.hasZeroKey = false 262 | // m.size-- 263 | // return true 264 | // } 265 | // return false 266 | // } 267 | 268 | // idx := m.startIndex(key) 269 | // p := m.data[idx] 270 | 271 | // if p.K == key { 272 | // // any keys that were pushed back needs to be shifted nack into the empty slot 273 | // // to avoid breaking the chain 274 | // m.shiftKeys(idx) 275 | // m.size-- 276 | // return true 277 | // } else if p.K == InvalidEntity { // end of chain already 278 | // return false 279 | // } 280 | 281 | // for { 282 | // idx = m.nextIndex(idx) 283 | // p = m.data[idx] 284 | 285 | // if p.K == key { 286 | // // any keys that were pushed back needs to be shifted nack into the empty slot 287 | // // to avoid breaking the chain 288 | // m.shiftKeys(idx) 289 | // m.size-- 290 | // return true 291 | // } else if p.K == InvalidEntity { 292 | // return false 293 | // } 294 | 295 | // } 296 | // } 297 | 298 | // func (m *LocMapImpl) shiftKeys(idx int) int { 299 | // // Shift entries with the same hash. 300 | // // We need to do this on deletion to ensure we don't have zeroes in the hash chain 301 | // for { 302 | // var p locPair 303 | // lastIdx := idx 304 | // idx = m.nextIndex(idx) 305 | // for { 306 | // p = m.data[idx] 307 | // if p.K == InvalidEntity { 308 | // m.data[lastIdx] = locPair{} 309 | // return lastIdx 310 | // } 311 | 312 | // slot := m.startIndex(p.K) 313 | // if lastIdx <= idx { 314 | // if lastIdx >= slot || slot > idx { 315 | // break 316 | // } 317 | // } else { 318 | // if lastIdx >= slot && slot > idx { 319 | // break 320 | // } 321 | // } 322 | // idx = m.nextIndex(idx) 323 | // } 324 | // m.data[lastIdx] = p 325 | // } 326 | // } 327 | 328 | // func nextPowerOf2(x uint32) uint32 { 329 | // if x == math.MaxUint32 { 330 | // return x 331 | // } 332 | 333 | // if x == 0 { 334 | // return 1 335 | // } 336 | 337 | // x-- 338 | // x |= x >> 1 339 | // x |= x >> 2 340 | // x |= x >> 4 341 | // x |= x >> 8 342 | // x |= x >> 16 343 | 344 | // return x + 1 345 | // } 346 | 347 | // func arraySize(exp int, fill float64) int { 348 | // s := nextPowerOf2(uint32(math.Ceil(float64(exp) / fill))) 349 | // if s < 2 { 350 | // s = 2 351 | // } 352 | // return int(s) 353 | // } 354 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type OnAdd struct { 4 | compId CompId 5 | } 6 | 7 | var _onAddId = NewEvent[OnAdd]() 8 | 9 | func (p OnAdd) EventId() EventId { 10 | return _onAddId 11 | } 12 | 13 | //-------------------------------------------------------------------------------- 14 | 15 | func (e *archEngine) runFinalizedHooks(id Id) { 16 | // Run, then clear add hooks 17 | for i := range e.finalizeOnAdd { 18 | e.runAddHook(id, e.finalizeOnAdd[i]) 19 | } 20 | e.finalizeOnAdd = e.finalizeOnAdd[:0] 21 | 22 | // TODO: Run other hooks? 23 | } 24 | 25 | func (e *archEngine) runAddHook(id Id, compId CompId) { 26 | current := e.onAddHooks[compId] 27 | if current == nil { 28 | return 29 | } 30 | 31 | current.Run(id, OnAdd{compId}) 32 | } 33 | 34 | // Marks all provided components 35 | func markComponents(slice []CompId, comp ...Component) []CompId { 36 | for i := range comp { 37 | slice = append(slice, comp[i].CompId()) 38 | } 39 | return slice 40 | } 41 | 42 | // Marks the provided components, excluding ones that are already set by the old mask 43 | func markNewComponents(slice []CompId, oldMask archetypeMask, comp ...Component) []CompId { 44 | for i := range comp { 45 | compId := comp[i].CompId() 46 | if oldMask.hasComponent(compId) { 47 | continue // Skip: Component already set in oldMask 48 | } 49 | 50 | slice = append(slice, compId) 51 | } 52 | return slice 53 | } 54 | 55 | func markComponentMask(slice []CompId, mask archetypeMask) []CompId { 56 | // TODO: Optimization: Technically this only has to loop to the max registered compId, not the max possible. Also see optimization note in archEngine 57 | for compId := CompId(0); compId <= maxComponentId; compId++ { 58 | if mask.hasComponent(compId) { 59 | slice = append(slice, compId) 60 | } 61 | } 62 | 63 | return slice 64 | } 65 | 66 | func markComponentDiff(slice []CompId, newMask, oldMask archetypeMask) []CompId { 67 | mask := newMask.bitwiseClear(oldMask) 68 | return markComponentMask(slice, mask) 69 | } 70 | -------------------------------------------------------------------------------- /inject.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "time" 7 | ) 8 | 9 | func GetInjectable2[T any](world *World, t T) T { 10 | return GetInjectable[T](world) 11 | } 12 | 13 | func GetInjectable[T any](world *World) T { 14 | var t T 15 | name := resourceName(t) 16 | 17 | // 1. If already created, just use this variable 18 | anyVal, ok := world.resources[name] 19 | if ok { 20 | return anyVal.(T) 21 | } 22 | 23 | // 2. If supports initialization, then make a new one and return it 24 | tAny := any(t) 25 | initializer, ok := tAny.(Initializer) 26 | if ok { 27 | anyVal = initializer.Initialize(world) 28 | world.resources[name] = anyVal 29 | return anyVal.(T) 30 | } 31 | 32 | // 3. Fallback: Just return the default value for whatever it is 33 | world.resources[name] = t 34 | return t 35 | } 36 | 37 | type Initializer interface { 38 | Initialize(*World) any 39 | } 40 | 41 | type SystemBuilder interface { 42 | Build(world *World) System 43 | } 44 | 45 | type System1[A any] struct { 46 | lambda func(dt time.Duration, a A) 47 | } 48 | 49 | func (s System1[A]) Build(world *World) System { 50 | aRes := GetInjectable[A](world) 51 | 52 | systemName := runtime.FuncForPC(reflect.ValueOf(any(s.lambda)).Pointer()).Name() 53 | return System{ 54 | Name: systemName, 55 | Func: func(dt time.Duration) { 56 | s.lambda(dt, aRes) 57 | }, 58 | } 59 | } 60 | func NewSystem1[A any](lambda func(dt time.Duration, a A)) System1[A] { 61 | return System1[A]{ 62 | lambda: lambda, 63 | } 64 | } 65 | 66 | type System2[A, B any] struct { 67 | lambda func(dt time.Duration, a A, b B) 68 | } 69 | 70 | func (s System2[A, B]) Build(world *World) System { 71 | aRes := GetInjectable[A](world) 72 | bRes := GetInjectable[B](world) 73 | 74 | systemName := runtime.FuncForPC(reflect.ValueOf(any(s.lambda)).Pointer()).Name() 75 | 76 | return System{ 77 | Name: systemName, 78 | Func: func(dt time.Duration) { 79 | s.lambda(dt, aRes, bRes) 80 | }, 81 | } 82 | } 83 | func NewSystem2[A, B any](lambda func(dt time.Duration, a A, b B)) System2[A, B] { 84 | return System2[A, B]{ 85 | lambda: lambda, 86 | } 87 | } 88 | 89 | type System3[A, B, C any] struct { 90 | lambda func(dt time.Duration, a A, b B, c C) 91 | } 92 | 93 | func (s System3[A, B, C]) Build(world *World) System { 94 | aRes := GetInjectable[A](world) 95 | bRes := GetInjectable[B](world) 96 | cRes := GetInjectable[C](world) 97 | 98 | systemName := runtime.FuncForPC(reflect.ValueOf(any(s.lambda)).Pointer()).Name() 99 | 100 | return System{ 101 | Name: systemName, 102 | Func: func(dt time.Duration) { 103 | s.lambda(dt, aRes, bRes, cRes) 104 | }, 105 | } 106 | } 107 | func NewSystem3[A, B, C any](lambda func(dt time.Duration, a A, b B, c C)) System3[A, B, C] { 108 | return System3[A, B, C]{ 109 | lambda: lambda, 110 | } 111 | } 112 | 113 | // func NewSystem4[A, B, C, D any](world *World, lambda func(dt time.Duration, a A, b B, c C, d D)) System { 114 | // aRes := GetInjectable[A](world) 115 | // bRes := GetInjectable[B](world) 116 | // cRes := GetInjectable[C](world) 117 | // dRes := GetInjectable[D](world) 118 | 119 | // systemName := runtime.FuncForPC(reflect.ValueOf(any(lambda)).Pointer()).Name() 120 | 121 | // return System{ 122 | // Name: systemName, 123 | // Func: func(dt time.Duration) { 124 | // lambda(dt, aRes, bRes, cRes, dRes) 125 | // }, 126 | // } 127 | // } 128 | -------------------------------------------------------------------------------- /internal/gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "go/format" 7 | "io/fs" 8 | "os" 9 | "strings" 10 | "text/template" 11 | ) 12 | 13 | //go:embed view.tgo 14 | var viewTemplate string 15 | 16 | type viewData struct { 17 | Views [][]string 18 | } 19 | 20 | // This is used to generate code for the ecs library 21 | func main() { 22 | data := viewData{ 23 | Views: [][]string{ 24 | []string{"A"}, 25 | []string{"A", "B"}, 26 | []string{"A", "B", "C"}, 27 | []string{"A", "B", "C", "D"}, 28 | []string{"A", "B", "C", "D", "E"}, 29 | []string{"A", "B", "C", "D", "E", "F"}, 30 | []string{"A", "B", "C", "D", "E", "F", "G"}, 31 | []string{"A", "B", "C", "D", "E", "F", "G", "H"}, 32 | []string{"A", "B", "C", "D", "E", "F", "G", "H", "I"}, 33 | []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"}, 34 | []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"}, 35 | []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"}, 36 | }, 37 | } 38 | funcs := template.FuncMap{ 39 | "join": strings.Join, 40 | "lower": func(val string) string { 41 | return strings.ToLower(val) 42 | }, 43 | "nils": func(n int) string { 44 | val := make([]string, 0) 45 | for i := 0; i < n; i++ { 46 | val = append(val, "nil") 47 | } 48 | return strings.Join(val, ", ") 49 | }, 50 | "retlist": func(val []string) string { 51 | ret := make([]string, len(val)) 52 | for i := range val { 53 | ret[i] = "ret" + val[i] 54 | } 55 | return strings.Join(ret, ", ") 56 | }, 57 | "lambdaArgs": func(val []string) string { 58 | ret := make([]string, len(val)) 59 | for i := range val { 60 | ret[i] = strings.ToLower(val[i]) + " *" + val[i] 61 | } 62 | return strings.Join(ret, ", ") 63 | }, 64 | "sliceLambdaArgs": func(val []string) string { 65 | ret := make([]string, len(val)) 66 | for i := range val { 67 | ret[i] = strings.ToLower(val[i]) + " []" + val[i] 68 | } 69 | return strings.Join(ret, ", ") 70 | }, 71 | "parallelLambdaStructArgs": func(val []string) string { 72 | ret := make([]string, len(val)) 73 | for i := range val { 74 | ret[i] = strings.ToLower(val[i]) + " []" + val[i] 75 | } 76 | return strings.Join(ret, "; ") 77 | }, 78 | "parallelLambdaArgsFromStruct": func(val []string) string { 79 | ret := make([]string, len(val)) 80 | for i := range val { 81 | ret[i] = "param" + val[i] 82 | } 83 | return strings.Join(ret, ", ") 84 | }, 85 | } 86 | 87 | t := template.Must(template.New("ViewTemplate").Funcs(funcs).Parse(viewTemplate)) 88 | 89 | buf := bytes.NewBuffer([]byte{}) 90 | 91 | t.Execute(buf, data) 92 | 93 | filename := "view_gen.go" 94 | 95 | // Attempt to write the file as formatted, falling back to writing it normally 96 | formatted, err := format.Source(buf.Bytes()) 97 | if err != nil { 98 | err = os.WriteFile(filename, buf.Bytes(), fs.ModePerm) 99 | if err != nil { 100 | panic(err) 101 | } 102 | panic(err) 103 | } 104 | 105 | err = os.WriteFile(filename, formatted, fs.ModePerm) 106 | if err != nil { 107 | panic(err) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/gen/old_view.tgo: -------------------------------------------------------------------------------- 1 | // Note: This just holds some old stuff 2 | 3 | // // Maps the lambda function across every entity which matched the specified filters. 4 | // func (v *View{{len $element}}[{{join $element ","}}]) MapId(lambda func(id Id, {{lambdaArgs $element}})) { 5 | // v.filter.regenerate(v.world) 6 | 7 | // {{range $ii, $arg := $element}} 8 | // var slice{{$arg}} *componentSlice[{{$arg}}] 9 | // var comp{{$arg}} []{{$arg}} 10 | // var ret{{$arg}} *{{$arg}} 11 | // {{end}} 12 | 13 | // for _, archId := range v.filter.archIds { 14 | // {{range $ii, $arg := $element}} 15 | // slice{{$arg}}, _ = v.storage{{$arg}}.slice[archId]{{end}} 16 | 17 | // lookup := v.world.engine.lookup[archId] 18 | // if lookup == nil { panic("LookupList is missing!") } 19 | // // lookup, ok := v.world.engine.lookup[archId] 20 | // // if !ok { panic("LookupList is missing!") } 21 | // ids := lookup.id 22 | 23 | 24 | // // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. 25 | // {{range $ii, $arg := $element}} 26 | // comp{{$arg}} = nil 27 | // if slice{{$arg}} != nil { 28 | // comp{{$arg}} = slice{{$arg}}.comp 29 | // }{{end}} 30 | 31 | // {{range $ii, $arg := $element}} 32 | // ret{{$arg}} = nil{{end}} 33 | // for idx := range ids { 34 | // if ids[idx] == InvalidEntity { continue } // Skip if its a hole 35 | // {{range $ii, $arg := $element}} 36 | // if comp{{$arg}} != nil { ret{{$arg}} = &comp{{$arg}}[idx] }{{end}} 37 | // lambda(ids[idx], {{retlist $element}}) 38 | // } 39 | 40 | // // // Option 2 - This is faster but has a combinatorial explosion problem 41 | // // if compA == nil && compB == nil { 42 | // // return 43 | // // } else if compA != nil && compB == nil { 44 | // // if len(ids) != len(compA) { 45 | // // panic("ERROR - Bounds don't match") 46 | // // } 47 | // // for i := range ids { 48 | // // if ids[i] == InvalidEntity { continue } 49 | // // lambda(ids[i], &compA[i], nil) 50 | // // } 51 | // // } else if compA == nil && compB != nil { 52 | // // if len(ids) != len(compB) { 53 | // // panic("ERROR - Bounds don't match") 54 | // // } 55 | // // for i := range ids { 56 | // // if ids[i] == InvalidEntity { continue } 57 | // // lambda(ids[i], nil, &compB[i]) 58 | // // } 59 | // // } else if compA != nil && compB != nil { 60 | // // if len(ids) != len(compA) || len(ids) != len(compB) { 61 | // // panic("ERROR - Bounds don't match") 62 | // // } 63 | // // for i := range ids { 64 | // // if ids[i] == InvalidEntity { continue } 65 | // // lambda(ids[i], &compA[i], &compB[i]) 66 | // // } 67 | // // } 68 | // } 69 | 70 | // // Original - doesn't handle optional 71 | // // for _, archId := range v.filter.archIds { 72 | // // aSlice, ok := v.storageA.slice[archId] 73 | // // if !ok { continue } 74 | // // bSlice, ok := v.storageB.slice[archId] 75 | // // if !ok { continue } 76 | 77 | // // lookup, ok := v.world.engine.lookup[archId] 78 | // // if !ok { panic("LookupList is missing!") } 79 | 80 | // // ids := lookup.id 81 | // // aComp := aSlice.comp 82 | // // bComp := bSlice.comp 83 | // // if len(ids) != len(aComp) || len(ids) != len(bComp) { 84 | // // panic("ERROR - Bounds don't match") 85 | // // } 86 | // // for i := range ids { 87 | // // if ids[i] == InvalidEntity { continue } 88 | // // lambda(ids[i], &aComp[i], &bComp[i]) 89 | // // } 90 | // // } 91 | // } 92 | 93 | // // Maps the lambda function across every entity which matched the specified filters. Splits components into chunks of size up to `chunkSize` and then maps them in parallel. Smaller chunks results in highter overhead for small lambdas, but execution time is more predictable. If the chunk size is too hight, there is posibillity that not all the resources will utilized. 94 | // func (v *View{{len $element}}[{{join $element ","}}]) MapIdParallel(chunkSize int, lambda func(id Id, {{lambdaArgs $element}})) { 95 | // v.filter.regenerate(v.world) 96 | 97 | // {{range $ii, $arg := $element}} 98 | // var slice{{$arg}} *componentSlice[{{$arg}}] 99 | // var comp{{$arg}} []{{$arg}} 100 | // {{end}} 101 | 102 | 103 | // workDone := &sync.WaitGroup{} 104 | // type workPackage struct{start int; end int; ids []Id; {{parallelLambdaStructArgs $element}}} 105 | // newWorkChanel := make(chan workPackage) 106 | // mapWorker := func() { 107 | // defer workDone.Done() 108 | 109 | // for { 110 | // newWork, ok := <-newWorkChanel 111 | // if !ok { 112 | // return 113 | // } 114 | 115 | // // TODO: most probably this part ruins vectorization and SIMD. Maybe create new (faster) function where this will not occure? 116 | // {{range $ii, $arg := $element}} 117 | // var param{{$arg}} *{{$arg}}{{end}} 118 | 119 | // for i := newWork.start; i < newWork.end; i++ { 120 | // {{range $ii, $arg := $element}} 121 | // if newWork.{{lower $arg}} != nil { param{{$arg}} = &newWork.{{lower $arg}}[i]}{{end}} 122 | 123 | // lambda(newWork.ids[i], {{parallelLambdaArgsFromStruct $element}}) 124 | // } 125 | // } 126 | // } 127 | // parallelLevel := runtime.NumCPU()*2 128 | // for i := 0; i < parallelLevel; i++ { 129 | // go mapWorker() 130 | // } 131 | 132 | 133 | // for _, archId := range v.filter.archIds { 134 | // {{range $ii, $arg := $element}} 135 | // slice{{$arg}}, _ = v.storage{{$arg}}.slice[archId]{{end}} 136 | 137 | // lookup := v.world.engine.lookup[archId] 138 | // if lookup == nil { panic("LookupList is missing!") } 139 | // // lookup, ok := v.world.engine.lookup[archId] 140 | // // if !ok { panic("LookupList is missing!") } 141 | // ids := lookup.id 142 | 143 | // {{range $ii, $arg := $element}} 144 | // comp{{$arg}} = nil 145 | // if slice{{$arg}} != nil { 146 | // comp{{$arg}} = slice{{$arg}}.comp 147 | // }{{end}} 148 | 149 | // startWorkRangeIndex := -1 150 | // for idx := range ids { 151 | // //TODO: chunks may be very small because of holes. Some clever heuristic is required. Most probably this is a problem of storage segmentation, but not this map algorithm. 152 | // if ids[idx] == InvalidEntity { 153 | // if startWorkRangeIndex != -1 { 154 | // newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} 155 | // startWorkRangeIndex = -1 156 | // } 157 | // continue 158 | // } // Skip if its a hole 159 | 160 | // if startWorkRangeIndex == -1 { 161 | // startWorkRangeIndex = idx 162 | // } 163 | 164 | // if idx - startWorkRangeIndex >= chunkSize { 165 | // newWorkChanel <- workPackage{start: startWorkRangeIndex, end: idx+1, ids: ids, {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} 166 | // startWorkRangeIndex = -1 167 | // } 168 | // } 169 | 170 | // if startWorkRangeIndex != -1 { 171 | // newWorkChanel <- workPackage{start: startWorkRangeIndex, end: len(ids), {{range $ii, $arg := $element}} {{lower $arg}}: comp{{$arg}},{{end}}} 172 | // } 173 | // } 174 | 175 | // close(newWorkChanel) 176 | // workDone.Wait() 177 | // } 178 | 179 | // // Deprecated: This API is a tentative alternative way to map 180 | // func (v *View{{len $element}}[{{join $element ","}}]) MapSlices(lambda func(id []Id, {{sliceLambdaArgs $element}})) { 181 | // v.filter.regenerate(v.world) 182 | 183 | // id := make([][]Id, 0) 184 | 185 | // {{range $ii, $arg := $element}} 186 | // sliceList{{$arg}} := make([][]{{$arg}}, 0){{end}} 187 | 188 | // for _, archId := range v.filter.archIds { 189 | // {{range $ii, $arg := $element}} 190 | // slice{{$arg}}, ok := v.storage{{$arg}}.slice[archId] 191 | // if !ok { continue }{{end}} 192 | 193 | // lookup := v.world.engine.lookup[archId] 194 | // if lookup == nil { panic("LookupList is missing!") } 195 | // // lookup, ok := v.world.engine.lookup[archId] 196 | // // if !ok { panic("LookupList is missing!") } 197 | 198 | // id = append(id, lookup.id) 199 | // {{range $ii, $arg := $element}} 200 | // sliceList{{$arg}} = append(sliceList{{$arg}}, slice{{$arg}}.comp){{end}} 201 | // } 202 | 203 | // for idx := range id { 204 | // lambda(id[idx], 205 | // {{range $ii, $arg := $element}}sliceList{{$arg}}[idx],{{end}} 206 | // ) 207 | // } 208 | // } 209 | -------------------------------------------------------------------------------- /internal/gen/view.tgo: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "sync" 5 | "runtime" 6 | ) 7 | 8 | // Warning: This is an autogenerated file. Do not modify!! 9 | 10 | {{range $i, $element := .Views}} 11 | 12 | // -------------------------------------------------------------------------------- 13 | // - View {{len $element}} 14 | // -------------------------------------------------------------------------------- 15 | 16 | // Represents a view of data in a specific world. Provides access to the components specified in the generic block 17 | type View{{len $element}}[{{join $element ","}} any] struct { 18 | world *World 19 | filter filterList 20 | {{range $ii, $arg := $element}} 21 | storage{{$arg}} *componentStorage[{{$arg}}]{{end}} 22 | } 23 | 24 | // implement the initializer interface so that it can be automatically created and injected into systems 25 | func (v *View{{len $element}}[{{join $element ","}}]) Initialize(world *World) any { 26 | // TODO: filters need to be a part of the query type 27 | return Query{{len $element}}[{{join $element ","}}](world) 28 | } 29 | 30 | 31 | // Creates a View for the specified world with the specified component filters. 32 | func Query{{len $element}}[{{join $element ","}} any](world *World, filters ...Filter) *View{{len $element}}[{{join $element ","}}] { 33 | {{range $ii, $arg := $element}} 34 | storage{{$arg}} := getStorage[{{$arg}}](world.engine){{end}} 35 | 36 | {{range $ii, $arg := $element}} 37 | var {{$arg}}{{$arg}} {{$arg}}{{end}} 38 | 39 | comps := []CompId{ 40 | {{range $ii, $arg := $element}} 41 | name({{$arg}}{{$arg}}),{{end}} 42 | 43 | } 44 | filterList := newFilterList(comps, filters...) 45 | filterList.regenerate(world) 46 | 47 | v := &View{{len $element}}[{{join $element ","}}]{ 48 | world: world, 49 | filter: filterList, 50 | {{range $ii, $arg := $element}} 51 | storage{{$arg}}: storage{{$arg}},{{end}} 52 | } 53 | return v 54 | } 55 | 56 | // Reads a pointer to the underlying component at the specified id. 57 | // Read will return even if the specified id doesn't match the filter list 58 | // Read will return the value if it exists, else returns nil. 59 | // If you execute any ecs.Write(...) or ecs.Delete(...) this pointer may become invalid. 60 | func (v *View{{len $element}}[{{join $element ","}}]) Read(id Id) (*{{join $element ",*"}}) { 61 | if id == InvalidEntity { 62 | return {{with len $element}}{{nils .}}{{end}} 63 | } 64 | 65 | loc, ok := v.world.arch.Get(id) 66 | if !ok { 67 | return {{with len $element}}{{nils .}}{{end}} 68 | } 69 | lookup := v.world.engine.lookup[loc.archId] 70 | if lookup == nil { 71 | panic("LookupList is missing!") 72 | } 73 | index := int(loc.index) 74 | 75 | {{range $ii, $arg := $element}} 76 | var ret{{$arg}} *{{$arg}}{{end}} 77 | 78 | {{range $ii, $arg := $element}} slice{{$arg}}, ok := v.storage{{$arg}}.slice.Get(loc.archId) 79 | if ok { 80 | ret{{$arg}} = &slice{{$arg}}.comp[index] 81 | } 82 | {{end}} 83 | 84 | return {{retlist $element}} 85 | } 86 | 87 | // Counts the number of entities that match this query 88 | func (v *View{{len $element}}[{{join $element ","}}]) Count() int { 89 | v.filter.regenerate(v.world) 90 | 91 | total := 0 92 | for _, archId := range v.filter.archIds { 93 | lookup := v.world.engine.lookup[archId] 94 | if lookup == nil { panic("LookupList is missing!") } 95 | 96 | total += lookup.Len() 97 | } 98 | return total 99 | } 100 | 101 | // Maps the lambda function across every entity which matched the specified filters. 102 | func (v *View{{len $element}}[{{join $element ","}}]) MapId(lambda func(id Id, {{lambdaArgs $element}})) { 103 | v.filter.regenerate(v.world) 104 | 105 | {{range $ii, $arg := $element}} 106 | var slice{{$arg}} *componentList[{{$arg}}] 107 | var comp{{$arg}} []{{$arg}} 108 | var ret{{$arg}} *{{$arg}} 109 | {{end}} 110 | 111 | for _, archId := range v.filter.archIds { 112 | {{range $ii, $arg := $element}} 113 | slice{{$arg}}, _ = v.storage{{$arg}}.slice.Get(archId){{end}} 114 | 115 | lookup := v.world.engine.lookup[archId] 116 | if lookup == nil { panic("LookupList is missing!") } 117 | // lookup, ok := v.world.engine.lookup[archId] 118 | // if !ok { panic("LookupList is missing!") } 119 | ids := lookup.id 120 | 121 | 122 | // TODO - this flattened version causes a mild performance hit. But the other one combinatorially explodes. I also cant get BCE to work with it. See option 2 for higher performance. 123 | {{range $ii, $arg := $element}} 124 | comp{{$arg}} = nil 125 | if slice{{$arg}} != nil { 126 | comp{{$arg}} = slice{{$arg}}.comp 127 | }{{end}} 128 | 129 | {{range $ii, $arg := $element}} 130 | ret{{$arg}} = nil{{end}} 131 | for idx := range ids { 132 | if ids[idx] == InvalidEntity { continue } // Skip if its a hole 133 | {{range $ii, $arg := $element}} 134 | if comp{{$arg}} != nil { ret{{$arg}} = &comp{{$arg}}[idx] }{{end}} 135 | lambda(ids[idx], {{retlist $element}}) 136 | } 137 | } 138 | } 139 | 140 | // Maps the lambda function across every entity which matched the specified filters. Components are split based on the number of OS threads available. 141 | func (v *View{{len $element}}[{{join $element ","}}]) MapIdParallel(lambda func(id Id, {{lambdaArgs $element}})) { 142 | v.filter.regenerate(v.world) 143 | 144 | {{range $ii, $arg := $element}} 145 | var slice{{$arg}} *componentList[{{$arg}}] 146 | {{end}} 147 | 148 | // 1. Calculate work 149 | // 2. Calculate number of threads to execute with 150 | // 3. Greedy divide work among N threads 151 | // 4. Execute for each in its own goroutine 152 | 153 | // 1. Calculate work 154 | totalWork := 0 155 | for _, archId := range v.filter.archIds { 156 | lookup := v.world.engine.lookup[archId] 157 | if lookup == nil { 158 | panic("LookupList is missing!") 159 | } 160 | 161 | // Each id represents an entity that holds the requested component(s) 162 | // Each hole represents a deleted entity that used to hold the requested component(s) 163 | totalWork += len(lookup.id) // - len(lookup.holes) 164 | } 165 | 166 | // Nothing to do if there is no work 167 | if totalWork == 0 { return } 168 | 169 | // 2. Calculate number of threads to execute with 170 | numThreads := runtime.NumCPU() 171 | 172 | // Ensure that the number of threads we plan to use is <= total amount of work 173 | numThreads = min(totalWork, numThreads) 174 | 175 | var waitGroup sync.WaitGroup 176 | 177 | type workItem struct{ 178 | ids []Id 179 | {{range $ii, $arg := $element}} 180 | comp{{$arg}} []{{$arg}} 181 | {{end}} 182 | } 183 | workChannel := make(chan workItem) 184 | 185 | for i := 0; i < numThreads; i++ { 186 | waitGroup.Add(1) 187 | go func() { 188 | defer waitGroup.Done() 189 | 190 | for { 191 | work, ok := <-workChannel 192 | if !ok { 193 | return 194 | } 195 | 196 | {{range $ii, $arg := $element}} 197 | var ret{{$arg}} *{{$arg}}{{end}} 198 | for idx := range work.ids { 199 | if work.ids[idx] == InvalidEntity { continue } // Skip if its a hole 200 | {{range $ii, $arg := $element}} 201 | if work.comp{{$arg}} != nil { ret{{$arg}} = &work.comp{{$arg}}[idx] }{{end}} 202 | lambda(work.ids[idx], {{retlist $element}}) 203 | } 204 | } 205 | }() 206 | } 207 | 208 | // 3. Greedy divide work among N threads 209 | // Simple algorithm: 210 | // a. Find an evenly balanced distribution per thread 211 | // b. Generate all work until it gets consumed 212 | workPerThread := totalWork / numThreads 213 | 214 | // Generate 215 | {{range $ii, $arg := $element}} 216 | var comp{{$arg}} []{{$arg}} 217 | {{end}} 218 | for _, archId := range v.filter.archIds { 219 | lookup := v.world.engine.lookup[archId] 220 | if lookup == nil { 221 | panic("LookupList is missing!") 222 | } 223 | ids := lookup.id 224 | 225 | {{range $ii, $arg := $element}} 226 | slice{{$arg}}, _ = v.storage{{$arg}}.slice.Get(archId){{end}} 227 | 228 | {{range $ii, $arg := $element}} 229 | comp{{$arg}} = nil 230 | if slice{{$arg}} != nil { 231 | comp{{$arg}} = slice{{$arg}}.comp 232 | }{{end}} 233 | 234 | // workPerformed := 0 235 | 236 | start := 0 237 | end := 0 238 | numWorkItems := (len(ids) / workPerThread) + 1 239 | actualWorkPerThread := (len(ids) / numWorkItems) + 1 240 | for i := 0; i < numWorkItems; i++ { 241 | start = i * actualWorkPerThread 242 | end = (i+1) * actualWorkPerThread 243 | if end > len(ids) { 244 | end = len(ids) 245 | } 246 | 247 | // workPerformed += len(ids[start:end]) 248 | workChannel <- workItem{ 249 | ids: ids[start:end], 250 | {{range $ii, $arg := $element}} 251 | comp{{$arg}}: comp{{$arg}}[start:end], 252 | {{end}} 253 | } 254 | } 255 | 256 | // if workPerformed != len(ids) { 257 | // panic("wrong") 258 | // } 259 | } 260 | 261 | close(workChannel) 262 | waitGroup.Wait() 263 | } 264 | 265 | // Deprecated: This API is a tentative alternative way to map 266 | func (v *View{{len $element}}[{{join $element ","}}]) MapSlices(lambda func(id []Id, {{sliceLambdaArgs $element}})) { 267 | v.filter.regenerate(v.world) 268 | 269 | id := make([][]Id, 0) 270 | 271 | {{range $ii, $arg := $element}} 272 | sliceList{{$arg}} := make([][]{{$arg}}, 0){{end}} 273 | 274 | for _, archId := range v.filter.archIds { 275 | {{range $ii, $arg := $element}} 276 | slice{{$arg}}, ok := v.storage{{$arg}}.slice.Get(archId) 277 | if !ok { continue }{{end}} 278 | 279 | lookup := v.world.engine.lookup[archId] 280 | if lookup == nil { panic("LookupList is missing!") } 281 | // lookup, ok := v.world.engine.lookup[archId] 282 | // if !ok { panic("LookupList is missing!") } 283 | 284 | id = append(id, lookup.id) 285 | {{range $ii, $arg := $element}} 286 | sliceList{{$arg}} = append(sliceList{{$arg}}, slice{{$arg}}.comp){{end}} 287 | } 288 | 289 | for idx := range id { 290 | lambda(id[idx], 291 | {{range $ii, $arg := $element}}sliceList{{$arg}}[idx],{{end}} 292 | ) 293 | } 294 | } 295 | {{end}} 296 | -------------------------------------------------------------------------------- /internal/intmap/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Brent Pedersen - Bioinformatics 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /internal/intmap/README.md: -------------------------------------------------------------------------------- 1 | ## Notes 2 | This is adapted from: github.com/kamstrup/intmap for my usecase 3 | 4 | # Original 5 | Fast hashmap with integer keys for Golang 6 | 7 | [![GoDoc](https://godoc.org/github.com/kamstrup/intmap?status.svg)](https://godoc.org/github.com/kamstrup/intmap) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/kamstrup/intmap)](https://goreportcard.com/report/github.com/kamstrup/intmap) 9 | 10 | # intmap 11 | 12 | import "github.com/kamstrup/intmap" 13 | 14 | Package intmap is a fast hashmap implementation for Golang, specialized for maps with integer type keys. 15 | The values can be of any type. 16 | 17 | It is a full port of https://github.com/brentp/intintmap to use type parameters (aka generics). 18 | 19 | It interleaves keys and values in the same underlying array to improve locality. 20 | This is also known as open addressing with linear probing. 21 | 22 | It is up to 3X faster than the builtin map: 23 | ``` 24 | name time/op 25 | Map64Fill-8 201ms ± 5% 26 | IntIntMapFill-8 207ms ±31% 27 | StdMapFill-8 371ms ±11% 28 | Map64Get10PercentHitRate-8 148µs ±40% 29 | IntIntMapGet10PercentHitRate-8 171µs ±50% 30 | StdMapGet10PercentHitRate-8 171µs ±33% 31 | Map64Get100PercentHitRate-8 4.50ms ± 5% 32 | IntIntMapGet100PercentHitRate-8 4.82ms ± 6% 33 | StdMapGet100PercentHitRate-8 15.5ms ±32% 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```go 39 | m := intmap.New[int64,int64](32768) 40 | m.Put(int64(1234), int64(-222)) 41 | m.Put(int64(123), int64(33)) 42 | 43 | v, ok := m.Get(int64(222)) 44 | v, ok := m.Get(int64(333)) 45 | 46 | m.Del(int64(222)) 47 | m.Del(int64(333)) 48 | 49 | fmt.Println(m.Len()) 50 | 51 | m.ForEach(func(k int64, v int64) { 52 | fmt.Printf("key: %d, value: %d\n", k, v) 53 | }) 54 | 55 | m.Clear() // all gone, but buffers kept 56 | ``` 57 | -------------------------------------------------------------------------------- /internal/intmap/intintmap_test.go: -------------------------------------------------------------------------------- 1 | package intmap 2 | 3 | // 4 | // This file contains the old intintmap_test.go code from https://github.com/brentp/intintmap, 5 | // ported verbatim to new Map code with parametrized types 6 | // 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | func TestMapSimple(t *testing.T) { 13 | m := New[int64, int64](10) 14 | var i int64 15 | var v int64 16 | var ok bool 17 | 18 | // -------------------------------------------------------------------- 19 | // Put() and Get() 20 | 21 | for i = 0; i < 20000; i += 2 { 22 | m.Put(i, i) 23 | } 24 | for i = 0; i < 20000; i += 2 { 25 | if v, ok = m.Get(i); !ok || v != i { 26 | t.Errorf("didn't get expected value") 27 | } 28 | if _, ok = m.Get(i + 1); ok { 29 | t.Errorf("didn't get expected 'not found' flag") 30 | } 31 | } 32 | 33 | if m.Len() != int(20000/2) { 34 | t.Errorf("size (%d) is not right, should be %d", m.Len(), int(20000/2)) 35 | } 36 | 37 | // -------------------------------------------------------------------- 38 | // Keys() 39 | 40 | m0 := make(map[int64]int64, 1000) 41 | for i = 0; i < 20000; i += 2 { 42 | m0[i] = i 43 | } 44 | n := len(m0) 45 | 46 | m.ForEach(func(k int64, v int64) { 47 | m0[k] = -k 48 | }) 49 | 50 | if n != len(m0) { 51 | t.Errorf("get unexpected more keys") 52 | } 53 | 54 | for k, v := range m0 { 55 | if k != -v { 56 | t.Errorf("didn't get expected changed value") 57 | } 58 | } 59 | 60 | // -------------------------------------------------------------------- 61 | // Items() 62 | 63 | m0 = make(map[int64]int64, 1000) 64 | for i = 0; i < 20000; i += 2 { 65 | m0[i] = i 66 | } 67 | n = len(m0) 68 | 69 | m.ForEach(func(k int64, v int64) { 70 | m0[k] = -v 71 | if k != v { 72 | t.Errorf("didn't get expected key-value pair") 73 | } 74 | }) 75 | 76 | if n != len(m0) { 77 | t.Errorf("get unexpected more keys") 78 | } 79 | 80 | for k, v := range m0 { 81 | if k != -v { 82 | t.Errorf("didn't get expected changed value") 83 | } 84 | } 85 | 86 | // -------------------------------------------------------------------- 87 | // Del() 88 | 89 | for i = 0; i < 20000; i += 2 { 90 | m.Del(i) 91 | } 92 | for i = 0; i < 20000; i += 2 { 93 | if _, ok = m.Get(i); ok { 94 | t.Errorf("didn't get expected 'not found' flag") 95 | } 96 | if _, ok = m.Get(i + 1); ok { 97 | t.Errorf("didn't get expected 'not found' flag") 98 | } 99 | } 100 | 101 | // -------------------------------------------------------------------- 102 | // Put() and Get() 103 | 104 | for i = 0; i < 20000; i += 2 { 105 | m.Put(i, i*2) 106 | } 107 | for i = 0; i < 20000; i += 2 { 108 | if v, ok = m.Get(i); !ok || v != i*2 { 109 | t.Errorf("didn't get expected value") 110 | } 111 | if _, ok = m.Get(i + 1); ok { 112 | t.Errorf("didn't get expected 'not found' flag") 113 | } 114 | } 115 | 116 | } 117 | 118 | func TestMap(t *testing.T) { 119 | m := New[int64, int64](10) 120 | var ok bool 121 | var v int64 122 | 123 | step := int64(61) 124 | 125 | var i int64 126 | m.Put(0, 12345) 127 | for i = 1; i < 100000000; i += step { 128 | m.Put(i, i+7) 129 | m.Put(-i, i-7) 130 | 131 | if v, ok = m.Get(i); !ok || v != i+7 { 132 | t.Errorf("expected %d as value for key %d, got %d", i+7, i, v) 133 | } 134 | if v, ok = m.Get(-i); !ok || v != i-7 { 135 | t.Errorf("expected %d as value for key %d, got %d", i-7, -i, v) 136 | } 137 | } 138 | for i = 1; i < 100000000; i += step { 139 | if v, ok = m.Get(i); !ok || v != i+7 { 140 | t.Errorf("expected %d as value for key %d, got %d", i+7, i, v) 141 | } 142 | if v, ok = m.Get(-i); !ok || v != i-7 { 143 | t.Errorf("expected %d as value for key %d, got %d", i-7, -i, v) 144 | } 145 | 146 | for j := i + 1; j < i+step; j++ { 147 | if v, ok = m.Get(j); ok { 148 | t.Errorf("expected 'not found' flag for %d, found %d", j, v) 149 | } 150 | } 151 | } 152 | 153 | if v, ok = m.Get(0); !ok || v != 12345 { 154 | t.Errorf("expected 12345 for key 0") 155 | } 156 | } 157 | 158 | const MAX = 999999999 159 | const STEP = 9534 160 | 161 | func fillMap64(m *Map[int64, int64]) { 162 | var j int64 163 | for j = 0; j < MAX; j += STEP { 164 | m.Put(j, -j) 165 | for k := j; k < j+16; k++ { 166 | m.Put(k, -k) 167 | } 168 | 169 | } 170 | } 171 | 172 | func fillStdMap(m map[int64]int64) { 173 | var j int64 174 | for j = 0; j < MAX; j += STEP { 175 | m[j] = -j 176 | for k := j; k < j+16; k++ { 177 | m[k] = -k 178 | } 179 | } 180 | } 181 | 182 | func BenchmarkMap64Fill(b *testing.B) { 183 | for i := 0; i < b.N; i++ { 184 | m := New[int64, int64](2048) 185 | fillMap64(m) 186 | } 187 | } 188 | 189 | func BenchmarkStdMapFill(b *testing.B) { 190 | for i := 0; i < b.N; i++ { 191 | m := make(map[int64]int64, 2048) 192 | fillStdMap(m) 193 | } 194 | } 195 | 196 | func BenchmarkMap64Get10PercentHitRate(b *testing.B) { 197 | var j, k, v, sum int64 198 | var ok bool 199 | m := New[int64, int64](2048) 200 | fillMap64(m) 201 | for i := 0; i < b.N; i++ { 202 | sum = int64(0) 203 | for j = 0; j < MAX; j += STEP { 204 | for k = j; k < 10; k++ { 205 | if v, ok = m.Get(k); ok { 206 | sum += v 207 | } 208 | } 209 | } 210 | // log.Println("int int sum:", sum) 211 | } 212 | } 213 | 214 | func BenchmarkStdMapGet10PercentHitRate(b *testing.B) { 215 | var j, k, v, sum int64 216 | var ok bool 217 | m := make(map[int64]int64, 2048) 218 | fillStdMap(m) 219 | for i := 0; i < b.N; i++ { 220 | sum = int64(0) 221 | for j = 0; j < MAX; j += STEP { 222 | for k = j; k < 10; k++ { 223 | if v, ok = m[k]; ok { 224 | sum += v 225 | } 226 | } 227 | } 228 | // log.Println("map sum:", sum) 229 | } 230 | } 231 | 232 | func BenchmarkMap64Get100PercentHitRate(b *testing.B) { 233 | var j, v, sum int64 234 | var ok bool 235 | m := New[int64, int64](2048) 236 | fillMap64(m) 237 | for i := 0; i < b.N; i++ { 238 | sum = int64(0) 239 | for j = 0; j < MAX; j += STEP { 240 | if v, ok = m.Get(j); ok { 241 | sum += v 242 | } 243 | } 244 | // log.Println("int int sum:", sum) 245 | } 246 | } 247 | 248 | func BenchmarkStdMapGet100PercentHitRate(b *testing.B) { 249 | var j, v, sum int64 250 | var ok bool 251 | m := make(map[int64]int64, 2048) 252 | fillStdMap(m) 253 | for i := 0; i < b.N; i++ { 254 | sum = int64(0) 255 | for j = 0; j < MAX; j += STEP { 256 | if v, ok = m[j]; ok { 257 | sum += v 258 | } 259 | } 260 | // log.Println("map sum:", sum) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /internal/intmap/map64.go: -------------------------------------------------------------------------------- 1 | // Package intmap contains a fast hashmap implementation for maps with keys of any integer type 2 | package intmap 3 | 4 | import ( 5 | "math" 6 | ) 7 | 8 | // IntKey is a type constraint for values that can be used as keys in Map 9 | type IntKey interface { 10 | ~int | ~uint | ~int64 | ~uint64 | ~int32 | ~uint32 | ~int16 | ~uint16 | ~int8 | ~uint8 | ~uintptr 11 | } 12 | 13 | type pair[K IntKey, V any] struct { 14 | K K 15 | V V 16 | } 17 | 18 | const fillFactor64 = 0.7 19 | 20 | // Hashing Reference: https://gist.github.com/badboy/6267743 21 | func phiMix64(x int) int { 22 | // Note: With this, we are only just a bit faster than swissmap 23 | // h := x * int(0x9E3779B9) 24 | // h := x * 0x9E3779B9 25 | h := x * (-1_640_531_527) // This is just the int32 version of the 0x9E3779B9 26 | return h ^ (h >> 16) 27 | 28 | // TODO: track collision counts and compare before enabling this 29 | // // Theory: Because ecs.Id is just incremented by 1 each time, it might be effective to just always take the next slot 30 | // return x + x 31 | } 32 | 33 | // Map is a hashmap where the keys are some any integer type. 34 | type Map[K IntKey, V any] struct { 35 | data []pair[K, V] // key-value pairs 36 | size int 37 | 38 | zeroVal V // value of 'zero' key 39 | hasZeroKey bool // do we have 'zero' key in the map? 40 | } 41 | 42 | // New creates a new map with keys being any integer subtype. 43 | // The map can store up to the given capacity before reallocation and rehashing occurs. 44 | func New[K IntKey, V any](capacity int) *Map[K, V] { 45 | return &Map[K, V]{ 46 | data: make([]pair[K, V], arraySize(capacity, fillFactor64)), 47 | } 48 | } 49 | 50 | // Get returns the value if the key is found. 51 | func (m *Map[K, V]) Get(key K) (V, bool) { 52 | if key == K(0) { 53 | if m.hasZeroKey { 54 | return m.zeroVal, true 55 | } 56 | var zero V 57 | return zero, false 58 | } 59 | 60 | idx := m.startIndex(key) 61 | p := m.data[idx] 62 | 63 | if p.K == K(0) { // end of chain already 64 | var zero V 65 | return zero, false 66 | } 67 | if p.K == key { // we check zero prior to this call 68 | return p.V, true 69 | } 70 | 71 | // hash collision, seek next hash match, bailing on first empty 72 | for { 73 | idx = m.nextIndex(idx) 74 | p = m.data[idx] 75 | if p.K == K(0) { 76 | var zero V 77 | return zero, false 78 | } 79 | if p.K == key { 80 | return p.V, true 81 | } 82 | } 83 | } 84 | 85 | // Put adds or updates key with value val. 86 | func (m *Map[K, V]) Put(key K, val V) { 87 | if key == K(0) { 88 | if !m.hasZeroKey { 89 | m.size++ 90 | } 91 | m.zeroVal = val 92 | m.hasZeroKey = true 93 | return 94 | } 95 | 96 | idx := m.startIndex(key) 97 | p := &m.data[idx] 98 | 99 | if p.K == K(0) { // end of chain already 100 | p.K = key 101 | p.V = val 102 | if m.size >= m.sizeThreshold() { 103 | m.rehash() 104 | } else { 105 | m.size++ 106 | } 107 | return 108 | } else if p.K == key { // overwrite existing value 109 | p.V = val 110 | return 111 | } 112 | 113 | // hash collision, seek next empty or key match 114 | for { 115 | idx = m.nextIndex(idx) 116 | p = &m.data[idx] 117 | 118 | if p.K == K(0) { 119 | p.K = key 120 | p.V = val 121 | if m.size >= m.sizeThreshold() { 122 | m.rehash() 123 | } else { 124 | m.size++ 125 | } 126 | return 127 | } else if p.K == key { 128 | p.V = val 129 | return 130 | } 131 | } 132 | } 133 | 134 | func (m *Map[K, V]) ForEach(f func(K, V)) { 135 | if m.hasZeroKey { 136 | f(K(0), m.zeroVal) 137 | } 138 | forEach64(m.data, f) 139 | } 140 | 141 | // Clear removes all items from the map, but keeps the internal buffers for reuse. 142 | func (m *Map[K, V]) Clear() { 143 | var zero V 144 | m.hasZeroKey = false 145 | m.zeroVal = zero 146 | 147 | // compiles down to runtime.memclr() 148 | for i := range m.data { 149 | m.data[i] = pair[K, V]{} 150 | } 151 | 152 | m.size = 0 153 | } 154 | 155 | func (m *Map[K, V]) rehash() { 156 | oldData := m.data 157 | m.data = make([]pair[K, V], 2*len(m.data)) 158 | 159 | // reset size 160 | if m.hasZeroKey { 161 | m.size = 1 162 | } else { 163 | m.size = 0 164 | } 165 | 166 | forEach64(oldData, m.Put) 167 | } 168 | 169 | // Len returns the number of elements in the map. 170 | func (m *Map[K, V]) Len() int { 171 | return m.size 172 | } 173 | 174 | func (m *Map[K, V]) sizeThreshold() int { 175 | return int(math.Floor(float64(len(m.data)) * fillFactor64)) 176 | } 177 | 178 | func (m *Map[K, V]) startIndex(key K) int { 179 | return phiMix64(int(key)) & (len(m.data) - 1) 180 | } 181 | 182 | func (m *Map[K, V]) nextIndex(idx int) int { 183 | return (idx + 1) & (len(m.data) - 1) 184 | } 185 | 186 | func forEach64[K IntKey, V any](pairs []pair[K, V], f func(k K, v V)) { 187 | for _, p := range pairs { 188 | if p.K != K(0) { 189 | f(p.K, p.V) 190 | } 191 | } 192 | } 193 | 194 | // Del deletes a key and its value, returning true iff the key was found 195 | func (m *Map[K, V]) Del(key K) bool { 196 | if key == K(0) { 197 | if m.hasZeroKey { 198 | m.hasZeroKey = false 199 | m.size-- 200 | return true 201 | } 202 | return false 203 | } 204 | 205 | idx := m.startIndex(key) 206 | p := m.data[idx] 207 | 208 | if p.K == key { 209 | // any keys that were pushed back needs to be shifted nack into the empty slot 210 | // to avoid breaking the chain 211 | m.shiftKeys(idx) 212 | m.size-- 213 | return true 214 | } else if p.K == K(0) { // end of chain already 215 | return false 216 | } 217 | 218 | for { 219 | idx = m.nextIndex(idx) 220 | p = m.data[idx] 221 | 222 | if p.K == key { 223 | // any keys that were pushed back needs to be shifted nack into the empty slot 224 | // to avoid breaking the chain 225 | m.shiftKeys(idx) 226 | m.size-- 227 | return true 228 | } else if p.K == K(0) { 229 | return false 230 | } 231 | 232 | } 233 | } 234 | 235 | func (m *Map[K, V]) shiftKeys(idx int) int { 236 | // Shift entries with the same hash. 237 | // We need to do this on deletion to ensure we don't have zeroes in the hash chain 238 | for { 239 | var p pair[K, V] 240 | lastIdx := idx 241 | idx = m.nextIndex(idx) 242 | for { 243 | p = m.data[idx] 244 | if p.K == K(0) { 245 | m.data[lastIdx] = pair[K, V]{} 246 | return lastIdx 247 | } 248 | 249 | slot := m.startIndex(p.K) 250 | if lastIdx <= idx { 251 | if lastIdx >= slot || slot > idx { 252 | break 253 | } 254 | } else { 255 | if lastIdx >= slot && slot > idx { 256 | break 257 | } 258 | } 259 | idx = m.nextIndex(idx) 260 | } 261 | m.data[lastIdx] = p 262 | } 263 | } 264 | 265 | func nextPowerOf2(x uint32) uint32 { 266 | if x == math.MaxUint32 { 267 | return x 268 | } 269 | 270 | if x == 0 { 271 | return 1 272 | } 273 | 274 | x-- 275 | x |= x >> 1 276 | x |= x >> 2 277 | x |= x >> 4 278 | x |= x >> 8 279 | x |= x >> 16 280 | 281 | return x + 1 282 | } 283 | 284 | func arraySize(exp int, fill float64) int { 285 | s := nextPowerOf2(uint32(math.Ceil(float64(exp) / fill))) 286 | if s < 2 { 287 | s = 2 288 | } 289 | return int(s) 290 | } 291 | -------------------------------------------------------------------------------- /internal/intmap/map64_test.go: -------------------------------------------------------------------------------- 1 | package intmap 2 | 3 | import "testing" 4 | 5 | func TestMap64(t *testing.T) { 6 | type pairs [][2]int64 7 | cases := []struct { 8 | name string 9 | vals pairs 10 | }{ 11 | { 12 | name: "empty", 13 | }, 14 | { 15 | name: "one", 16 | vals: pairs{{1, 2}}, 17 | }, 18 | { 19 | name: "one_zero", 20 | vals: pairs{{0, 2}}, 21 | }, 22 | { 23 | name: "two", 24 | vals: pairs{{1, 2}, {3, 4}}, 25 | }, 26 | { 27 | name: "two_zero", 28 | vals: pairs{{1, 2}, {0, 4}}, 29 | }, 30 | { 31 | name: "ten", 32 | vals: pairs{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7}, {8, 8}, {9, 9}, {10, 10}}, 33 | }, 34 | { 35 | name: "ten_zero", 36 | vals: pairs{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 6}, {7, 7}, {8, 8}, {9, 9}, {10, 10}, {0, 11}}, 37 | }, 38 | } 39 | 40 | runTest := func(t *testing.T, m *Map[int64, int64], vals pairs) { 41 | for i, pair := range vals { 42 | m.Put(pair[0], pair[1]) 43 | if sz := m.Len(); sz != i+1 { 44 | t.Fatalf("unexpected size after %d put()s: %d", sz, i+1) 45 | } 46 | } 47 | for i, pair := range vals { 48 | val, ok := m.Get(pair[0]) 49 | if !ok { 50 | t.Fatalf("key number %d not found: %d", i, pair[0]) 51 | } 52 | if val != pair[1] { 53 | t.Fatalf("incorrect value %d for key %d, expected %d", pair[1], pair[0], val) 54 | } 55 | } 56 | } 57 | 58 | for _, tc := range cases { 59 | t.Run(tc.name, func(t *testing.T) { 60 | t.Run("zero_cap", func(t *testing.T) { 61 | m := New[int64, int64](0) 62 | runTest(t, m, tc.vals) 63 | }) 64 | t.Run("full_cap", func(t *testing.T) { 65 | m := New[int64, int64](len(tc.vals)) 66 | runTest(t, m, tc.vals) 67 | }) 68 | }) 69 | } 70 | } 71 | 72 | func TestMap64Delete(t *testing.T) { 73 | m := New[int, int](10) 74 | for i := 0; i < 100; i++ { 75 | m.Put(i, -i) 76 | } 77 | if sz := m.Len(); sz != 100 { 78 | t.Fatalf("expected %d elements in map: %d", 100, sz) 79 | } 80 | 81 | for i := 0; i < 100; i++ { 82 | if found := m.Del(i); !found { 83 | t.Fatalf("deleted key should have been there: %d", i) 84 | } 85 | if sz := m.Len(); sz != 100-i-1 { 86 | t.Fatalf("expected %d elements in map: %d", 100-i-1, sz) 87 | } 88 | if found := m.Del(i); found { 89 | t.Fatalf("deleted key should not be there: %d", i) 90 | } 91 | } 92 | 93 | if sz := m.Len(); sz != 0 { 94 | t.Fatalf("map not empty, %d elements remain", sz) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type list[T any] struct { 4 | list []T 5 | } 6 | 7 | func newList[T any]() list[T] { 8 | return list[T]{ 9 | list: make([]T, 0), 10 | } 11 | } 12 | func (l *list[T]) Add(t T) { 13 | l.list = append(l.list, t) 14 | } 15 | -------------------------------------------------------------------------------- /mask.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "fmt" 4 | 5 | // Note: you can increase max component size by increasing maxComponentId and archetypeMask 6 | // TODO: I should have some kind of panic if you go over maximum component size 7 | const numMaskBlocks = 4 8 | const maxComponentId = (numMaskBlocks * 64) - 1 // 4 maskBlocks = 255 components 9 | 10 | var blankArchMask archetypeMask 11 | 12 | // Supports maximum 256 unique component types 13 | type archetypeMask [numMaskBlocks]uint64 // TODO: can/should I make this configurable? 14 | func (a archetypeMask) String() string { 15 | return fmt.Sprintf("0x%x%x%x%x", a[0], a[1], a[2], a[3]) 16 | } 17 | 18 | func buildArchMask(comps ...Component) archetypeMask { 19 | var mask archetypeMask 20 | for _, comp := range comps { 21 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 22 | c := comp.CompId() 23 | idx := c / 64 24 | offset := c - (64 * idx) 25 | mask[idx] |= (1 << offset) 26 | } 27 | return mask 28 | } 29 | func buildArchMaskFromAny(comps ...any) archetypeMask { 30 | var mask archetypeMask 31 | for _, comp := range comps { 32 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 33 | c := name(comp) 34 | idx := c / 64 35 | offset := c - (64 * idx) 36 | mask[idx] |= (1 << offset) 37 | } 38 | return mask 39 | } 40 | func buildArchMaskFromId(compIds ...CompId) archetypeMask { 41 | var mask archetypeMask 42 | for _, c := range compIds { 43 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 44 | idx := c / 64 45 | offset := c - (64 * idx) 46 | mask[idx] |= (1 << offset) 47 | } 48 | return mask 49 | } 50 | 51 | func (m *archetypeMask) addComponent(compId CompId) { 52 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 53 | idx := compId / 64 54 | offset := compId - (64 * idx) 55 | m[idx] |= (1 << offset) 56 | } 57 | 58 | func (m *archetypeMask) removeComponent(compId CompId) { 59 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 60 | idx := compId / 64 61 | offset := compId - (64 * idx) 62 | m[idx] &= ^(1 << offset) 63 | } 64 | 65 | // Performs a bitwise OR on the base mask `m` with the added mask `a` 66 | func (m archetypeMask) bitwiseOr(a archetypeMask) archetypeMask { 67 | for i := range m { 68 | m[i] = m[i] | a[i] 69 | } 70 | return m 71 | } 72 | 73 | // Performs a bitwise AND on the base mask `m` with the added mask `a` 74 | func (m archetypeMask) bitwiseAnd(a archetypeMask) archetypeMask { 75 | for i := range m { 76 | m[i] = m[i] & a[i] 77 | } 78 | return m 79 | } 80 | 81 | // Clears every bit in m based on the bits set in 'c' 82 | func (m archetypeMask) bitwiseClear(c archetypeMask) archetypeMask { 83 | for i := range m { 84 | m[i] = m[i] & (^c[i]) 85 | } 86 | return m 87 | } 88 | 89 | // m: 0x1010 90 | // c: 0x1100 91 | //!c: 0x0011 92 | // f: 0x0010 93 | 94 | // Checks to ensure archetype m contains archetype a 95 | // Returns true if every bit in m is also set in a 96 | // Returns false if at least one set bit in m is not set in a 97 | func (m archetypeMask) contains(a archetypeMask) bool { 98 | // Logic: Bitwise AND on every segment, if the 'check' result doesn't match m[i] for that segment 99 | // then we know there was a bit in a[i] that was not set 100 | var check uint64 101 | for i := range m { 102 | check = m[i] & a[i] 103 | if check != m[i] { 104 | return false 105 | } 106 | } 107 | return true 108 | } 109 | 110 | // Checks to see if a mask m contains the supplied componentId 111 | // Returns true if the bit location in that mask is set, else returns false 112 | func (m archetypeMask) hasComponent(compId CompId) bool { 113 | // Ranges: [0, 64), [64, 128), [128, 192), [192, 256) 114 | idx := compId / 64 115 | offset := compId - (64 * idx) 116 | return (m[idx] & (1 << offset)) != 0 117 | } 118 | 119 | // Generates and returns a list of every componentId that this archetype contains 120 | func (m archetypeMask) getComponentList() []CompId { 121 | ret := make([]CompId, 0) 122 | for compId := CompId(0); compId <= maxComponentId; compId++ { 123 | if m.hasComponent(compId) { 124 | ret = append(ret, compId) 125 | } 126 | } 127 | return ret 128 | } 129 | -------------------------------------------------------------------------------- /mask_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "testing" 4 | 5 | func TestBitwiseClear(t *testing.T) { 6 | type testStruct struct { 7 | input archetypeMask 8 | clear archetypeMask 9 | expected archetypeMask 10 | } 11 | 12 | tests := []testStruct{ 13 | { 14 | input: buildArchMaskFromId(0, 1, 2, 3, 4), 15 | clear: buildArchMaskFromId(2, 3, 4), 16 | expected: buildArchMaskFromId(0, 1), 17 | }, 18 | { 19 | input: buildArchMaskFromId(0, 1), 20 | clear: buildArchMaskFromId(1), 21 | expected: buildArchMaskFromId(0), 22 | }, 23 | { 24 | input: buildArchMaskFromId(0, 1), 25 | clear: buildArchMaskFromId(0, 1), 26 | expected: buildArchMaskFromId(), 27 | }, 28 | { 29 | input: buildArchMaskFromId(0, 1), 30 | clear: buildArchMaskFromId(0, 1, 2), 31 | expected: buildArchMaskFromId(), 32 | }, 33 | { 34 | input: buildArchMaskFromId(0, 1), 35 | clear: buildArchMaskFromId(2), 36 | expected: buildArchMaskFromId(0, 1), 37 | }, 38 | } 39 | 40 | for _, test := range tests { 41 | got := test.input.bitwiseClear(test.clear) 42 | if got != test.expected { 43 | t.Errorf("error") 44 | } 45 | } 46 | } 47 | 48 | func TestBitwiseOr(t *testing.T) { 49 | type testStruct struct { 50 | input archetypeMask 51 | clear archetypeMask 52 | expected archetypeMask 53 | } 54 | 55 | tests := []testStruct{ 56 | { 57 | input: buildArchMaskFromId(0, 1, 2, 3, 4), 58 | clear: buildArchMaskFromId(2, 3, 4), 59 | expected: buildArchMaskFromId(0, 1, 2, 3, 4), 60 | }, 61 | { 62 | input: buildArchMaskFromId(0, 1), 63 | clear: buildArchMaskFromId(1), 64 | expected: buildArchMaskFromId(0, 1), 65 | }, 66 | { 67 | input: buildArchMaskFromId(0, 1), 68 | clear: buildArchMaskFromId(0, 1), 69 | expected: buildArchMaskFromId(0, 1), 70 | }, 71 | { 72 | input: buildArchMaskFromId(0, 1), 73 | clear: buildArchMaskFromId(0, 1, 2), 74 | expected: buildArchMaskFromId(0, 1, 2), 75 | }, 76 | { 77 | input: buildArchMaskFromId(0, 1), 78 | clear: buildArchMaskFromId(2), 79 | expected: buildArchMaskFromId(0, 1, 2), 80 | }, 81 | { 82 | input: buildArchMaskFromId(0, 1), 83 | clear: buildArchMaskFromId(2, 3), 84 | expected: buildArchMaskFromId(0, 1, 2, 3), 85 | }, 86 | { 87 | input: buildArchMaskFromId(), 88 | clear: buildArchMaskFromId(2, 3), 89 | expected: buildArchMaskFromId(2, 3), 90 | }, 91 | { 92 | input: buildArchMaskFromId(2, 3), 93 | clear: buildArchMaskFromId(), 94 | expected: buildArchMaskFromId(2, 3), 95 | }, 96 | { 97 | input: buildArchMaskFromId(), 98 | clear: buildArchMaskFromId(), 99 | expected: buildArchMaskFromId(), 100 | }, 101 | } 102 | 103 | for _, test := range tests { 104 | got := test.input.bitwiseOr(test.clear) 105 | if got != test.expected { 106 | t.Errorf("error") 107 | } 108 | } 109 | } 110 | 111 | func TestBitwiseAnd(t *testing.T) { 112 | type testStruct struct { 113 | input archetypeMask 114 | clear archetypeMask 115 | expected archetypeMask 116 | } 117 | 118 | tests := []testStruct{ 119 | { 120 | input: buildArchMaskFromId(0, 1, 2, 3, 4), 121 | clear: buildArchMaskFromId(2, 3, 4), 122 | expected: buildArchMaskFromId(2, 3, 4), 123 | }, 124 | { 125 | input: buildArchMaskFromId(0, 1), 126 | clear: buildArchMaskFromId(1), 127 | expected: buildArchMaskFromId(1), 128 | }, 129 | { 130 | input: buildArchMaskFromId(0, 1), 131 | clear: buildArchMaskFromId(0, 1), 132 | expected: buildArchMaskFromId(0, 1), 133 | }, 134 | { 135 | input: buildArchMaskFromId(0, 1), 136 | clear: buildArchMaskFromId(0, 1, 2), 137 | expected: buildArchMaskFromId(0, 1), 138 | }, 139 | { 140 | input: buildArchMaskFromId(0, 1), 141 | clear: buildArchMaskFromId(2), 142 | expected: buildArchMaskFromId(), 143 | }, 144 | { 145 | input: buildArchMaskFromId(0, 1), 146 | clear: buildArchMaskFromId(2, 3), 147 | expected: buildArchMaskFromId(), 148 | }, 149 | { 150 | input: buildArchMaskFromId(), 151 | clear: buildArchMaskFromId(2, 3), 152 | expected: buildArchMaskFromId(), 153 | }, 154 | { 155 | input: buildArchMaskFromId(2, 3), 156 | clear: buildArchMaskFromId(), 157 | expected: buildArchMaskFromId(), 158 | }, 159 | { 160 | input: buildArchMaskFromId(), 161 | clear: buildArchMaskFromId(), 162 | expected: buildArchMaskFromId(), 163 | }, 164 | { 165 | input: buildArchMaskFromId(0, 2, 4), 166 | clear: buildArchMaskFromId(1, 2, 3, 4), 167 | expected: buildArchMaskFromId(2, 4), 168 | }, 169 | } 170 | 171 | for _, test := range tests { 172 | got := test.input.bitwiseAnd(test.clear) 173 | if got != test.expected { 174 | t.Errorf("error") 175 | } 176 | } 177 | } 178 | 179 | func TestAddCompMask(t *testing.T) { 180 | type testStruct struct { 181 | input archetypeMask 182 | add CompId 183 | expected archetypeMask 184 | } 185 | 186 | tests := []testStruct{ 187 | { 188 | input: buildArchMaskFromId(0), 189 | add: 1, 190 | expected: buildArchMaskFromId(0, 1), 191 | }, 192 | { 193 | input: buildArchMaskFromId(0, 1), 194 | add: 3, 195 | expected: buildArchMaskFromId(0, 1, 3), 196 | }, 197 | { 198 | input: buildArchMaskFromId(0, 1), 199 | add: 63, 200 | expected: buildArchMaskFromId(0, 1, 63), 201 | }, 202 | { 203 | input: buildArchMaskFromId(0, 1), 204 | add: 1, 205 | expected: buildArchMaskFromId(0, 1), 206 | }, 207 | } 208 | 209 | for _, test := range tests { 210 | test.input.addComponent(test.add) 211 | got := test.input 212 | if got != test.expected { 213 | t.Errorf("error") 214 | } 215 | } 216 | } 217 | 218 | func TestRemoveCompMask(t *testing.T) { 219 | type testStruct struct { 220 | input archetypeMask 221 | remove CompId 222 | expected archetypeMask 223 | } 224 | 225 | tests := []testStruct{ 226 | { 227 | input: buildArchMaskFromId(0), 228 | remove: 1, 229 | expected: buildArchMaskFromId(0), 230 | }, 231 | { 232 | input: buildArchMaskFromId(0, 1), 233 | remove: 63, 234 | expected: buildArchMaskFromId(0, 1), 235 | }, 236 | { 237 | input: buildArchMaskFromId(0), 238 | remove: 0, 239 | expected: buildArchMaskFromId(), 240 | }, 241 | { 242 | input: buildArchMaskFromId(0, 1), 243 | remove: 1, 244 | expected: buildArchMaskFromId(0), 245 | }, 246 | { 247 | input: buildArchMaskFromId(0, 1), 248 | remove: 0, 249 | expected: buildArchMaskFromId(1), 250 | }, 251 | } 252 | 253 | for _, test := range tests { 254 | test.input.removeComponent(test.remove) 255 | got := test.input 256 | if got != test.expected { 257 | t.Errorf("error") 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /name.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | ) 8 | 9 | func nameTyped[T any](comp T) CompId { 10 | compId := name(comp) 11 | registerComponentStorage[T](compId) 12 | return compId 13 | } 14 | 15 | type storageBuilder interface { 16 | build() storage 17 | } 18 | type storageBuilderImp[T any] struct { 19 | } 20 | 21 | func (s storageBuilderImp[T]) build() storage { 22 | return &componentStorage[T]{ 23 | slice: newMap[archetypeId, *componentList[T]](DefaultAllocation), 24 | } 25 | } 26 | 27 | var componentStorageLookupMut sync.RWMutex 28 | var componentStorageLookup = make(map[CompId]storageBuilder) 29 | 30 | func registerComponentStorage[T any](compId CompId) { 31 | componentStorageLookupMut.Lock() 32 | _, ok := componentStorageLookup[compId] 33 | if !ok { 34 | componentStorageLookup[compId] = storageBuilderImp[T]{} 35 | } 36 | componentStorageLookupMut.Unlock() 37 | } 38 | 39 | func newComponentStorage(c CompId) storage { 40 | componentStorageLookupMut.RLock() 41 | s, ok := componentStorageLookup[c] 42 | if !ok { 43 | panic(fmt.Sprintf("tried to build component storage with unregistered componentId: %d", c)) 44 | } 45 | 46 | componentStorageLookupMut.RUnlock() 47 | return s.build() 48 | } 49 | 50 | //-------------------------------------------------------------------------------- 51 | 52 | var componentIdMutex sync.Mutex 53 | var registeredComponents = make(map[reflect.Type]CompId, maxComponentId) 54 | var invalidComponentId CompId = 0 55 | var componentRegistryCounter CompId = 1 56 | 57 | func name(t any) CompId { 58 | // Note: We have to lock here in case there are multiple worlds 59 | // TODO!! - This probably causes some performance penalty 60 | componentIdMutex.Lock() 61 | defer componentIdMutex.Unlock() 62 | 63 | typeof := reflect.TypeOf(t) 64 | compId, ok := registeredComponents[typeof] 65 | if !ok { 66 | compId = componentRegistryCounter 67 | registeredComponents[typeof] = compId 68 | componentRegistryCounter++ 69 | } 70 | return compId 71 | } 72 | 73 | // // Possible solution: Runs faster than reflection (mostly useful for potentially removing/reducing ecs.C(...) overhead 74 | // import ( 75 | // "sync" 76 | // "unsafe" 77 | // ) 78 | 79 | // type emptyInterface struct { 80 | // typ unsafe.Pointer 81 | // ptr unsafe.Pointer 82 | // } 83 | 84 | // var componentIdMutex sync.Mutex 85 | // var registeredComponents = make(map[uintptr]componentId, maxComponentId) 86 | // var invalidComponentId componentId = 0 87 | // var componentRegistryCounter componentId = 1 88 | 89 | // func name(t any) componentId { 90 | // // Note: We have to lock here in case there are multiple worlds 91 | // // TODO!! - This probably causes some performance penalty 92 | // componentIdMutex.Lock() 93 | // defer componentIdMutex.Unlock() 94 | 95 | // iface := (*emptyInterface)(unsafe.Pointer(&t)) 96 | // typeptr := uintptr(iface.typ) 97 | // compId, ok := registeredComponents[typeptr] 98 | // if !ok { 99 | // compId = componentRegistryCounter 100 | // registeredComponents[typeptr] = compId 101 | // componentRegistryCounter++ 102 | // } 103 | // return compId 104 | // } 105 | -------------------------------------------------------------------------------- /name_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "testing" 4 | 5 | // Reflect: BenchmarkName-12 21372271 56.14 ns/op 0 B/op 0 allocs/op 6 | // Reflect: BenchmarkName-12 21361663 56.60 ns/op 0 B/op 0 allocs/op 7 | // Unsafe: BenchmarkName-12 32242930 37.01 ns/op 0 B/op 0 allocs/op 8 | // Unsafe: BenchmarkName-12 31874323 36.90 ns/op 0 B/op 0 allocs/op 9 | func BenchmarkName(b *testing.B) { 10 | b.ResetTimer() 11 | 12 | for n := 0; n < b.N; n++ { 13 | C(position{}) 14 | C(velocity{}) 15 | C(acceleration{}) 16 | C(radius{}) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import "testing" 4 | 5 | func TestQuery(t *testing.T) { 6 | world := NewWorld() 7 | ids := make([]Id, 0) 8 | for i := 0; i < 1e6; i++ { 9 | id := world.NewId() 10 | v := float64(id) 11 | pos := position{v, v, v} 12 | vel := velocity{v, v, v} 13 | Write(world, id, pos, vel) 14 | ids = append(ids, id) 15 | } 16 | 17 | m := make(map[Id]struct{}) 18 | query := Query1[position](world) 19 | query.MapId(func(id Id, pos *position) { 20 | compare(t, *pos, position{float64(id), float64(id), float64(id)}) 21 | m[id] = struct{}{} 22 | }) 23 | 24 | for _, id := range ids { 25 | _, ok := m[id] 26 | check(t, ok) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | type storage interface { 4 | ReadToEntity(*Entity, archetypeId, int) bool 5 | ReadToRawEntity(*RawEntity, archetypeId, int) bool 6 | Allocate(archetypeId, int) // Allocates the index, setting the data there to the zero value 7 | Delete(archetypeId, int) 8 | moveArchetype(entLoc, entLoc) // From -> To 9 | } 10 | 11 | // -------------------------------------------------------------------------------- 12 | // - Lookup List 13 | // -------------------------------------------------------------------------------- 14 | // TODO: Rename, this is kind of like an archetype header 15 | type lookupList struct { 16 | id []Id // An array of every id in the arch list (essentially a reverse mapping from index to Id) 17 | holes []int // List of indexes that have ben deleted 18 | mask archetypeMask 19 | components []CompId // This is a list of all components that this archetype contains 20 | } 21 | 22 | func (l *lookupList) Len() int { 23 | return len(l.id) - len(l.holes) 24 | } 25 | 26 | // Adds ourselves to the last available hole, else appends 27 | // Returns the index 28 | func (l *lookupList) addToEasiestHole(id Id) int { 29 | if len(l.holes) > 0 { 30 | lastHoleIndex := len(l.holes) - 1 31 | index := l.holes[lastHoleIndex] 32 | l.id[index] = id 33 | 34 | l.holes = l.holes[:lastHoleIndex] 35 | return index 36 | } else { 37 | // Because the Id hasn't been added to this arch, we need to append it to the end 38 | l.id = append(l.id, id) 39 | index := len(l.id) - 1 40 | return index 41 | } 42 | } 43 | 44 | // -------------------------------------------------------------------------------- 45 | // - ComponentSlice 46 | // -------------------------------------------------------------------------------- 47 | type componentList[T any] struct { 48 | comp []T 49 | } 50 | 51 | // Note: This will panic if you write past the buffer by more than 1 52 | func (s *componentList[T]) Write(index int, val T) { 53 | if index == len(s.comp) { 54 | // Case: index causes a single append (new element added) 55 | s.comp = append(s.comp, val) 56 | } else { 57 | // Case: index is inside the length 58 | // Edge: (Causes Panic): Index is greater than 1 plus length 59 | s.comp[index] = val 60 | } 61 | } 62 | 63 | // -------------------------------------------------------------------------------- 64 | // - ComponentSliceStorage 65 | // -------------------------------------------------------------------------------- 66 | type componentStorage[T any] struct { 67 | // TODO: Could these just increment rather than be a map lookup? I guess not every component type would have a storage slice for every archetype so we'd waste some memory. I guess at the very least we could use the faster lookup map 68 | slice *internalMap[archetypeId, *componentList[T]] 69 | } 70 | 71 | func (ss *componentStorage[T]) ReadToEntity(entity *Entity, archId archetypeId, index int) bool { 72 | cSlice, ok := ss.slice.Get(archId) 73 | if !ok { 74 | return false 75 | } 76 | entity.Add(C(cSlice.comp[index])) 77 | return true 78 | } 79 | 80 | func (ss *componentStorage[T]) ReadToRawEntity(entity *RawEntity, archId archetypeId, index int) bool { 81 | cSlice, ok := ss.slice.Get(archId) 82 | if !ok { 83 | return false 84 | } 85 | entity.Add(&cSlice.comp[index]) 86 | return true 87 | } 88 | 89 | func (ss *componentStorage[T]) GetSlice(archId archetypeId) *componentList[T] { 90 | list, ok := ss.slice.Get(archId) 91 | if !ok { 92 | list = &componentList[T]{ 93 | comp: make([]T, 0, DefaultAllocation), 94 | } 95 | ss.slice.Put(archId, list) 96 | } 97 | return list 98 | } 99 | 100 | func (ss *componentStorage[T]) Allocate(archId archetypeId, index int) { 101 | cSlice := ss.GetSlice(archId) 102 | 103 | var val T 104 | cSlice.Write(index, val) 105 | } 106 | 107 | func (ss *componentStorage[T]) moveArchetype(oldLoc, newLoc entLoc) { 108 | oldSlice, _ := ss.slice.Get(oldLoc.archId) 109 | newSlice, _ := ss.slice.Get(newLoc.archId) 110 | 111 | val := oldSlice.comp[oldLoc.index] 112 | newSlice.Write(int(newLoc.index), val) 113 | } 114 | 115 | // Delete is somewhat special because it deletes the index of the archId for the componentSlice 116 | // but then plugs the hole by pushing the last element of the componentSlice into index 117 | func (ss *componentStorage[T]) Delete(archId archetypeId, index int) { 118 | cSlice, ok := ss.slice.Get(archId) 119 | if !ok { 120 | return 121 | } 122 | 123 | lastVal := cSlice.comp[len(cSlice.comp)-1] 124 | cSlice.comp[index] = lastVal 125 | cSlice.comp = cSlice.comp[:len(cSlice.comp)-1] 126 | } 127 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | "runtime" 9 | ) 10 | 11 | // Represents an individual system 12 | type System struct { 13 | Name string 14 | Func func(dt time.Duration) 15 | } 16 | 17 | func (s System) Build(world *World) System { 18 | return s 19 | } 20 | 21 | // Create a new system. The system name will be automatically created based on the function name that calls this function 22 | func NewSystem(lambda func(dt time.Duration)) System { 23 | systemName := "UnknownSystemName" 24 | 25 | pc, _, _, ok := runtime.Caller(1) 26 | if ok { 27 | details := runtime.FuncForPC(pc) 28 | systemName = details.Name() 29 | } 30 | 31 | return System{ 32 | Name: systemName, 33 | Func: lambda, 34 | } 35 | } 36 | 37 | // Executes the system once, returning the time taken. 38 | // This is mostly used by the scheduler, but you can use it too. 39 | func (s *System) step(dt time.Duration) { 40 | // Note: Disable timing 41 | s.Func(dt) 42 | // return 0 43 | 44 | // fmt.Println(s.Name) // Spew 45 | 46 | // start := time.Now() 47 | // s.Func(dt) 48 | 49 | // return time.Since(start) 50 | } 51 | 52 | // A log of a system and the time it took to execute 53 | type SystemLog struct { 54 | Name string 55 | Time time.Duration 56 | } 57 | 58 | func (s *SystemLog) String() string { 59 | return fmt.Sprintf("%s: %s", s.Name, s.Time) 60 | } 61 | 62 | // // TODO - Just use an atomic here? 63 | // type signal struct { 64 | // mu sync.Mutex 65 | // value bool 66 | // } 67 | 68 | // func (s *signal) Set(val bool) { 69 | // s.mu.Lock() 70 | // s.value = val 71 | // s.mu.Unlock() 72 | // } 73 | 74 | // func (s *signal) Get() bool { 75 | // s.mu.Lock() 76 | // ret := s.value 77 | // s.mu.Unlock() 78 | // return ret 79 | // } 80 | 81 | // Scheduler is a place to put your systems and have them run. 82 | // There are two types of systems: Fixed time systems and dynamic time systems 83 | // 1. Fixed time systems will execute on a fixed time step 84 | // 2. Dynamic time systems will execute as quickly as they possibly can 85 | // The scheduler may change in the future, but right now how it works is simple: 86 | // Input: Execute input systems (Dynamic time systems) 87 | // Physics: Execute physics systems (Fixed time systems) 88 | // Render: Execute render systems (Dynamic time systems) 89 | type Scheduler struct { 90 | world *World 91 | systems [][]System 92 | sysTimeFront, sysTimeBack [][]SystemLog // Rotating log of how long each system takes 93 | stageTimingFront, stageTimingBack []SystemLog // Rotating log of how long each stage takes 94 | 95 | fixedTimeStep time.Duration 96 | accumulator time.Duration 97 | gameSpeed float64 98 | quit atomic.Bool 99 | pauseRender atomic.Bool 100 | maxLoopCount int 101 | } 102 | 103 | // Creates a scheduler 104 | func NewScheduler(world *World) *Scheduler { 105 | return &Scheduler{ 106 | world: world, 107 | systems: make([][]System, StageLast+1), 108 | sysTimeFront: make([][]SystemLog, StageLast+1), 109 | sysTimeBack: make([][]SystemLog, StageLast+1), 110 | 111 | fixedTimeStep: 16 * time.Millisecond, 112 | accumulator: 0, 113 | gameSpeed: 1, 114 | } 115 | } 116 | 117 | // TODO make SetGameSpeed and SetFixedTimeStep thread safe. 118 | 119 | // Sets the rate at which time accumulates. Also, you want them to only change at the end of a frame, else you might get some inconsistencies. Just use a mutex and a single temporary variable 120 | func (s *Scheduler) SetGameSpeed(speed float64) { 121 | s.gameSpeed = speed 122 | } 123 | 124 | // Tells the scheduler to exit. Scheduler will finish executing its remaining tick before closing. 125 | func (s *Scheduler) SetQuit(value bool) { 126 | s.quit.Store(true) 127 | } 128 | 129 | // Returns the quit value of the scheduler 130 | func (s *Scheduler) Quit() bool { 131 | return s.quit.Load() 132 | } 133 | 134 | // Pauses the set of render systems (ie they will be skipped). 135 | // Deprecated: This API is tentatitive 136 | func (s *Scheduler) PauseRender(value bool) { 137 | s.pauseRender.Store(value) 138 | } 139 | 140 | // Sets the amount of time required before the fixed time systems will execute 141 | func (s *Scheduler) SetFixedTimeStep(t time.Duration) { 142 | s.fixedTimeStep = t 143 | } 144 | 145 | type Stage uint8 146 | 147 | func (s Stage) String() string { 148 | switch s { 149 | case StageStartup: 150 | return "StageStartup" 151 | case StagePreUpdate: 152 | return "StagePreUpdate" 153 | case StageFixedUpdate: 154 | return "StageFixedUpdate" 155 | case StageUpdate: 156 | return "StageUpdate" 157 | case StageLast: 158 | return "StageLast" 159 | } 160 | return "Unknown" 161 | } 162 | 163 | const ( 164 | // StagePreStartup 165 | StageStartup Stage = iota 166 | // StagePostStartup 167 | // StageFirst 168 | StagePreUpdate // Note: Used to be Input 169 | // StageStateTransition 170 | StageFixedUpdate 171 | // StagePostFixedUpdate 172 | StageUpdate 173 | // StagePostUpdate 174 | StageLast 175 | ) 176 | 177 | // Returns true if the scheduler only has fixed systems 178 | func (s *Scheduler) isFixedOnly() bool { 179 | return len(s.systems[StagePreUpdate]) == 0 && len(s.systems[StageUpdate]) == 0 && len(s.systems[StageLast]) == 0 180 | } 181 | 182 | func (s *Scheduler) ClearSystems(stage Stage) { 183 | // Note: Make a new slices so that any of the old system pointers get released 184 | s.systems[stage] = make([]System, 0) 185 | } 186 | 187 | func (s *Scheduler) AddSystems(stage Stage, systems ...SystemBuilder) { 188 | for _, sys := range systems { 189 | system := sys.Build(s.world) 190 | s.systems[stage] = append(s.systems[stage], system) 191 | } 192 | } 193 | 194 | func (s *Scheduler) SetSystems(stage Stage, systems ...SystemBuilder) { 195 | s.ClearSystems(stage) 196 | s.AddSystems(stage, systems...) 197 | } 198 | 199 | // Sets the accumulator maximum point so that if the accumulator gets way to big, we will reset it and continue on, dropping all physics ticks that would have been executed. This is useful in a runtime like WASM where the browser may not let us run as frequently as we may need (for example, when the tab is hidden or minimized). 200 | // Note: This must be set before you call scheduler.Run() 201 | // Note: The default value is 0, which will force every physics tick to run. I highly recommend setting this to something if you plan to build for WASM! 202 | func (s *Scheduler) SetMaxPhysicsLoopCount(count int) { 203 | s.maxLoopCount = count 204 | } 205 | 206 | func (s *Scheduler) Syslog(stage Stage) []SystemLog { 207 | return s.sysTimeFront[stage] 208 | } 209 | 210 | // Returns an interpolation value which represents how close we are to the next fixed time step execution. Can be useful for interpolating dynamic time systems to the fixed time systems. I might rename this 211 | func (s *Scheduler) GetRenderInterp() float64 { 212 | return s.accumulator.Seconds() / s.fixedTimeStep.Seconds() 213 | } 214 | 215 | func (s *Scheduler) runUntrackedStage(stage Stage, dt time.Duration) { 216 | for _, sys := range s.systems[stage] { 217 | sys.step(dt) 218 | s.world.cmd.Execute() 219 | } 220 | } 221 | 222 | func (s *Scheduler) runStage(stage Stage, dt time.Duration) { 223 | start := time.Now() 224 | 225 | for _, sys := range s.systems[stage] { 226 | sysStart := time.Now() 227 | sys.step(dt) 228 | s.world.cmd.Execute() 229 | 230 | { 231 | tmp := s.sysTimeFront[stage] 232 | s.sysTimeFront[stage] = s.sysTimeBack[stage] 233 | s.sysTimeBack[stage] = tmp[:0] 234 | } 235 | s.sysTimeBack[stage] = append(s.sysTimeBack[stage], SystemLog{ 236 | Name: sys.Name, 237 | Time: time.Since(sysStart), 238 | }) 239 | } 240 | 241 | { 242 | tmp := s.stageTimingFront 243 | s.stageTimingFront = s.stageTimingBack 244 | s.stageTimingBack = tmp[:0] 245 | } 246 | s.stageTimingBack = append(s.stageTimingBack, SystemLog{ 247 | Name: "STAGE NAME TODO", 248 | Time: time.Since(start), 249 | }) 250 | } 251 | 252 | // Performs a single step of the scheduler with the provided time 253 | func (s *Scheduler) Step(dt time.Duration) { 254 | // Pre Update 255 | s.runStage(StagePreUpdate, dt) 256 | 257 | maxLoopCount := time.Duration(s.maxLoopCount) 258 | if maxLoopCount > 0 { 259 | if s.accumulator > (maxLoopCount * s.fixedTimeStep) { 260 | s.accumulator = s.fixedTimeStep // Just run one loop 261 | } 262 | } 263 | 264 | // Physics Systems 265 | for s.accumulator >= s.fixedTimeStep { 266 | s.runStage(StageFixedUpdate, s.fixedTimeStep) 267 | s.accumulator -= s.fixedTimeStep 268 | } 269 | 270 | // Render Systems 271 | if !s.pauseRender.Load() { 272 | s.runStage(StageUpdate, dt) 273 | } 274 | } 275 | 276 | // Note: Would be nice to sleep or something to prevent spinning while we wait for work to do 277 | // Could also separate the render loop from the physics loop (requires some thread safety in ECS) 278 | func (s *Scheduler) Run() { 279 | s.runUntrackedStage(StageStartup, 0) 280 | 281 | frameStart := time.Now() 282 | dt := s.fixedTimeStep 283 | s.accumulator = 0 284 | 285 | for !s.quit.Load() { 286 | s.Step(dt) 287 | 288 | // Edge case for schedules only fixed time steps 289 | if s.isFixedOnly() { 290 | // Note: This is guaranteed to be positive because the physics execution loops until the accumulator is less than fixedtimestep 291 | time.Sleep(s.fixedTimeStep - s.accumulator) 292 | } 293 | 294 | // Capture Frame time 295 | now := time.Now() 296 | dt = now.Sub(frameStart) 297 | frameStart = now 298 | 299 | scaledDt := float64(dt.Nanoseconds()) * s.gameSpeed 300 | s.accumulator += time.Duration(scaledDt) 301 | } 302 | } 303 | 304 | // //Separates physics loop from render loop 305 | // func (s *Scheduler) Run2() { 306 | // var worldMu sync.Mutex 307 | 308 | // frameStart := time.Now() 309 | // dt := s.fixedTimeStep 310 | // // var accumulator time.Duration 311 | // s.accumulator = 0 312 | // maxLoopCount := time.Duration(s.maxLoopCount) 313 | 314 | // // physicsTicker := time.NewTicker(s.fixedTimeStep) 315 | // // defer physicsTicker.Stop() 316 | // go func() { 317 | // // for { 318 | // // _, more := <-physicsTicker.C 319 | // // if !more { break } // Exit early, ticker channel is closed 320 | // // // fmt.Println(phyTime) 321 | // // worldMu.Lock() 322 | // // for _, sys := range s.physics { 323 | // // sys.Run(s.fixedTimeStep) 324 | // // } 325 | // // worldMu.Unlock() 326 | // // } 327 | 328 | // for !s.quit.Load() { 329 | // worldMu.Lock() 330 | // if maxLoopCount > 0 { 331 | // if s.accumulator > (maxLoopCount * s.fixedTimeStep) { 332 | // s.accumulator = s.fixedTimeStep // Just run one loop 333 | // } 334 | // } 335 | // for s.accumulator >= s.fixedTimeStep { 336 | // for _, sys := range s.physics { 337 | // sys.Run(s.fixedTimeStep) 338 | // } 339 | // s.accumulator -= s.fixedTimeStep 340 | // } 341 | 342 | // worldMu.Unlock() 343 | // time.Sleep(s.fixedTimeStep - s.accumulator) 344 | // } 345 | // }() 346 | 347 | // for !s.quit.Load() { 348 | // worldMu.Lock() 349 | 350 | // for _, sys := range s.render { 351 | // sys.Run(dt) 352 | // } 353 | 354 | // for _, sys := range s.input { 355 | // sys.Run(dt) 356 | // } 357 | 358 | // // Capture Frame time 359 | // now := time.Now() 360 | // dt = now.Sub(frameStart) 361 | // frameStart = now 362 | 363 | // s.accumulator += dt 364 | // worldMu.Unlock() 365 | // } 366 | // } 367 | -------------------------------------------------------------------------------- /system_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // // Note: You can do this to pack more queries in 10 | // type custom struct { 11 | // query1 *View2[position, velocity] 12 | // query2 *View2[position, velocity] 13 | // } 14 | 15 | // func (c *custom) initialize(world *World) any { 16 | // return &custom{ 17 | // GetInjectable[*View2[position, velocity]](world), 18 | // GetInjectable[*View2[position, velocity]](world), 19 | // } 20 | // } 21 | 22 | func physicsSystem(dt time.Duration, query *View2[position, velocity]) { 23 | query.MapId(func(id Id, pos *position, vel *velocity) { 24 | pos.x += vel.x * dt.Seconds() 25 | pos.y += vel.y * dt.Seconds() 26 | pos.z += vel.z * dt.Seconds() 27 | }) 28 | } 29 | 30 | func TestSystemCreationNew(t *testing.T) { 31 | world := setupPhysics(100) 32 | sys := NewSystem1(physicsSystem).Build(world) 33 | 34 | fmt.Println("NAME", sys.Name) 35 | for range 100 { 36 | sys.step(16 * time.Millisecond) 37 | } 38 | } 39 | 40 | var lastTime time.Time 41 | 42 | func TestSchedulerPhysics(t *testing.T) { 43 | world := NewWorld() 44 | scheduler := NewScheduler(world) 45 | scheduler.AddSystems(StageFixedUpdate, System{ 46 | Name: "TestSystem", 47 | Func: func(dt time.Duration) { 48 | fmt.Printf("%v - %v\n", dt, time.Since(lastTime)) 49 | lastTime = time.Now() 50 | }, 51 | }) 52 | lastTime = time.Now() 53 | go scheduler.Run() 54 | time.Sleep(1 * time.Second) 55 | scheduler.SetQuit(true) 56 | } 57 | 58 | var lastTimeInput, lastTimePhysics, lastTimeRender time.Time 59 | 60 | func TestSchedulerAll(t *testing.T) { 61 | world := NewWorld() 62 | scheduler := NewScheduler(world) 63 | scheduler.AddSystems(StagePreUpdate, System{ 64 | Name: "TestSystemInput", 65 | Func: func(dt time.Duration) { 66 | fmt.Printf("Input: %v - %v\n", dt, time.Since(lastTimeInput)) 67 | lastTimeInput = time.Now() 68 | time.Sleep(1 * time.Millisecond) 69 | }, 70 | }) 71 | scheduler.AddSystems(StageFixedUpdate, System{ 72 | Name: "TestSystemPhysics", 73 | Func: func(dt time.Duration) { 74 | fmt.Printf("Physics: %v - %v\n", dt, time.Since(lastTimePhysics)) 75 | lastTimePhysics = time.Now() 76 | }, 77 | }) 78 | scheduler.AddSystems(StageUpdate, System{ 79 | Name: "TestSystemRender", 80 | Func: func(dt time.Duration) { 81 | fmt.Printf("Render: %v - %v\n", dt, time.Since(lastTimeRender)) 82 | lastTimeRender = time.Now() 83 | time.Sleep(100 * time.Millisecond) 84 | }, 85 | }) 86 | lastTimeInput = time.Now() 87 | lastTimePhysics = time.Now() 88 | lastTimeRender = time.Now() 89 | go scheduler.Run() 90 | time.Sleep(1 * time.Second) 91 | scheduler.SetQuit(true) 92 | } 93 | -------------------------------------------------------------------------------- /world.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "sync/atomic" 7 | "time" 8 | 9 | "reflect" // For resourceName 10 | ) 11 | 12 | var ( 13 | DefaultAllocation = 0 14 | ) 15 | 16 | const ( 17 | InvalidEntity Id = 0 // Represents the default entity Id, which is invalid 18 | firstEntity Id = 1 19 | MaxEntity Id = math.MaxUint32 20 | ) 21 | 22 | // World is the main data-holder. You usually pass it to other functions to do things. 23 | type World struct { 24 | idCounter atomic.Uint64 25 | nextId Id 26 | minId, maxId Id // This is the range of Ids returned by NewId 27 | arch locMap 28 | engine *archEngine 29 | resources map[reflect.Type]any 30 | observers *internalMap[EventId, list[Handler]] // TODO: SliceMap instead of map 31 | cmd *CommandQueue 32 | } 33 | 34 | // Creates a new world 35 | func NewWorld() *World { 36 | world := &World{ 37 | nextId: firstEntity + 1, 38 | minId: firstEntity + 1, 39 | maxId: MaxEntity, 40 | arch: newLocMap(DefaultAllocation), 41 | engine: newArchEngine(), 42 | 43 | resources: make(map[reflect.Type]any), 44 | observers: newMap[EventId, list[Handler]](0), 45 | } 46 | 47 | world.cmd = GetInjectable[*CommandQueue](world) 48 | 49 | return world 50 | } 51 | 52 | func (w *World) print() { 53 | fmt.Printf("%+v\n", *w) 54 | 55 | w.engine.print() 56 | } 57 | 58 | // Sets an range of Ids that the world will use when creating new Ids. Potentially helpful when you have multiple worlds and don't want their Id space to collide. 59 | // Deprecated: This API is tentative. It may be better to just have the user create Ids as they see fit 60 | func (w *World) SetIdRange(min, max Id) { 61 | if min <= firstEntity { 62 | panic("max must be greater than 1") 63 | } 64 | if max <= firstEntity { 65 | panic("max must be greater than 1") 66 | } 67 | if min > max { 68 | panic("min must be less than max!") 69 | } 70 | 71 | w.minId = min 72 | w.maxId = max 73 | } 74 | 75 | // Creates a new Id which can then be used to create an entity. This is threadsafe 76 | func (w *World) NewId() Id { 77 | for { 78 | val := w.idCounter.Load() 79 | if w.idCounter.CompareAndSwap(val, val+1) { 80 | return (Id(val) % (w.maxId - w.minId)) + w.minId 81 | } 82 | } 83 | 84 | // if w.nextId < w.minId { 85 | // w.nextId = w.minId 86 | // } 87 | 88 | // id := w.nextId 89 | 90 | // if w.nextId == w.maxId { 91 | // w.nextId = w.minId 92 | // } else { 93 | // w.nextId++ 94 | // } 95 | // return id 96 | } 97 | 98 | func (world *World) Spawn(comp ...Component) Id { 99 | id := world.NewId() 100 | world.spawn(id, comp...) 101 | return id 102 | } 103 | 104 | func (world *World) spawn(id Id, comp ...Component) { 105 | // Id does not yet exist, we need to add it for the first time 106 | mask := buildArchMask(comp...) 107 | archId := world.engine.getArchetypeId(mask) 108 | 109 | // Write all components to that archetype 110 | index := world.engine.spawn(archId, id, comp...) 111 | world.arch.Put(id, entLoc{archId, uint32(index)}) 112 | 113 | world.engine.runFinalizedHooks(id) 114 | } 115 | 116 | // returns true if the entity in the world has the compId 117 | func (world *World) hasCompId(id Id, compId CompId) bool { 118 | loc, ok := world.arch.Get(id) 119 | if !ok { 120 | return false 121 | } 122 | 123 | lookup := world.engine.lookup[loc.archId] 124 | return lookup.mask.hasComponent(compId) 125 | } 126 | 127 | // Writes components to the entity specified at id. This API can potentially break if you call it inside of a loop. Specifically, if you cause the archetype of the entity to change by writing a new component, then the loop may act in mysterious ways. 128 | // Deprecated: This API is tentative, I might replace it with something similar to bevy commands to alleviate the above concern 129 | func Write(world *World, id Id, comp ...Component) { 130 | world.Write(id, comp...) 131 | } 132 | 133 | func (world *World) Write(id Id, comp ...Component) { 134 | if len(comp) <= 0 { 135 | return // Do nothing if there are no components 136 | } 137 | 138 | loc, ok := world.arch.Get(id) 139 | if ok { 140 | newLoc := world.engine.rewriteArch(loc, id, comp...) 141 | world.arch.Put(id, newLoc) 142 | } else { 143 | world.spawn(id, comp...) 144 | } 145 | 146 | world.engine.runFinalizedHooks(id) 147 | } 148 | 149 | func (w *World) writeBundler(id Id, b *Bundler) { 150 | newLoc := w.allocateMove(id, b.archMask) 151 | 152 | wd := W{ 153 | engine: w.engine, 154 | archId: newLoc.archId, 155 | index: int(newLoc.index), 156 | } 157 | 158 | for i := CompId(0); i <= b.maxComponentIdAdded; i++ { 159 | if !b.Set[i] { 160 | continue 161 | } 162 | 163 | b.Components[i].CompWrite(wd) 164 | } 165 | 166 | w.engine.runFinalizedHooks(id) 167 | } 168 | 169 | // func (world *World) GetArchetype(comp ...Component) archetypeId { 170 | // mask := buildArchMask(comp...) 171 | // return world.engine.getArchetypeId(mask) 172 | // } 173 | 174 | // // Note: This returns the index of the location allocated 175 | // func (world *World) Allocate(id Id, archId archetypeId) int { 176 | // return world.allocate(id, world.engine.dcr.revArchMask[archId]) 177 | // } 178 | 179 | // Allocates an index for the id at the specified addMask location 180 | // 1. If the id already exists, an archetype move will happen 181 | // 2. If the id doesn't exist, then the addMask is the newMask and the entity will be allocated there 182 | // Returns the index of the location allocated. May return -1 if invalid archMask supplied 183 | func (world *World) allocateMove(id Id, addMask archetypeMask) entLoc { 184 | if addMask == blankArchMask { 185 | // Nothing to allocate, aka do nothing 186 | loc, _ := world.arch.Get(id) 187 | // TODO: Technically this is some kind of error if id isn't set 188 | return loc 189 | } 190 | 191 | loc, ok := world.arch.Get(id) 192 | if ok { 193 | // Calculate the new mask based on the bitwise or of the old and added masks 194 | lookup := world.engine.lookup[loc.archId] 195 | oldMask := lookup.mask 196 | newMask := oldMask.bitwiseOr(addMask) 197 | 198 | // If the new mask matches the old mask, then we don't need to move anything 199 | if oldMask == newMask { 200 | return loc 201 | } 202 | 203 | newLoc := world.engine.moveArchetype(loc, newMask, id) 204 | world.arch.Put(id, newLoc) 205 | 206 | world.engine.finalizeOnAdd = markComponentDiff(world.engine.finalizeOnAdd, addMask, oldMask) 207 | 208 | return newLoc 209 | } else { 210 | // Id does not yet exist, we need to add it for the first time 211 | archId := world.engine.getArchetypeId(addMask) 212 | // Write all components to that archetype 213 | newIndex := world.engine.allocate(archId, id) 214 | 215 | newLoc := entLoc{archId, uint32(newIndex)} 216 | world.arch.Put(id, newLoc) 217 | 218 | world.engine.finalizeOnAdd = markComponentMask(world.engine.finalizeOnAdd, addMask) 219 | 220 | return newLoc 221 | } 222 | } 223 | 224 | // May return -1 if invalid archMask supplied, or if the entity doesn't exist 225 | func (world *World) deleteMask(id Id, deleteMask archetypeMask) { 226 | loc, ok := world.arch.Get(id) 227 | if !ok { 228 | return 229 | } 230 | 231 | // 1. calculate the destination mask 232 | lookup := world.engine.lookup[loc.archId] 233 | oldMask := lookup.mask 234 | newMask := oldMask.bitwiseClear(deleteMask) 235 | 236 | // If the new mask requires the removal of all components, then just delete the current entity 237 | if newMask == blankArchMask { 238 | Delete(world, id) 239 | return 240 | } 241 | 242 | // If the new mask matches the old mask, then we don't need to move anything 243 | if oldMask == newMask { 244 | return 245 | } 246 | 247 | // 2. Move all components from source arch to dest arch 248 | newLoc := world.engine.moveArchetypeDown(loc, newMask, id) 249 | world.arch.Put(id, newLoc) 250 | } 251 | 252 | // This is safe for maps and loops 253 | // 1. This deletes the high level id -> archId lookup 254 | // 2. This creates a "hole" in the archetype list 255 | // Returns true if the entity was deleted, else returns false if the entity does not exist (or was already deleted) 256 | 257 | // Deletes the entire entity specified by the id 258 | // This can be called inside maps and loops, it will delete the entity immediately. 259 | // Returns true if the entity exists and was actually deleted, else returns false 260 | func Delete(world *World, id Id) bool { 261 | archId, ok := world.arch.Get(id) 262 | if !ok { 263 | return false 264 | } 265 | 266 | world.arch.Delete(id) 267 | 268 | world.engine.TagForDeletion(archId, id) 269 | return true 270 | } 271 | 272 | // Deletes specific components from an entity in the world 273 | // Skips all work if the entity doesn't exist 274 | // Skips deleting components that the entity doesn't have 275 | // If no components remain after the delete, the entity will be completely removed 276 | func DeleteComponent(world *World, id Id, comp ...Component) { 277 | if len(comp) <= 0 { 278 | return 279 | } 280 | 281 | mask := buildArchMask(comp...) 282 | world.deleteMask(id, mask) 283 | } 284 | 285 | // Returns true if the entity exists in the world else it returns false 286 | func (world *World) Exists(id Id) bool { 287 | return world.arch.Has(id) 288 | } 289 | 290 | // -------------------------------------------------------------------------------- 291 | // - Observers 292 | // -------------------------------------------------------------------------------- 293 | func (w *World) Trigger(event Event, id Id) { 294 | handlerList, ok := w.observers.Get(event.EventId()) 295 | if !ok { 296 | return 297 | } 298 | for _, handler := range handlerList.list { 299 | handler.Run(id, event) 300 | } 301 | } 302 | 303 | func (w *World) AddObserver(handler Handler) { 304 | handlerList, ok := w.observers.Get(handler.EventTrigger()) 305 | if !ok { 306 | handlerList = newList[Handler]() 307 | } 308 | 309 | handlerList.Add(handler) 310 | w.observers.Put(handler.EventTrigger(), handlerList) 311 | } 312 | 313 | // You may only register one hook per component, else it will panic 314 | func (w *World) SetHookOnAdd(comp Component, handler Handler) { 315 | current := w.engine.onAddHooks[comp.CompId()] 316 | if current != nil { 317 | panic("AddHook: You may only register one hook per component") 318 | } 319 | w.engine.onAddHooks[comp.CompId()] = handler 320 | } 321 | 322 | // -------------------------------------------------------------------------------- 323 | // - Resources 324 | // -------------------------------------------------------------------------------- 325 | func resourceName(t any) reflect.Type { 326 | return reflect.TypeOf(t) 327 | } 328 | 329 | // TODO: Should I force people to do pointers? 330 | func PutResource[T any](world *World, resource *T) { 331 | name := resourceName(resource) 332 | world.resources[name] = resource 333 | } 334 | 335 | func GetResource[T any](world *World) *T { 336 | var t T 337 | name := resourceName(&t) 338 | anyVal, ok := world.resources[name] 339 | if !ok { 340 | return nil 341 | } 342 | 343 | return anyVal.(*T) 344 | } 345 | 346 | // -------------------------------------------------------------------------------- 347 | // - Systems 348 | // -------------------------------------------------------------------------------- 349 | func (w *World) StepSystemList(dt time.Duration, systems ...System) time.Duration { 350 | start := time.Now() 351 | for i := range systems { 352 | systems[i].step(dt) 353 | w.cmd.Execute() 354 | } 355 | return time.Since(start) 356 | } 357 | -------------------------------------------------------------------------------- /world_test.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | var positionId = NewComp[position]() 9 | var velocityId = NewComp[velocity]() 10 | var accelerationId = NewComp[acceleration]() 11 | var radiusId = NewComp[radius]() 12 | 13 | func (p position) CompId() CompId { 14 | return positionId.CompId() 15 | } 16 | func (p velocity) CompId() CompId { 17 | return velocityId.CompId() 18 | } 19 | func (p acceleration) CompId() CompId { 20 | return accelerationId.CompId() 21 | } 22 | func (p radius) CompId() CompId { 23 | return radiusId.CompId() 24 | } 25 | 26 | func (p position) CompWrite(cw W) { 27 | positionId.WriteVal(cw, p) 28 | } 29 | func (p velocity) CompWrite(cw W) { 30 | velocityId.WriteVal(cw, p) 31 | } 32 | func (p acceleration) CompWrite(cw W) { 33 | accelerationId.WriteVal(cw, p) 34 | } 35 | func (p radius) CompWrite(cw W) { 36 | radiusId.WriteVal(cw, p) 37 | } 38 | 39 | type position struct { 40 | x, y, z float64 41 | } 42 | 43 | type velocity struct { 44 | x, y, z float64 45 | } 46 | 47 | type acceleration struct { 48 | x, y, z float64 49 | } 50 | 51 | type radius struct { 52 | r float64 53 | } 54 | 55 | // Helper functions 56 | 57 | // Check that this boolean is true 58 | func check(t *testing.T, b bool) { 59 | if !b { 60 | _, f, l, _ := runtime.Caller(1) 61 | t.Errorf("%s:%d - checked boolean is false!", f, l) 62 | } 63 | } 64 | 65 | // Check two things match, if they don't, throw an error 66 | func compare[T comparable](t *testing.T, actual, expected T) { 67 | if expected != actual { 68 | _, f, l, _ := runtime.Caller(1) 69 | t.Errorf("%s:%d - actual(%v) did not match expected(%v)", f, l, actual, expected) 70 | } 71 | } 72 | 73 | func TestWorldSpawn(t *testing.T) { 74 | world := NewWorld() 75 | 76 | // Write position 77 | id1 := world.Spawn(position{1, 1, 1}) 78 | id2 := world.Spawn(position{2, 2, 2}, velocity{3, 3, 3}) 79 | 80 | p, ok := Read[position](world, id1) 81 | check(t, ok) 82 | compare(t, p, position{1, 1, 1}) 83 | 84 | p, ok = Read[position](world, id2) 85 | check(t, ok) 86 | compare(t, p, position{2, 2, 2}) 87 | 88 | v, ok := Read[velocity](world, id2) 89 | check(t, ok) 90 | compare(t, v, velocity{3, 3, 3}) 91 | } 92 | 93 | func TestBlankWrite(t *testing.T) { 94 | world := NewWorld() 95 | id := world.NewId() 96 | Write(world, id) 97 | 98 | ok := world.Exists(id) 99 | check(t, !ok) 100 | } 101 | 102 | func TestWorldReadWrite(t *testing.T) { 103 | world := NewWorld() 104 | id := world.NewId() 105 | 106 | // Write position 107 | pos := position{1, 1, 1} 108 | Write(world, id, C(pos)) 109 | 110 | // Check position and velocity 111 | posOut, ok := Read[position](world, id) 112 | check(t, ok) 113 | compare(t, posOut, pos) 114 | velOut, ok := Read[velocity](world, id) 115 | check(t, !ok) // We expect this to be false 116 | compare(t, velOut, velocity{0, 0, 0}) 117 | 118 | // Write velocity 119 | vel := velocity{2, 2, 2} 120 | Write(world, id, C(vel)) 121 | 122 | // Check position and velocity 123 | posOut, ok = Read[position](world, id) 124 | check(t, ok) 125 | compare(t, posOut, pos) 126 | velOut, ok = Read[velocity](world, id) 127 | check(t, ok) 128 | compare(t, velOut, vel) 129 | 130 | compare(t, world.engine.count(position{}), 1) 131 | compare(t, world.engine.count(position{}, velocity{}), 1) 132 | compare(t, world.engine.count(position{}, velocity{}), 1) 133 | compare(t, world.engine.count(acceleration{}), 0) 134 | 135 | // count := 0 136 | // Map2(world, func(id Id, p *position, v *velocity) { 137 | // count++ 138 | // }) 139 | // compare(t, count, 1) 140 | 141 | // count = 0 142 | // view := ViewAll2[position, velocity](world) 143 | // for { 144 | // _, _, _, ok := view.Iter() 145 | // if !ok { break } 146 | // count++ 147 | // } 148 | // compare(t, count, 1) 149 | } 150 | 151 | func TestWorldReadMultiWrite(t *testing.T) { 152 | world := NewWorld() 153 | id := world.NewId() 154 | 155 | pos := position{1, 1, 1} 156 | vel := velocity{2, 2, 2} 157 | Write(world, id, C(pos), C(vel)) 158 | 159 | // Check position and velocity 160 | posOut, ok := Read[position](world, id) 161 | check(t, ok) 162 | compare(t, posOut, pos) 163 | velOut, ok := Read[velocity](world, id) 164 | check(t, ok) 165 | compare(t, velOut, vel) 166 | 167 | // Write accel and size 168 | accel := acceleration{3, 3, 3} 169 | rad := radius{4} 170 | Write(world, id, C(accel), C(rad)) 171 | 172 | // Check all 173 | posOut, ok = Read[position](world, id) 174 | check(t, ok) 175 | compare(t, posOut, pos) 176 | velOut, ok = Read[velocity](world, id) 177 | check(t, ok) 178 | compare(t, velOut, vel) 179 | accelOut, ok := Read[acceleration](world, id) 180 | check(t, ok) 181 | compare(t, accelOut, accel) 182 | radOut, ok := Read[radius](world, id) 183 | check(t, ok) 184 | compare(t, radOut, rad) 185 | } 186 | 187 | func TestWorldWriteDelete(t *testing.T) { 188 | world := NewWorld() 189 | ids := make([]Id, 0) 190 | for i := 0; i < 1e6; i++ { 191 | id := world.NewId() 192 | v := float64(id) 193 | pos := position{v, v, v} 194 | vel := velocity{v, v, v} 195 | Write(world, id, C(pos), C(vel)) 196 | ids = append(ids, id) 197 | } 198 | 199 | // Verify they are all correct 200 | for _, id := range ids { 201 | expected := float64(id) 202 | 203 | posOut, ok := Read[position](world, id) 204 | check(t, ok) 205 | compare(t, posOut, position{expected, expected, expected}) 206 | 207 | velOut, ok := Read[velocity](world, id) 208 | check(t, ok) 209 | compare(t, velOut, velocity{expected, expected, expected}) 210 | } 211 | 212 | // Delete every even index 213 | for i, id := range ids { 214 | if i%2 == 0 { 215 | Delete(world, id) 216 | } 217 | } 218 | 219 | // Verify they are all correct 220 | for i, id := range ids { 221 | expected := float64(id) 222 | 223 | if i%2 == 0 { 224 | // Expect these to be deleted in the world 225 | expected = 0.0 226 | posOut, ok := Read[position](world, id) 227 | check(t, !ok) // Expect to be false because we've deleted this 228 | compare(t, posOut, position{expected, expected, expected}) 229 | 230 | velOut, ok := Read[velocity](world, id) 231 | check(t, !ok) // Expect to be false because we've deleted this 232 | compare(t, velOut, velocity{expected, expected, expected}) 233 | } else { 234 | // Expect these to still exist in the world 235 | posOut, ok := Read[position](world, id) 236 | check(t, ok) 237 | compare(t, posOut, position{expected, expected, expected}) 238 | 239 | velOut, ok := Read[velocity](world, id) 240 | check(t, ok) 241 | compare(t, velOut, velocity{expected, expected, expected}) 242 | } 243 | } 244 | } 245 | 246 | func TestWorldDeleteAllViaComponents(t *testing.T) { 247 | world := NewWorld() 248 | 249 | id := world.NewId() 250 | world.Write(id, C(position{}), C(velocity{}), C(acceleration{}), C(radius{})) 251 | 252 | DeleteComponent(world, id, C(position{})) 253 | DeleteComponent(world, id, C(velocity{})) 254 | DeleteComponent(world, id, C(acceleration{})) 255 | DeleteComponent(world, id, C(radius{})) 256 | _, ok := Read[position](world, id) 257 | check(t, !ok) 258 | _, ok = Read[velocity](world, id) 259 | check(t, !ok) 260 | _, ok = Read[acceleration](world, id) 261 | check(t, !ok) 262 | _, ok = Read[radius](world, id) 263 | check(t, !ok) 264 | 265 | exists := world.Exists(id) 266 | check(t, !exists) 267 | } 268 | 269 | func TestWorldDeleteComponent(t *testing.T) { 270 | world := NewWorld() 271 | ids := make([]Id, 0) 272 | for i := 0; i < 1e6; i++ { 273 | id := world.NewId() 274 | v := float64(id) 275 | pos := position{v, v, v} 276 | vel := velocity{v, v, v} 277 | Write(world, id, C(pos), C(vel)) 278 | ids = append(ids, id) 279 | } 280 | 281 | // Verify they are all correct 282 | for _, id := range ids { 283 | expected := float64(id) 284 | 285 | posOut, ok := Read[position](world, id) 286 | check(t, ok) 287 | compare(t, posOut, position{expected, expected, expected}) 288 | 289 | velOut, ok := Read[velocity](world, id) 290 | check(t, ok) 291 | compare(t, velOut, velocity{expected, expected, expected}) 292 | } 293 | 294 | // different deletes for different modulos 295 | for i, id := range ids { 296 | if i%2 == 0 { 297 | DeleteComponent(world, id, C(position{})) 298 | } else if i%3 == 0 { 299 | DeleteComponent(world, id, C(velocity{})) 300 | } else if i%5 == 0 { 301 | DeleteComponent(world, id, C(position{}), C(velocity{})) 302 | } else if i%7 == 0 { 303 | DeleteComponent(world, id, C(velocity{})) 304 | DeleteComponent(world, id, C(position{})) 305 | } else if i%13 == 0 { 306 | DeleteComponent(world, id, C(radius{})) // Note: This shouldn't do anything 307 | } 308 | } 309 | 310 | // Verify they are all correct 311 | for i, id := range ids { 312 | expected := float64(id) 313 | 314 | if i%2 == 0 { 315 | // Expect these to be deleted in the world 316 | _, ok := Read[position](world, id) 317 | check(t, !ok) // Expect to be false because we've deleted this 318 | 319 | velOut, ok := Read[velocity](world, id) 320 | check(t, ok) 321 | compare(t, velOut, velocity{expected, expected, expected}) 322 | } else if i%3 == 0 { 323 | // Expect these to still exist in the world 324 | posOut, ok := Read[position](world, id) 325 | check(t, ok) 326 | compare(t, posOut, position{expected, expected, expected}) 327 | 328 | _, ok = Read[velocity](world, id) 329 | check(t, !ok) // Expect to be false because we've deleted this 330 | } else if i%5 == 0 { 331 | _, ok := Read[position](world, id) 332 | check(t, !ok) // Expect to be false because we've deleted this 333 | _, ok = Read[velocity](world, id) 334 | check(t, !ok) // Expect to be false because we've deleted this 335 | } else if i%7 == 0 { 336 | _, ok := Read[position](world, id) 337 | check(t, !ok) // Expect to be false because we've deleted this 338 | _, ok = Read[velocity](world, id) 339 | check(t, !ok) // Expect to be false because we've deleted this 340 | } else { 341 | // Expect these to still exist in the world 342 | posOut, ok := Read[position](world, id) 343 | check(t, ok) 344 | compare(t, posOut, position{expected, expected, expected}) 345 | velOut, ok := Read[velocity](world, id) 346 | check(t, ok) 347 | compare(t, velOut, velocity{expected, expected, expected}) 348 | } 349 | } 350 | } 351 | 352 | func TestResources(t *testing.T) { 353 | world := NewWorld() 354 | p := position{1, 2, 3} 355 | 356 | p0 := GetResource[position](world) 357 | compare(t, p0, nil) // should be nil b/c it isnt added yet 358 | 359 | PutResource(world, &p) 360 | 361 | p1 := GetResource[position](world) 362 | compare(t, p1, &p) // Should match the original pointer 363 | compare(t, *p1, p) 364 | } 365 | --------------------------------------------------------------------------------