├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── unittests.yaml │ └── unittests_devel.yaml ├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── easyess.nimble ├── examples ├── example_01.nim ├── example_01_no_comments.nim ├── minimal.nim ├── nim.cfg ├── nimraylib_game_3d │ ├── nim.cfg │ ├── nimraylib_game_3d.nimble │ └── src │ │ └── nimraylib_game_3d.nim ├── rapid_game │ ├── .gitignore │ ├── rapid_game.nimble │ └── src │ │ └── rapid_game.nim └── snakelike_rapid_game │ ├── .gitignore │ ├── README.md │ ├── rapid_game.nimble │ └── src │ └── rapid_game.nim ├── nim.cfg ├── src ├── easyess.nim └── easyess │ └── core.nim └── tests ├── nim.cfg ├── test_components.nim ├── test_entities.nim └── test_systems.nim /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *.mp3 filter=lfs diff=lfs merge=lfs -text 3 | *.ase filter=lfs diff=lfs merge=lfs -text 4 | *.wav filter=lfs diff=lfs merge=lfs -text 5 | *.jpg filter=lfs diff=lfs merge=lfs -text 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ErikWDev] 4 | patreon: ErikWDev 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: ErikWDev 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/unittests.yaml: -------------------------------------------------------------------------------- 1 | name: Unittests 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - 'tests/**' 8 | - '.github/**' 9 | 10 | jobs: 11 | nimble-test: 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | nim: 17 | - '1.6.0' 18 | - 'stable' 19 | os: 20 | - 'ubuntu-latest' 21 | 22 | name: Tests on ${{ matrix.os }}, nim ${{ matrix.nim }} 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Cache nimble 27 | id: cache-nimble 28 | uses: actions/cache@v1 29 | with: 30 | path: ~/.nimble 31 | key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 32 | if: runner.os != 'Windows' 33 | 34 | - name: Setup nim 35 | uses: jiro4989/setup-nim-action@v1 36 | with: 37 | nim-version: ${{ matrix.nim }} 38 | 39 | - name: Run nimble tests 40 | run: nimble tests -Y 41 | -------------------------------------------------------------------------------- /.github/workflows/unittests_devel.yaml: -------------------------------------------------------------------------------- 1 | name: Unittests Devel 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'src/**' 7 | - 'tests/**' 8 | - '.github/**' 9 | 10 | jobs: 11 | nimble-test: 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | nim: 17 | - 'devel' 18 | os: 19 | - 'ubuntu-latest' 20 | 21 | name: Tests on ${{ matrix.os }}, nim ${{ matrix.nim }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Cache nimble 26 | id: cache-nimble 27 | uses: actions/cache@v1 28 | with: 29 | path: ~/.nimble 30 | key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 31 | if: runner.os != 'Windows' 32 | 33 | - name: Setup nim 34 | uses: jiro4989/setup-nim-action@v1 35 | with: 36 | nim-version: ${{ matrix.nim }} 37 | 38 | - name: Run nimble tests 39 | run: nimble tests -Y 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore all 3 | * 4 | 5 | # Unignore all with extensions 6 | !*.* 7 | 8 | # Unignore all dirs 9 | !*/ 10 | 11 | *.out 12 | *.exe 13 | *.bin 14 | 15 | bin/ 16 | htmldocs/ 17 | env.json 18 | !LICENSE 19 | !README 20 | !README.md 21 | !TODO -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 ErikWDev 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Easyess

2 |

"An easy to use ECS... EasyEss?" - Erik

3 |

4 | 5 | 6 |

7 | 8 | ## About 9 | First and foremost, if you really want an ECS with great performance 10 | and lots of thought put into it, this might not be for you. I instead point 11 | you to the amazing [polymorph](https://github.com/rlipsc/polymorph) which has 12 | has great documentation and great performance from own experience. 13 | 14 | Easyess started as a learning project for myself after having used polymorph 15 | and being amazed by its performance. I had never really gotten into writing 16 | `macros` and `templates` in Nim before this, and after having had that 17 | experience I began investigaring them more. 18 | 19 | ## Features 20 | - [X] Components of any kind (int, enum, object, tuple ..) 21 | - [X] Systems with ability to name components whatever `(vel: Velocity, pos: Position)` 22 | - [X] Good enough performance for simple games IMO 23 | - [X] Ability to group systems and run groups 24 | - [X] All systems can be run individually as well 25 | 26 | ## TODO 27 | - [ ] Ability to pause and unpause systems 28 | - [ ] Ability to clear ecs worlds easily 29 | - [ ] Host documentation (available locally with `nimble docgen`!) 30 | - [ ] Add example with graphics using something like glfw / sdl2 / rapid 31 | - [ ] Publish sample game using `easyess` 32 | 33 | ## Example 34 | The minimalistic example in `example/minimal.nim` without comments and detailed explanations looks like the following: 35 | ```nim 36 | 37 | import easyess 38 | 39 | comp: 40 | type 41 | Position = object 42 | x: float 43 | y: float 44 | 45 | Velocity = object 46 | dx: float 47 | dy: float 48 | 49 | 50 | sys [Position, vel: Velocity], "systems": 51 | func moveSystem(item: Item) = 52 | let 53 | (ecs, entity) = item 54 | oldPosition = position 55 | 56 | position.y += vel.dy 57 | item.position.x += item.velocity.dx 58 | 59 | when not defined(release): 60 | debugEcho "Moved " & ecs.inspect(entity) & " from ", oldPosition, " to ", position 61 | 62 | 63 | createECS(ECSConfig(maxEntities: 100)) 64 | 65 | when isMainModule: 66 | let 67 | ecs = newECS() 68 | entity2 = ecs.newEntity("test") 69 | # entity1 = ecs.createEntity("Entity 1"): ( 70 | # Position(x: 0.0, y: 0.0), 71 | # Velocity(dx: 10.0, dy: -10.0) 72 | # ) 73 | # entity2 = ecs.newEntity("Entity 2") 74 | 75 | (ecs, entity2).addComponent(Position(x: 0.0, y: 0.0)) 76 | (ecs, entity2).addVelocity(Velocity(dx: -10.0, dy: 10.0)) 77 | 78 | for i in 1 .. 10: 79 | ecs.runSystems() 80 | ``` 81 | 82 | This example is taken from `examples/example_01.nim`. It is quite long, but 83 | includes detailed explanations in the comments and covers basically everything 84 | that easyess provides. If you want the same exaple without the comments, see 85 | `examples/example_01_no_comments.nim`. Also check out the tests inside `tests/`! 86 | 87 | ```nim 88 | 89 | import easyess 90 | 91 | 92 | type 93 | Game = object 94 | value: int 95 | 96 | # Define components using the `comp` macro. Components can have any type 97 | # that doesn't use generics. 98 | comp: 99 | type 100 | Position = object 101 | x: float 102 | y: float 103 | 104 | Velocity = object 105 | dx: float 106 | dy: float 107 | 108 | Sprite = tuple 109 | id: int 110 | potato: int 111 | 112 | TupleComponent = tuple 113 | test: string 114 | 115 | CustomFlag = enum 116 | ckTest 117 | ckPotato 118 | 119 | Name = string 120 | 121 | Value = int 122 | 123 | DistinctValue = distinct int 124 | 125 | IsDead = bool 126 | 127 | # Define systems using the `sys` macro. 128 | # Specify which components are needed using `[Component1, Component2]` and 129 | # the 'group' that this system belongs to using a string. 130 | # The system should be a `proc` or `func` that takes an argument of type `Item` 131 | # The `Item` type is a `tuple[ecs: ECS, entity: Entity]`. 132 | const 133 | systemsGroup = "systems" 134 | renderingGroup = "rendering" 135 | 136 | sys [Position, Velocity], systemsGroup: 137 | func moveSystem(item: Item) = 138 | let 139 | (ecs, entity) = item 140 | oldPosition = position 141 | # Inside your system, templates are defined corresponging to 142 | # the Components that you have requested. `Position` nad `Velocity` 143 | # were requested here, so now 'position' and 'velocity' are available 144 | position.x += velocity.dx 145 | 146 | # You can also do `item.position` explicitly, but it is also a template 147 | item.position.y += item.velocity.dy 148 | when not defined(release): 149 | debugEcho "Moved " & ecs.inspect(entity) & " from ", oldPosition, " to ", position 150 | 151 | 152 | # Systems can have side-effects when marked 153 | # as `proc` and access variables either outside 154 | # the entire `sys` macro or 'within' it, but those 155 | # defined on the inside will still be considered global. 156 | 157 | # You can also pass an extra 'Data' parameter to a system 158 | # by specifying it after the `Item`. You must later provide 159 | # a variable of that same type when you call the system's group! 160 | 161 | var oneGlobalValue = 1 162 | 163 | sys [Sprite], renderingGroup: 164 | var secondGlobalValue = "Renderingwindow" 165 | 166 | proc renderSpriteSystem(item: Item, game: var Game) = 167 | # Note that we request `var Game` here: ^^^^^^^^ 168 | # That means that when we later call `ecs.runRendering()`, 169 | # we will have to supply an extra argument of the same type! 170 | # like so: `ecs.runRendering(game)` 171 | 172 | echo secondGlobalValue, ": Rendering sprite #", sprite.id 173 | inc oneGlobalValue 174 | inc game.value 175 | 176 | # If you want to give your components a different variable 177 | # name within the system, you can do so by specifying it as 178 | # such: `: `. The default name is always 179 | # otherwise`` (first letter lowercase) 180 | sys [dead: IsDead], systemsGroup: 181 | proc isDeadSystem(item: Item) = 182 | echo dead 183 | 184 | sys [CustomFlag], systemsGroup: 185 | proc customFlagSystem(item: Item) = 186 | echo customFlag 187 | 188 | # State machines can be implemented using a single enum as the component! 189 | case customFlag: 190 | of ckTest: customFlag = ckPotato 191 | of ckPotato: customFlag = ckTest 192 | 193 | # Once all components and systems have been defined or 194 | # imported, call `createECS` with a `ECSConfig`. The order 195 | # matters here and `createECS` has to be called AFTER component 196 | # and system definitions. 197 | createECS(ECSConfig(maxEntities: 100)) 198 | 199 | when isMainModule: 200 | # The ecs state can be instantiated using `newECS` (created by `createECS`) 201 | let ecs = newECS() 202 | var game = Game(value: 0) 203 | 204 | # Entities can be instantiated either manually or using the template 205 | # `createEntity` which takes a debug label that will be ignored 206 | # `when defined(release)`, as well as a tuple of Components 207 | # For the template to work with non-object components, the type 208 | # has to be specified within brackets as `[]` 209 | let entity1 = ecs.createEntity("Entity 1"): ( 210 | Position(x: 10.0, y: 0.0), 211 | Velocity(dx: 1.0, dy: -1.0), 212 | 213 | [Sprite](id: 42, potato: 12), 214 | [CustomFlag]ckTest, 215 | [Name]"SomeNiceName", 216 | [Value]10, 217 | [DistinctValue]20, 218 | [IsDead]true 219 | ) 220 | 221 | # Entities can also be instantiated manually as such 222 | let entity2 = ecs.newEntity("Entity 2") 223 | # To add components, use `item.addComponent(component)` or using 224 | # `item`.add()`. `item` is simply a tuple containing 225 | # the ecs and the entity in question. 226 | (ecs, entity2).addComponent(Position(x: 10.0, y: 10.0)) 227 | (ecs, entity2).addVelocity(Velocity(dx: 1.0, dy: -1.0)) 228 | 229 | let item = (ecs, entity2) 230 | # if the call could be ambiguous (such as when using tuples) 231 | # the `` can be explicitly assigned to 232 | item.addComponent(sprite = (id: 42, potato: 12)) 233 | # or just use `item.addSprite((id: 42, potato: 12))` 234 | 235 | item.addCustomFlag(ckTest) 236 | item.addName("SomeNiceName") 237 | item.addValue(10) 238 | item.addDistinctValue(20.DistinctValue) 239 | item.addIsDead(true) 240 | 241 | # Components can be removed as well 242 | item.removeComponent(IsDead) 243 | # item.removeIsDead() 244 | (ecs, entity1).removeIsDead() 245 | 246 | # To access an entity's component, you can call `item.` 247 | item.position.x += 20.0 248 | # If you try to access a component that hasn't been adde to the entity, 249 | # an AssertionDefect will be thrown. 250 | # Since all entities that enters a system has all the components by definition, 251 | # this shouldn't happen unless the component has been removed within the system 252 | # itself and then accessed again after the removal statement. 253 | when false: 254 | item.isDead # would throw exception since it was removed above^ 255 | 256 | echo " == Components of entity2 == " 257 | echo item.position 258 | echo item.velocity 259 | echo item.sprite 260 | echo item.customFlag 261 | echo "..." 262 | 263 | echo "\n == ID of entity1 == " 264 | # The Entity type is simply an integer ID 265 | echo entity1 266 | echo typeof(entity1) 267 | 268 | # You can inspect entities using `ecs.inspect` which will 269 | # return a useful string for debugging when not in release 270 | # mode. The string will contain the label from when the entity 271 | # was instantiated. In release mode, just the ID will be return. 272 | # labels are not saved in release mode in order to save memory 273 | when not defined(release): 274 | echo "\n == ecs.inspect(entity1) == " 275 | echo ecs.inspect(entity1) 276 | 277 | # You call your system groups using `ecs.run()` 278 | echo "\n == Running \"systems\" group 10 times == " 279 | for i in 0 ..< 10: # You would probably use an infinite game loop instead of a for loop.. 280 | ecs.runSystems() 281 | 282 | # You can also call systems individually using `ecs.run()` 283 | echo "\n == Running \"moveSystem\" alone 10 times == " 284 | for i in 0 ..< 10: 285 | ecs.runMoveSystem() 286 | 287 | echo "\n == Running \"rendering\" group once == " 288 | # Note that we have to pass `game: var Game` here! 289 | # Check `renderSpriteSystem` above for details on why ^ 290 | 291 | # `game` currently only has a value, but a more useful 292 | # usage would be to perhaps have a reference to your 293 | # window and/or renderer in the case of a game. That way 294 | # you can still write your rendering logic within a System 295 | doAssert game.value == 0 296 | ecs.runRendering(game) 297 | doAssert game.value == 1 298 | 299 | # You can also query entities using the iterator `queryAll`. 300 | # The following will yield all entities with a `Position` component. 301 | # ckPosition (ck) is a member of a generated enum `ComponentKind`. 302 | echo "\n == Querying {ckPosition} entities == " 303 | for entity in ecs.queryAll({ckPosition}): 304 | echo ecs.inspect(entity) 305 | 306 | (ecs, entity1).removePosition() 307 | echo "\n == Querying {ckPosition} entities after removing Position from entity1 == " 308 | for entity in ecs.queryAll({ckPosition}): 309 | echo ecs.inspect(entity) 310 | 311 | # To get all entities, use the special ComponentKind called `ckExists` 312 | # `ckExists` is added to all entities that have been instantiated 313 | echo "\n == Querying {ckExists} entities == " 314 | for entity in ecs.queryAll({ckExists}): # This is also the default when calling `ecs.queryAll()` or `ecs.queryAll({})` 315 | echo ecs.inspect(entity) 316 | 317 | # The 'query' above is actually known as the entity's `Signature` 318 | # which can be accessed using `entity.getSignature(ecs)` (or `item.getSignature`) 319 | echo (ecs, entity1).getSignature() # {ckExists, ckPosition, ckVelocity, ckSprite, ...} 320 | 321 | # So you can query all entities like another one as such shown below. 322 | # Note that this will, of course, include entity1. 323 | echo "\n == Querying entities that have all of entity1's components or more == " 324 | for entity in ecs.queryAll((ecs, entity1).getSignature()): 325 | echo ecs.inspect(entity) 326 | 327 | # That's all for this example! Generate documentation using `nimble docgen` from the root 328 | # of easyess to get a bit more tecchnical documentation for each and every function and 329 | # template! 330 | 331 | # Try to compile this using `-d:danger` or `-d:release` and see if you can 332 | # notice the difference in the output! 333 | ``` 334 | 335 | ## Documentation 336 | `nimble docgen` from the root of the project will generate HTML documentation. 337 | This is a special task that will include some example components and systems in 338 | order to also show documentation for all of the compileTime-generated procs, 339 | funcs, templates and macros. 340 | 341 | The gist of it is that: 342 | - For each component, the following will be generated at compile time (`Position` used as example): 343 | - An enum `ComponentKind` with names like `ck` (like `ckPosition`) 344 | - Procs to add and remove components: 345 | - `proc add(: )` (like `addPosition(Position(0.0, 0.0))`) 346 | - `proc addComponent(: )` (like `addComponent(position = Position(0.0, 0.0))`) 347 | 348 | - `proc remove` (like `removePosition()`) 349 | - `proc removeComponent[T: ]()` (like `removeComponent[Position]()`) 350 | - a template `template (): ` (like `template position(): Position`) 351 | so that you can do `item.position += vec2(0.1, 0.1)` 352 | 353 | - An `iterator queryAll(ecs: ECS, signature: set[ComponentKind])` 354 | 355 | - For each system, the following will be generated: 356 | - `proc run(ecs: ECS)` like (`proc runLogicSystems(ecs: ECS)`) 357 | - `proc run(ecs: ECS)` like (`proc runPositionSystem(ecs: ECS)`) 358 | 359 | If you want to view everything generated yourself, you can compile with `-d:ecsDebugMacros` 360 | 361 | ## Performance 362 | While easyess provides more than enough performance for my personal needs, I have not 363 | done any extensive profiling or heavy optimizations other than the most obvious ones, 364 | including: 365 | - All internal 'component containers' are static `array[N, ]` 366 | - Entity labels are not stored when compiled with `-d:release` 367 | - Components of smaller types like enums, integers (+ unsigned), chars and bools are supported without any 'Box types'. 368 | If your component is just a char, the internal container representation will be of `array[, char]` 369 | 370 | I want to do a benchmark some time, but I suspect [polymorph](https://github.com/rlipsc/polymorph) will 371 | win over easyess in many aspects. 372 | 373 | ## Limitations 374 | Currently there is no way to have components with generic types since internally only one `array[N, T]` is stored. 375 | This means that only one Kind of `T` can be stored and that would kind of defeat the point of having generics. 376 | I have to generate an enum for each component as well, and I don't know how that would handle generics. 377 | 378 | This is maybe solvable by inheritance or objects utilizing the `case kind of [...]` syntax within an object, but I have not had a need to test and use that. 379 | 380 | ## License 381 | Easyess is released under the MIT license. See `LICENSE` for details. 382 | Copyright (c) 2021-2022 ErikWDev 383 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | # Easyess Todo 2 | - [ ] System Definition 3 | - [X] Generate system definitions systems after ECS type 4 | - [ ] Generate system definitions in same order as originally defined 5 | - [ ] Remove unused data in SystemDefinition container 6 | 7 | - [X] Allow components of any type (Not Generics) 8 | 9 | - [X] Create more entity templates / functions 10 | - [X] addComponent[T](item: (ECS, Entity), c: T) 11 | - [X] removeComponent[T](item: (ECS, Entity), c: typedesc[T]) 12 | - [X] add(item: (ECS, Entity), c: ComponentName) 13 | - [X] remove(item: (ECS, Entity)) 14 | - [X] (item: (ECS, Entity)): ComponentName 15 | - [X] removeEntity(item: (ECS, Entity)) 16 | - [X] removeEntity(ecs: ECS, entity: Entity) 17 | - [X] Keep track of current lowest and highest registered Entity in ECS (optimize loops) 18 | - [X] Add above optimization to `queryAll()` and `newEntity()` 19 | 20 | - [X] Be able to name components in the sys-macro like `(vel: Velocity)` 21 | 22 | - [ ] Only export component procedures and templates for a given Component if it is marked with * 23 | - [ ] Ability to pause and unpause systems 24 | - [ ] Ability to choose to execute groups in parallel? 25 | - [ ] -------------------------------------------------------------------------------- /easyess.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.2.0" 3 | author = "ErikWDev" 4 | description = "A simple ECS - easyess" 5 | license = "MIT" 6 | srcDir = "src" 7 | 8 | # Tasks 9 | task docgen, "Generate docs": 10 | exec "nim doc2 --index:on -d:docgen --docCmd:'-d:docgen' --outdir:htmldocs src/easyess.nim" 11 | 12 | import os 13 | task tests, "Run tests using both -d:release and without": 14 | echo "Running tests in debug mode" 15 | exec "nimble test" 16 | echo "Running tests in release mode" 17 | exec "nimble -d:danger test" 18 | 19 | echo "Checking all examples" 20 | 21 | for kind, path in walkDir("examples"): 22 | let (dir, name, ext) = splitFile(path) 23 | 24 | if ext == ".nim": 25 | exec "nim check " & path 26 | 27 | # Dependencies 28 | requires "nim >= 1.6.0" 29 | -------------------------------------------------------------------------------- /examples/example_01.nim: -------------------------------------------------------------------------------- 1 | 2 | import easyess 3 | 4 | 5 | type 6 | Game = object 7 | value: int 8 | 9 | # Define components using the `comp` macro. Components can have any type 10 | # that doesn't use generics. 11 | comp: 12 | type 13 | Position = object 14 | x: float 15 | y: float 16 | 17 | Velocity = object 18 | dx: float 19 | dy: float 20 | 21 | Sprite = tuple 22 | id: int 23 | potato: int 24 | 25 | TupleComponent = tuple 26 | test: string 27 | 28 | CustomFlag = enum 29 | ckTest 30 | ckPotato 31 | 32 | Name = string 33 | 34 | Value = int 35 | 36 | DistinctValue = distinct int 37 | 38 | IsDead = bool 39 | 40 | # Define systems using the `sys` macro. 41 | # Specify which components are needed using `[Component1, Component2]` and 42 | # the 'group' that this system belongs to using a string. 43 | # The system should be a `proc` or `func` that takes an argument of type `Item` 44 | # The `Item` type is a `tuple[ecs: ECS, entity: Entity]`. 45 | const 46 | systemsGroup = "systems" 47 | renderingGroup = "rendering" 48 | 49 | sys [Position, Velocity], systemsGroup: 50 | func moveSystem(item: Item) = 51 | let 52 | (ecs, entity) = item 53 | oldPosition = position 54 | # Inside your system, templates are defined corresponging to 55 | # the Components that you have requested. `Position` nad `Velocity` 56 | # were requested here, so now 'position' and 'velocity' are available 57 | position.x += velocity.dx 58 | 59 | # You can also do `item.position` explicitly, but it is also a template 60 | item.position.y += item.velocity.dy 61 | when not defined(release): 62 | debugEcho "Moved " & ecs.inspect(entity) & " from ", oldPosition, " to ", position 63 | 64 | 65 | # Systems can have side-effects when marked 66 | # as `proc` and access variables either outside 67 | # the entire `sys` macro or 'within' it, but those 68 | # defined on the inside will still be considered global. 69 | 70 | # You can also pass an extra 'Data' parameter to a system 71 | # by specifying it after the `Item`. You must later provide 72 | # a variable of that same type when you call the system's group! 73 | 74 | var oneGlobalValue = 1 75 | 76 | sys [Sprite], renderingGroup: 77 | var secondGlobalValue = "Renderingwindow" 78 | 79 | proc renderSpriteSystem(item: Item, game: var Game) = 80 | # Note that we request `var Game` here: ^^^^^^^^ 81 | # That means that when we later call `ecs.runRendering()`, 82 | # we will have to supply an extra argument of the same type! 83 | # like so: `ecs.runRendering(game)` 84 | 85 | echo secondGlobalValue, ": Rendering sprite #", sprite.id 86 | inc oneGlobalValue 87 | inc game.value 88 | 89 | # If you want to give your components a different variable 90 | # name within the system, you can do so by specifying it as 91 | # such: `: `. The default name is always 92 | # otherwise`` (first letter lowercase) 93 | sys [dead: IsDead], systemsGroup: 94 | proc isDeadSystem(item: Item) = 95 | echo dead 96 | 97 | sys [CustomFlag], systemsGroup: 98 | proc customFlagSystem(item: Item) = 99 | echo customFlag 100 | 101 | # State machines can be implemented using a single enum as the component! 102 | case customFlag: 103 | of ckTest: customFlag = ckPotato 104 | of ckPotato: customFlag = ckTest 105 | 106 | # Once all components and systems have been defined or 107 | # imported, call `createECS` with a `ECSConfig`. The order 108 | # matters here and `createECS` has to be called AFTER component 109 | # and system definitions. 110 | createECS(ECSConfig(maxEntities: 100)) 111 | 112 | when isMainModule: 113 | # The ecs state can be instantiated using `newECS` (created by `createECS`) 114 | let ecs = newECS() 115 | var game = Game(value: 0) 116 | 117 | # Entities can be instantiated either manually or using the template 118 | # `createEntity` which takes a debug label that will be ignored 119 | # `when defined(release)`, as well as a tuple of Components 120 | # For the template to work with non-object components, the type 121 | # has to be specified within brackets as `[]` 122 | let entity1 = ecs.createEntity("Entity 1"): ( 123 | Position(x: 10.0, y: 0.0), 124 | Velocity(dx: 1.0, dy: -1.0), 125 | 126 | [Sprite](id: 42, potato: 12), 127 | [CustomFlag]ckTest, 128 | [Name]"SomeNiceName", 129 | [Value]10, 130 | [DistinctValue]20, 131 | [IsDead]true 132 | ) 133 | 134 | # Entities can also be instantiated manually as such 135 | let entity2 = ecs.newEntity("Entity 2") 136 | # To add components, use `item.addComponent(component)` or using 137 | # `item`.add()`. `item` is simply a tuple containing 138 | # the ecs and the entity in question. 139 | (ecs, entity2).addComponent(Position(x: 10.0, y: 10.0)) 140 | (ecs, entity2).addVelocity(Velocity(dx: 1.0, dy: -1.0)) 141 | 142 | let item = (ecs, entity2) 143 | # if the call could be ambiguous (such as when using tuples) 144 | # the `` can be explicitly assigned to 145 | item.addComponent(sprite = (id: 42, potato: 12)) 146 | # or just use `item.addSprite((id: 42, potato: 12))` 147 | 148 | item.addCustomFlag(ckTest) 149 | item.addName("SomeNiceName") 150 | item.addValue(10) 151 | item.addDistinctValue(20.DistinctValue) 152 | item.addIsDead(true) 153 | 154 | # Components can be removed as well 155 | item.removeComponent(IsDead) 156 | # item.removeIsDead() 157 | (ecs, entity1).removeIsDead() 158 | 159 | # To access an entity's component, you can call `item.` 160 | item.position.x += 20.0 161 | # If you try to access a component that hasn't been adde to the entity, 162 | # an AssertionDefect will be thrown. 163 | # Since all entities that enters a system has all the components by definition, 164 | # this shouldn't happen unless the component has been removed within the system 165 | # itself and then accessed again after the removal statement. 166 | when false: 167 | item.isDead # would throw exception since it was removed above^ 168 | 169 | echo " == Components of entity2 == " 170 | echo item.position 171 | echo item.velocity 172 | echo item.sprite 173 | echo item.customFlag 174 | echo "..." 175 | 176 | echo "\n == ID of entity1 == " 177 | # The Entity type is simply an integer ID 178 | echo entity1 179 | echo typeof(entity1) 180 | 181 | # You can inspect entities using `ecs.inspect` which will 182 | # return a useful string for debugging when not in release 183 | # mode. The string will contain the label from when the entity 184 | # was instantiated. In release mode, just the ID will be return. 185 | # labels are not saved in release mode in order to save memory 186 | when not defined(release): 187 | echo "\n == ecs.inspect(entity1) == " 188 | echo ecs.inspect(entity1) 189 | 190 | # You call your system groups using `ecs.run()` 191 | echo "\n == Running \"systems\" group 10 times == " 192 | for i in 0 ..< 10: # You would probably use an infinite game loop instead of a for loop.. 193 | ecs.runSystems() 194 | 195 | # You can also call systems individually using `ecs.run()` 196 | echo "\n == Running \"moveSystem\" alone 10 times == " 197 | for i in 0 ..< 10: 198 | ecs.runMoveSystem() 199 | 200 | echo "\n == Running \"rendering\" group once == " 201 | # Note that we have to pass `game: var Game` here! 202 | # Check `renderSpriteSystem` above for details on why ^ 203 | 204 | # `game` currently only has a value, but a more useful 205 | # usage would be to perhaps have a reference to your 206 | # window and/or renderer in the case of a game. That way 207 | # you can still write your rendering logic within a System 208 | doAssert game.value == 0 209 | ecs.runRendering(game) 210 | doAssert game.value == 1 211 | 212 | # You can also query entities using the iterator `queryAll`. 213 | # The following will yield all entities with a `Position` component. 214 | # ckPosition (ck) is a member of a generated enum `ComponentKind`. 215 | echo "\n == Querying {ckPosition} entities == " 216 | for entity in ecs.queryAll({ckPosition}): 217 | echo ecs.inspect(entity) 218 | 219 | (ecs, entity1).removePosition() 220 | echo "\n == Querying {ckPosition} entities after removing Position from entity1 == " 221 | for entity in ecs.queryAll({ckPosition}): 222 | echo ecs.inspect(entity) 223 | 224 | # To get all entities, use the special ComponentKind called `ckExists` 225 | # `ckExists` is added to all entities that have been instantiated 226 | echo "\n == Querying {ckExists} entities == " 227 | for entity in ecs.queryAll({ckExists}): # This is also the default when calling `ecs.queryAll()` or `ecs.queryAll({})` 228 | echo ecs.inspect(entity) 229 | 230 | # The 'query' above is actually known as the entity's `Signature` 231 | # which can be accessed using `entity.getSignature(ecs)` (or `item.getSignature`) 232 | echo (ecs, entity1).getSignature() # {ckExists, ckPosition, ckVelocity, ckSprite, ...} 233 | 234 | # So you can query all entities like another one as such shown below. 235 | # Note that this will, of course, include entity1. 236 | echo "\n == Querying entities that have all of entity1's components or more == " 237 | for entity in ecs.queryAll((ecs, entity1).getSignature()): 238 | echo ecs.inspect(entity) 239 | 240 | # That's all for this example! Generate documentation using `nimble docgen` from the root 241 | # of easyess to get a bit more tecchnical documentation for each and every function and 242 | # template! 243 | 244 | # Try to compile this using `-d:danger` or `-d:release` and see if you can 245 | # notice the difference in the output! 246 | -------------------------------------------------------------------------------- /examples/example_01_no_comments.nim: -------------------------------------------------------------------------------- 1 | 2 | import easyess 3 | 4 | 5 | type 6 | Game = object 7 | value: int 8 | 9 | comp: 10 | type 11 | Position = object 12 | x: float 13 | y: float 14 | 15 | Velocity = object 16 | dx: float 17 | dy: float 18 | 19 | Sprite = tuple 20 | id: int 21 | potato: int 22 | 23 | TupleComponent = tuple 24 | test: string 25 | 26 | CustomFlag = enum 27 | ckTest 28 | ckPotato 29 | 30 | Name = string 31 | 32 | Value = int 33 | 34 | DistinctValue = distinct int 35 | 36 | IsDead = bool 37 | 38 | const 39 | systemsGroup = "systems" 40 | renderingGroup = "rendering" 41 | 42 | sys [Position, Velocity], systemsGroup: 43 | func moveSystem(item: Item) = 44 | let 45 | (ecs, entity) = item 46 | oldPosition = position 47 | position.x += velocity.dx 48 | 49 | item.position.y += item.velocity.dy 50 | when not defined(release): 51 | debugEcho "Moved " & ecs.inspect(entity) & " from ", oldPosition, " to ", position 52 | 53 | 54 | var oneGlobalValue = 1 55 | 56 | sys [Sprite], renderingGroup: 57 | var secondGlobalValue = "Renderingwindow" 58 | 59 | proc renderSpriteSystem(item: Item, game: var Game) = 60 | echo secondGlobalValue, ": Rendering sprite #", sprite.id 61 | inc oneGlobalValue 62 | inc game.value 63 | 64 | sys [dead: IsDead], systemsGroup: 65 | proc isDeadSystem(item: Item) = 66 | echo dead 67 | 68 | sys [CustomFlag], systemsGroup: 69 | proc customFlagSystem(item: Item) = 70 | echo customFlag 71 | 72 | case customFlag: 73 | of ckTest: customFlag = ckPotato 74 | of ckPotato: customFlag = ckTest 75 | 76 | createECS(ECSConfig(maxEntities: 100)) 77 | 78 | when isMainModule: 79 | let ecs = newECS() 80 | var game = Game(value: 0) 81 | 82 | let entity1 = ecs.createEntity("Entity 1"): ( 83 | Position(x: 10.0, y: 0.0), 84 | Velocity(dx: 1.0, dy: -1.0), 85 | 86 | [Sprite](id: 42, potato: 12), 87 | [CustomFlag]ckTest, 88 | [Name]"SomeNiceName", 89 | [Value]10, 90 | [DistinctValue]20, 91 | [IsDead]true 92 | ) 93 | 94 | let entity2 = ecs.newEntity("Entity 2") 95 | (ecs, entity2).addComponent(Position(x: 10.0, y: 10.0)) 96 | (ecs, entity2).addVelocity(Velocity(dx: 1.0, dy: -1.0)) 97 | 98 | let item = (ecs, entity2) 99 | item.addComponent(sprite = (id: 42, potato: 12)) 100 | 101 | item.addCustomFlag(ckTest) 102 | item.addName("SomeNiceName") 103 | item.addValue(10) 104 | item.addDistinctValue(20.DistinctValue) 105 | item.addIsDead(true) 106 | 107 | item.removeComponent(IsDead) 108 | (ecs, entity1).removeIsDead() 109 | 110 | item.position.x += 20.0 111 | when false: 112 | item.isDead 113 | 114 | echo " == Components of entity2 == " 115 | echo item.position 116 | echo item.velocity 117 | echo item.sprite 118 | echo item.customFlag 119 | echo "..." 120 | 121 | echo "\n == ID of entity1 == " 122 | echo entity1 123 | echo typeof(entity1) 124 | 125 | when not defined(release): 126 | echo "\n == ecs.inspect(entity1) == " 127 | echo ecs.inspect(entity1) 128 | 129 | echo "\n == Running \"systems\" group 10 times == " 130 | for i in 0 ..< 10: 131 | ecs.runSystems() 132 | 133 | echo "\n == Running \"moveSystem\" alone 10 times == " 134 | for i in 0 ..< 10: 135 | ecs.runMoveSystem() 136 | 137 | echo "\n == Running \"rendering\" group once == " 138 | doAssert game.value == 0 139 | ecs.runRendering(game) 140 | doAssert game.value == 1 141 | 142 | echo "\n == Querying {ckPosition} entities == " 143 | for entity in ecs.queryAll({ckPosition}): 144 | echo ecs.inspect(entity) 145 | 146 | (ecs, entity1).removePosition() 147 | echo "\n == Querying {ckPosition} entities after removing Position from entity1 == " 148 | for entity in ecs.queryAll({ckPosition}): 149 | echo ecs.inspect(entity) 150 | 151 | echo "\n == Querying {ckExists} entities == " 152 | for entity in ecs.queryAll({ckExists}): 153 | echo ecs.inspect(entity) 154 | 155 | echo (ecs, entity1).getSignature() 156 | 157 | echo "\n == Querying entities that have all of entity1's components or more == " 158 | for entity in ecs.queryAll((ecs, entity1).getSignature()): 159 | echo ecs.inspect(entity) 160 | -------------------------------------------------------------------------------- /examples/minimal.nim: -------------------------------------------------------------------------------- 1 | import easyess 2 | 3 | comp: 4 | type 5 | Position = object 6 | x: float 7 | y: float 8 | 9 | Velocity = object 10 | dx: float 11 | dy: float 12 | 13 | 14 | sys [Position, vel: Velocity], "systems": 15 | func moveSystem(item: Item) = 16 | let 17 | (ecs, entity) = item 18 | oldPosition = position 19 | 20 | position.y += vel.dy 21 | item.position.x += item.velocity.dx 22 | 23 | when not defined(release): 24 | debugEcho "Moved " & ecs.inspect(entity) & " from ", oldPosition, " to ", position 25 | 26 | 27 | createECS(ECSConfig(maxEntities: 100)) 28 | 29 | when isMainModule: 30 | let 31 | ecs = newECS() 32 | entity2 = ecs.newEntity("test") 33 | # entity1 = ecs.createEntity("Entity 1"): ( 34 | # Position(x: 0.0, y: 0.0), 35 | # Velocity(dx: 10.0, dy: -10.0) 36 | # ) 37 | # entity2 = ecs.newEntity("Entity 2") 38 | 39 | (ecs, entity2).addComponent(Position(x: 0.0, y: 0.0)) 40 | (ecs, entity2).addVelocity(Velocity(dx: -10.0, dy: 10.0)) 41 | 42 | for i in 1 .. 10: 43 | ecs.runSystems() 44 | -------------------------------------------------------------------------------- /examples/nim.cfg: -------------------------------------------------------------------------------- 1 | --path: "$projectDir/../src" 2 | -------------------------------------------------------------------------------- /examples/nimraylib_game_3d/nim.cfg: -------------------------------------------------------------------------------- 1 | -d:danger 2 | -------------------------------------------------------------------------------- /examples/nimraylib_game_3d/nimraylib_game_3d.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "ErikWDev" 5 | description = "A new awesome nimble package" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["nimraylib_game_3d"] 9 | binDir = "bin" 10 | 11 | 12 | # Dependencies 13 | 14 | requires "nim >= 1.7.1" 15 | requires "easyess" 16 | 17 | requires "nimraylib_now >= 0.14.2" 18 | -------------------------------------------------------------------------------- /examples/nimraylib_game_3d/src/nimraylib_game_3d.nim: -------------------------------------------------------------------------------- 1 | 2 | import nimraylib_now 3 | import easyess 4 | 5 | import std/[random] 6 | 7 | func vec3(x, y, z: float): Vector3 {.inline.} = 8 | Vector3(x: x, y: y, z: z) 9 | 10 | 11 | type 12 | Game = ref object 13 | camera: Camera3D 14 | deltaTime: float 15 | 16 | comp: 17 | type 18 | Position = Vector3 19 | 20 | Velocity = Vector3 21 | 22 | Sphere = object 23 | radius: float 24 | 25 | 26 | sys [Position, Velocity], "logicSystems": 27 | proc moveSystem(item: Item, game: Game) = 28 | position += velocity * game.deltaTime * 10.0 29 | velocity *= max(min(1.0, game.deltaTime * 8.0), 0.98) 30 | 31 | if abs(velocity.x) < 0.1 and abs(velocity.y) < 0.1 and abs(velocity.z) < 0.1: 32 | velocity = vec3(rand(-1.0 .. 1.0), rand(-1.0 .. 1.0), rand(-1.0 .. 1.0)) 33 | 34 | 35 | sys [Position, Sphere], "renderSystems": 36 | proc renderSphereSystem(item: Item) = 37 | drawSphereWires(position, sphere.radius, 5, 5, RED) 38 | 39 | 40 | createECS(ECSConfig(maxEntities: 5000)) 41 | 42 | 43 | const n = 2500 44 | 45 | proc main() = 46 | setConfigFlags(MSAA_4X_HINT or WINDOW_RESIZABLE) 47 | 48 | initWindow(800, 800, "3D game") 49 | setTargetFPS 0 50 | 51 | let ecs = newECS() 52 | 53 | for i in 0 .. n: 54 | discard ecs.createEntity("Sphere"): ( 55 | Sphere(radius: rand(0.1 .. 0.6)), 56 | [Position]vec3(0.0, 0.0, 0.0), 57 | [Velocity]vec3(rand(-1.0 .. 1.0), rand(-1.0 .. 1.0), rand(-1.0 .. 1.0)), 58 | ) 59 | 60 | let game = Game.new() 61 | 62 | game.camera = Camera3D( 63 | position: vec3(0.0, 10.0, 10.0), 64 | target: vec3(0.0, 0.0, 0.0), 65 | up: vec3(0.0, 1.0, 0.0), 66 | fovy: 95.0, 67 | projection: PERSPECTIVE 68 | ) 69 | game.camera.setCameraMode(CameraMode.ORBITAL) 70 | 71 | while not windowShouldClose(): 72 | game.deltaTime = getFrameTime() 73 | updateCamera(game.camera.addr) 74 | 75 | ecs.runLogicSystems(game) 76 | 77 | beginDrawing(): 78 | clearBackground(Color(r: 12, g: 12, b: 12)) 79 | 80 | beginMode3D(game.camera): 81 | ecs.runRenderSystems() 82 | drawGrid(100, 1.0) 83 | 84 | drawFPS 10, 10 85 | 86 | closeWindow() 87 | 88 | 89 | when isMainModule: 90 | main() 91 | -------------------------------------------------------------------------------- /examples/rapid_game/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore all 3 | * 4 | 5 | # Unignore all with extensions 6 | !*.* 7 | 8 | # Unignore all dirs 9 | !*/ 10 | 11 | *.out 12 | *.exe 13 | *.bin 14 | 15 | bin/ 16 | htmldocs/ 17 | env.json 18 | !LICENSE 19 | !README 20 | !TODO 21 | -------------------------------------------------------------------------------- /examples/rapid_game/rapid_game.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "ErikWDev" 5 | description = "Simple example using rapid and easyess" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["rapid_game"] 9 | binDir = "bin" 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.6.4" 14 | requires "easyess" 15 | 16 | requires "https://github.com/liquidev/rapid#a50704e542987dc9cb9456e481f8f631e885c56a" 17 | 18 | -------------------------------------------------------------------------------- /examples/rapid_game/src/rapid_game.nim: -------------------------------------------------------------------------------- 1 | import aglet 2 | import aglet/window/glfw 3 | import rapid/graphics 4 | import glm 5 | 6 | import easyess 7 | 8 | comp: 9 | type 10 | Position = Vec2[float] 11 | 12 | Velocity = Vec2[float] 13 | 14 | sys [Position, Velocity], "logicSystems": 15 | proc positionVelocitySystem(item: Item) = 16 | echo position 17 | echo velocity 18 | 19 | position += velocity 20 | 21 | sys [vel: Velocity], "logicSystems": 22 | proc velocitySystem(item: Item) = 23 | vel *= 0.8 24 | 25 | createECS(ECSConfig(maxEntities: 1200)) 26 | 27 | proc main() = 28 | var agl = initAglet() 29 | agl.initWindow() 30 | 31 | let 32 | window = agl.newWindowGlfw(800, 600, "rapid/gfx", winHints(msaaSamples = 8)) 33 | graphics = window.newGraphics() 34 | 35 | const bg = rgba(0.125, 0.125, 0.125, 1.0) 36 | 37 | var world = newECS() 38 | 39 | discard world.createEntity("Player"): ( 40 | [Position]vec2(0.0, 0.0), 41 | [Velocity]vec2(10.0, 10.0) 42 | ) 43 | 44 | while not window.closeRequested: 45 | window.pollEvents do (event: InputEvent): 46 | case event.kind 47 | of iekWindowFrameResize: 48 | echo event.size 49 | 50 | of iekKeyPress: 51 | echo event 52 | case event.key 53 | of keyEsc: window.requestClose() 54 | else: discard 55 | 56 | else: discard 57 | 58 | var frame = window.render() 59 | frame.clearColor(bg) 60 | frame.finish() 61 | 62 | world.runLogicSystems() 63 | 64 | 65 | when isMainModule: 66 | main() 67 | -------------------------------------------------------------------------------- /examples/snakelike_rapid_game/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Ignore all 3 | * 4 | 5 | # Unignore all with extensions 6 | !*.* 7 | 8 | # Unignore all dirs 9 | !*/ 10 | 11 | *.out 12 | *.exe 13 | *.bin 14 | 15 | bin/ 16 | htmldocs/ 17 | env.json 18 | !LICENSE 19 | !README 20 | !TODO 21 | -------------------------------------------------------------------------------- /examples/snakelike_rapid_game/README.md: -------------------------------------------------------------------------------- 1 | # Snake-like game 2 | 3 | Run using `nimble run` 4 | 5 | Controls are arrows keys or WASD. You cannot die. Become huge! It's fun! 6 | Being able to move diagonally is a feature, not a bug ;) 7 | -------------------------------------------------------------------------------- /examples/snakelike_rapid_game/rapid_game.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "ErikWDev" 5 | description = "A new awesome nimble package" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["rapid_game"] 9 | binDir = "bin" 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.6.4" 14 | requires "https://github.com/liquidev/rapid#a50704e542987dc9cb9456e481f8f631e885c56a" 15 | 16 | requires "easyess" 17 | -------------------------------------------------------------------------------- /examples/snakelike_rapid_game/src/rapid_game.nim: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import aglet 4 | import aglet/window/glfw 5 | import rapid/graphics 6 | import glm 7 | 8 | import easyess 9 | 10 | type 11 | InputKind = enum 12 | ikMoveUp, ikMoveDown, ikMoveLeft, ikMoveRight 13 | 14 | Game = object 15 | graphics: Graphics 16 | input: set[InputKind] 17 | 18 | # === Components === 19 | when true: 20 | comp: 21 | type 22 | Position = Vec2[float] 23 | 24 | Velocity = Vec2[float] 25 | 26 | IsMove = bool 27 | DidGrow = bool 28 | 29 | GridPosition = Vec2[int] 30 | GridCollider = bool 31 | 32 | Direction = enum 33 | dNone, dUp, dDown, dLeft, dRight 34 | 35 | NextDirection = Direction 36 | NextGridPosition = GridPosition 37 | 38 | Player = bool 39 | Apple = bool 40 | TailReference = Entity 41 | HeadReference = Entity 42 | 43 | Snake = int 44 | Tail = bool 45 | NextSnake = Entity 46 | 47 | Explosion = int 48 | Particle = Color 49 | 50 | const 51 | gridSize = 12 52 | gridSizeFloat = gridSize.toFloat() 53 | 54 | func manhattanDistance(a, b: Vec2[int]): int {.inline.} = 55 | return abs(a.x - b.x) + abs(a.y - b.y) 56 | 57 | # === Systems === 58 | when true: 59 | # === Logic Systems === 60 | when true: 61 | sys [Explosion, Position], "logicSystems": 62 | proc createExmplosion(item: Item) = 63 | for i in 0 .. explosion: 64 | let 65 | color = rgba(rand(0.0 .. 1.0), rand(0.0 .. 1.0), rand(0.0 .. 1.0), 0.4) 66 | s = 10.0 67 | velocity = vec2(rand(-s .. s), rand(-s .. s)) 68 | 69 | discard item.ecs.createEntity("Particle"): ( 70 | [Particle]color, 71 | [Position]position, 72 | [Velocity]velocity 73 | ) 74 | 75 | item.ecs.removeEntity(item.entity) 76 | 77 | sys [Position, Velocity], "logicSystems": 78 | proc positionVelocitySystem(item: Item) = 79 | position += velocity 80 | 81 | sys [vel: Velocity], "logicSystems": 82 | proc velocitySystem(item: Item) = 83 | vel *= 0.97 84 | 85 | sys [Snake], "logicSystems": 86 | proc isMoveSystem(item: Item, game: Game) = 87 | if card({ikMoveUp, ikMoveDown, ikMoveLeft, ikMoveRight} * game.input) != 0: 88 | item.addIsMove(true) 89 | 90 | sys [nextPos: NextGridPosition, NextDirection, NextSnake, IsMove], "logicSystems": 91 | proc setNextDirectionSystem(item: Item, game: Game) = 92 | nextDirection = (item.ecs, nextSnake).direction 93 | nextPos = (item.ecs, nextSnake).gridPosition 94 | 95 | sys [pos: GridPosition, Direction, Player, IsMove, tail: TailReference], "logicSystems": 96 | proc playerMoveSystem(item: Item, game: Game) = 97 | var delta = vec2(0, 0) 98 | 99 | if ikMoveUp in game.input: delta.y -= 1 100 | if ikMoveDown in game.input: delta.y += 1 101 | if ikMoveLeft in game.input: delta.x -= 1 102 | if ikMoveRight in game.input: delta.x += 1 103 | 104 | pos += delta 105 | 106 | var newDirection: Direction 107 | if delta.x > 0: newDirection = dRight 108 | elif delta.x < 0: newDirection = dLeft 109 | elif delta.y > 0: newDirection = dDown 110 | elif delta.y < 0: newDirection = dUp 111 | 112 | direction = newDirection 113 | 114 | for apple in item.ecs.queryAll({ckApple, ckGridPosition}): 115 | if manhattanDistance((item.ecs, apple).gridPosition, pos) <= 2: 116 | (item.ecs, tail).addDidGrow(true) 117 | discard item.ecs.createEntity("Explosion"): ( 118 | [Explosion]20, 119 | [Position]vec2(pos.x.toFloat(), pos.y.toFloat()) * gridSizeFloat 120 | ) 121 | 122 | item.ecs.removeEntity(apple) 123 | 124 | discard item.ecs.createEntity("Apple"): ( 125 | [Apple]true, 126 | [GridPosition]vec2(rand(0 .. 50), rand(0 .. 50)) 127 | ) 128 | 129 | break 130 | 131 | sys [pos: GridPosition, Direction, Tail, DidGrow, head: HeadReference], "logicSystems": 132 | proc createNewTailSystem(item: Item) = 133 | let newTail = item.ecs.createEntity("Body"): ( 134 | [Snake]rand(0 .. 10), 135 | [Tail]true, 136 | [NextSnake]item.entity, 137 | [HeadReference]head, 138 | [Direction]direction, 139 | [NextDirection]dNone, 140 | [GridPosition]pos, 141 | [NextGridPosition]pos 142 | ) 143 | 144 | item.ecs.tailReferenceContainer[head.idx] = newTail 145 | 146 | item.removeTail() 147 | item.removeHeadReference() 148 | 149 | sys [pos: GridPosition, nextPos: NextGridPosition, Direction, NextDirection, IsMove], "logicSystems": 150 | proc snakeBodyMoveSystem(item: Item) = 151 | direction = nextDirection 152 | pos = nextPos 153 | 154 | sys [pos: GridPosition, IsMove], "logicSystems": 155 | proc wrapSystem(item: Item) = 156 | if pos.x < 0: 157 | pos.x = 50 158 | if pos.x > 50: 159 | pos.x = 0 160 | 161 | if pos.y < 0: 162 | pos.y = 50 163 | if pos.y > 50: pos.y = 0 164 | 165 | sys [IsMove], "logicSystems": 166 | proc removeIsMoveSystem(item: Item) = item.removeIsMove() 167 | 168 | sys [Particle, Velocity], "logicSystems": 169 | proc removeParticleSystem(item: Item) = 170 | if length(velocity) <= 0.3: 171 | item.ecs.removeEntity(item.entity) 172 | 173 | # === Rendering Systems === 174 | when true: 175 | sys [pos: GridPosition, Snake], "renderingSystems": 176 | proc bodyRenderingSystem(item: Item, game: Game) = 177 | var canvas = game.graphics 178 | 179 | let 180 | x = (pos.x * gridSize).toFloat() 181 | y = (pos.y * gridSize).toFloat() 182 | w = gridSizeFloat 183 | n1 = snake.toFloat()/10.0 184 | n2 = 1.0 - n1 185 | 186 | canvas.rectangle(x, y, w, w, rgba(n1, n1, n2, 1)) 187 | 188 | sys [pos: Position, Particle], "renderingSystems": 189 | proc renderParticleSystem(item: Item, game: Game) = 190 | game.graphics.circle(pos.x, pos.y, 3.0, particle, 6) 191 | 192 | sys [pos: GridPosition, Apple], "renderingSystems": 193 | proc appleRenderingSystem(item: Item, game: Game) = 194 | var canvas = game.graphics 195 | 196 | let 197 | x = (pos.x * gridSize).toFloat() 198 | y = (pos.y * gridSize).toFloat() 199 | w = gridSizeFloat 200 | 201 | canvas.circle(x, y, w, rgba(1, 0, 0, 0.4), 16) 202 | 203 | sys [pos: GridPosition, Player], "renderingSystems": 204 | proc playerRenderingSystem(item: Item, game: Game) = 205 | var canvas = game.graphics 206 | 207 | let 208 | x = (pos.x * gridSize).toFloat() 209 | y = (pos.y * gridSize).toFloat() 210 | 211 | canvas.circle(x + gridSizeFloat/2.0, y + gridSizeFloat/2.0, 10, rgba(1, 1, 0, 1)) 212 | 213 | createECS(ECSConfig(maxEntities: 15000)) 214 | 215 | 216 | proc createPlayer(ecs: var ECS): Entity = 217 | let origin = vec2(10, 10) 218 | 219 | let player = ecs.createEntity("Player [Head]"): ( 220 | [Player]true, 221 | [Snake]rand(0 .. 10), 222 | [Direction]dUp, 223 | [GridPosition]origin + vec2(0, 0) 224 | ) 225 | 226 | let body = ecs.createEntity("Body"): ( 227 | [Snake]rand(0 .. 10), 228 | [NextSnake]player, 229 | [Direction]dDown, 230 | [NextDirection]dUp, 231 | [GridPosition]origin + vec2(0, -1), 232 | [NextGridPosition]vec2(0, 0) 233 | ) 234 | 235 | let tail = ecs.createEntity("Body"): ( 236 | [Snake]rand(0 .. 10), 237 | [Tail]true, 238 | [NextSnake]body, 239 | [HeadReference]player, 240 | [Direction]dDown, 241 | [NextDirection]dDown, 242 | [GridPosition]origin + vec2(0, -2), 243 | [NextGridPosition]vec2(0, 0) 244 | ) 245 | 246 | (ecs, player).addTailReference(tail) 247 | 248 | return player 249 | 250 | 251 | proc main() = 252 | var agl = initAglet() 253 | agl.initWindow() 254 | 255 | let 256 | window = agl.newWindowGlfw(800, 600, "rapid/gfx", winHints(msaaSamples = 8)) 257 | graphics = window.newGraphics() 258 | 259 | const bg = rgba(0.125, 0.125, 0.125, 1.0) 260 | 261 | var 262 | game = Game(graphics: graphics) 263 | world = newECS() 264 | 265 | 266 | discard world.createPlayer() 267 | discard world.createEntity("Applce"): ( 268 | [Apple]true, 269 | [GridPosition]vec2(20, 20), 270 | ) 271 | 272 | while not window.closeRequested: 273 | window.pollEvents do (event: InputEvent): 274 | case event.kind 275 | of iekWindowFrameResize: 276 | # echo event.size 277 | discard 278 | 279 | of iekKeyPress: 280 | case event.key 281 | of Key.keyEsc: window.requestClose() 282 | 283 | of keyUp, Key.keyW: 284 | game.input.incl(ikMoveUp) 285 | of keyDown, Key.keyS: 286 | game.input.incl(ikMoveDown) 287 | of keyLeft, Key.keyA: 288 | game.input.incl(ikMoveLeft) 289 | of keyRight, Key.keyD: 290 | game.input.incl(ikMoveRight) 291 | else: discard 292 | 293 | of iekKeyRelease: 294 | case event.key 295 | of keyUp, Key.keyW: 296 | game.input.excl(ikMoveUp) 297 | of keyDown, Key.keyS: 298 | game.input.excl(ikMoveDown) 299 | of keyLeft, Key.keyA: 300 | game.input.excl(ikMoveLeft) 301 | of keyRight, Key.keyD: 302 | game.input.excl(ikMoveRight) 303 | else: discard 304 | 305 | else: discard 306 | 307 | world.runLogicSystems(game) 308 | 309 | var frame = window.render() 310 | frame.clearColor(bg) 311 | 312 | graphics.resetShape() 313 | world.runRenderingSystems(game) 314 | 315 | graphics.draw(frame) 316 | frame.finish() 317 | 318 | 319 | when isMainModule: 320 | main() 321 | -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --gc: orc 2 | # --showAllMismatches: on 3 | --threads: on 4 | --experimental: strictEffects 5 | --hints: off 6 | # -d: nimPreviewFloatRoundtrip 7 | # -d: nimPreviewDotLikeOps 8 | -------------------------------------------------------------------------------- /src/easyess.nim: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Easyess 4 | # Copyright (c) 2022 ErikWDev 5 | # 6 | # See the file "LICENSE", included in this 7 | # distribution, for details about the copyright. 8 | # 9 | 10 | 11 | ##[ 12 | 13 | :Author: ErikWDev (Erik W. Gren) 14 | :Copyright: 2021-2022 15 | 16 | 17 | The `easyess` module aims to provive a basic ECS setup for nim with 18 | macros and templates in order to abstract away the implementation 19 | details with minimal effect on performance. 20 | 21 | `easyess` is still under active development. For a more complete, mature 22 | and flexible setup I recommend the package [polymorph](https://github.com/rlipsc/polymorph). 23 | 24 | While the name `easyess` might seem terrible for searchability (and it is..), I still argue 25 | its usefulness. You can nom create a file named `ecs.nim` within which you do `import esasyess`, 26 | define all your components and systems and call `createECS `_ 27 | which generates a bunch of code that is exported from the file. You can the simply 28 | `import ecs` within your other modules without any name conflict with this package. 29 | 30 | Note on Docs 31 | ============ 32 | Since the toplevel exported parts of `easyess` are just three macros, I thought it much more helpful to provide 33 | documentation on all the code that is generated at compiletime once you actually use the package within your code. 34 | 35 | In order to show all functions, procs, templates and macros that are generated in the end 36 | by the `createECS `_ macro, these docs were generated with the following code present. 37 | 38 | .. code-block:: nim 39 | import easyess 40 | 41 | comp: 42 | type 43 | ExampleComponent* = object ## \ 44 | ## An example component for documentation purposes 45 | data: int 46 | 47 | DocumentationComponent* = tuple ## \ 48 | ## An example component for documentation purposes 49 | data: float 50 | 51 | ExampleFlag* = enum ## \ 52 | ## An example component for documentation purposes 53 | efOne 54 | efTwo 55 | 56 | ExampleID* = uint16 ## \ 57 | ## An example component for documentation purposes 58 | 59 | sys [ExampleComponent, DocumentationComponent, ExampleFlag], "exampleSystems": 60 | func exampleSystem(item: Item) = 61 | let (ecs, entity) = item 62 | 63 | discard exampleComponent.data 64 | discard documentationComponent.data 65 | discard exampleFlag 66 | 67 | sys [ExampleID], "exampleSystems": 68 | func exampleIDSystem(item: Item) = 69 | discard exampleID 70 | 71 | createECS(ECSConfig(maxEntities: 100)) 72 | 73 | ]## 74 | 75 | when not defined(docgen): 76 | import ./easyess/core 77 | 78 | export core 79 | 80 | else: 81 | include ./easyess/core 82 | 83 | comp: 84 | type 85 | ExampleComponent* = object ## \ 86 | ## An example component for documentation purposes 87 | data: int 88 | 89 | DocumentationComponent* = tuple ## \ 90 | ## An example component for documentation purposes 91 | data: float 92 | 93 | ExampleFlag* = enum ## \ 94 | ## An example component for documentation purposes 95 | efOne 96 | efTwo 97 | 98 | ExampleID* = uint16 ## \ 99 | ## An example component for documentation purposes 100 | 101 | sys [ExampleComponent, DocumentationComponent, ExampleFlag], "exampleSystems": 102 | func exampleSystem(item: Item) = 103 | let (ecs, entity) = item 104 | 105 | discard exampleComponent.data 106 | discard documentationComponent.data 107 | discard exampleFlag 108 | 109 | sys [ExampleID], "exampleSystems": 110 | func exampleIDSystem(item: Item) = 111 | discard exampleID 112 | 113 | createECS(ECSConfig(maxEntities: 100)) 114 | -------------------------------------------------------------------------------- /src/easyess/core.nim: -------------------------------------------------------------------------------- 1 | import macros, strutils, strformat, tables 2 | export macros, tables 3 | 4 | type 5 | BaseIDType* = distinct uint16 ## \ 6 | ## The integer kind used for IDs. Currently `uint16` is used which 7 | ## allows for 65 535 entities alive simultaniously. If more are 8 | ## required (or less), change this type manually within your project. 9 | 10 | Entity* = distinct BaseIDType ## \ 11 | ## Entities are simply distinct IDs without any behaviour 12 | ## or data attached. Data is added using Components and 13 | ## behaviour is added using Systems. See `comp` and `sys` macro 14 | ## for more details on those. 15 | 16 | ECSConfig* = object 17 | ## A configureation that can be specified statically to 18 | ## `createECS `_ to determine the settings of the `ECS `_ 19 | maxEntities*: int 20 | 21 | ComponentDefinition = tuple 22 | ## Internal 23 | name: NimNode 24 | body: NimNode 25 | 26 | SystemDefinition = tuple 27 | ## Internal 28 | name: NimNode 29 | signature: NimNode 30 | components: NimNode 31 | entireSystem: NimNode 32 | dataType: NimNode 33 | itemName: NimNode 34 | 35 | const ecsDebugMacros = false or defined(ecsDebugMacros) 36 | 37 | func firstLetterLower(word: string): string = 38 | word[0..0].toLower() & word[1..^1] 39 | 40 | func firstLetterUpper(word: string): string = 41 | word[0..0].toUpper() & word[1..^1] 42 | 43 | func toComponentKindName(word: string): string = 44 | "ck" & firstLetterUpper(word) 45 | 46 | func toContainerName(word: string): string = 47 | firstLetterLower(word) & "Container" 48 | 49 | template declareOps(typ: typedesc) = 50 | proc `inc`*(x: var typ) {.borrow.} 51 | proc `dec`*(x: var typ) {.borrow.} 52 | 53 | proc `+` *(x, y: typ): typ {.borrow.} 54 | proc `-` *(x, y: typ): typ {.borrow.} 55 | 56 | proc `<`*(x, y: typ): bool {.borrow.} 57 | proc `<=`*(x, y: typ): bool {.borrow.} 58 | proc `==`*(x, y: typ): bool {.borrow.} 59 | 60 | declareOps(BaseIDType) 61 | declareOps(Entity) 62 | 63 | var 64 | systemDefinitions {.compileTime.}: Table[string, seq[SystemDefinition]] 65 | componentDefinitions {.compileTime.}: seq[ComponentDefinition] 66 | numberOfComponents {.compileTime.} = 0 67 | 68 | entityName {.compileTime.} = ident("entity") 69 | itemType {.compileTime.} = ident("Item") 70 | ecsType {.compileTime.} = ident("ECS") 71 | ecsName {.compileTime.} = ident("ecs") 72 | 73 | macro comp*(body: untyped) = 74 | ## Define one or more components. Components can be of any type, but remember 75 | ## that the type's mutability will be reflected in the way the component 76 | ## can be accessed and manipulated within systems. 77 | ## 78 | ## **Example** 79 | ## 80 | ## .. code-block:: nim 81 | ## 82 | ## import esasyess 83 | ## 84 | ## comp: 85 | ## type 86 | ## ExampleComponent* = object ## \ 87 | ## ## Note that in order to export you Components 88 | ## ## you, as usual, have to explicitly mark the 89 | ## ## type with `*` 90 | ## data: int 91 | ## 92 | ## TupleComponent* = tuple 93 | ## data: string 94 | ## data2: int 95 | ## 96 | ## EnumComponent* = enum 97 | ## ecFlagOne 98 | ## ecFlagTwo 99 | ## 100 | ## Health* = distinct int 101 | ## 102 | ## InternalUnexportedFlag = distinct bool 103 | 104 | for typeSectionChild in body: 105 | for typeDefChild in typeSectionChild: 106 | typeDefChild.expectKind(nnkTypeDef) 107 | 108 | block typeDefLoop: 109 | for postfixOrIdent in typeDefChild: 110 | case postfixOrIdent.kind 111 | of nnkPostfix: 112 | for identifier in postfixOrIdent: 113 | if identifier.eqIdent("*"): 114 | continue 115 | 116 | let componentDefinition: ComponentDefinition = ( 117 | name: identifier, 118 | body: typeDefChild 119 | ) 120 | componentDefinitions.add(componentDefinition) 121 | inc numberOfComponents 122 | 123 | break typeDefLoop 124 | 125 | of nnkIdent: 126 | let componentDefinition: ComponentDefinition = ( 127 | name: postfixOrIdent, 128 | body: typeDefChild 129 | ) 130 | componentDefinitions.add(componentDefinition) 131 | inc numberOfComponents 132 | 133 | break typeDefLoop 134 | 135 | else: 136 | error(&"'{postfixOrIdent.kind}' has to be 'Identifier' or 'Postfix NimNodeKind' ", postfixOrIdent) 137 | 138 | result = body 139 | when ecsDebugMacros: echo repr(result) 140 | 141 | macro sys*(components: untyped; 142 | group: static[string]; 143 | system: untyped) = 144 | ## Define a system. Systems are defined by what components they wish to work on. 145 | ## Components are specified using an openArray of their typedescs, i.e.: 146 | ## `[, ]`. The group is a string to which this system 147 | ## belongs to. Once `createECS `_ has been called, a run procedure is generated 148 | ## for every system group. These groups can then be called using `ecs.run()` 149 | ## 150 | ## Systems are called using an `item`. The item is a `tuple[ecs: ECS, entity: Entity]`. Inside 151 | ## each system, templates are generated for accessing the specified components. If a component's 152 | ## name was `Position`, a template called `template position(): Position` will be generated 153 | ## inside the system's scope. 154 | ## 155 | ## **Example** 156 | ## 157 | ## .. code-block:: nim 158 | ## 159 | ## import easyess 160 | ## comp: 161 | ## type 162 | ## Component = object 163 | ## data: int 164 | ## 165 | ## sys [Component], "mySystemGroup": 166 | ## proc componentSysem(item: Item) = 167 | ## let (ecs, entity) = item 168 | ## inc component.data 169 | ## 170 | ## when not defined(release): 171 | ## debugEcho ecs.inspect(entity) & $component 172 | ## 173 | ## createECS() 174 | ## let 175 | ## ecs = newEcs() 176 | ## entity = ecs.createEntity("Entity"): (Component(data: 42)) 177 | ## ecs.runMySystemGroup() 178 | 179 | 180 | var 181 | itemName: NimNode = newNilLit() 182 | dataName: NimNode = newNilLit() 183 | dataType: NimNode = newNilLit() 184 | 185 | systemName: NimNode 186 | systemBody: NimNode 187 | 188 | let 189 | systemsignature = newNimNode(nnkCurly) 190 | beforeSystem = newNimNode(nnkStmtList) 191 | containerTemplates = newNimNode(nnkStmtList) 192 | componentTypes: NimNode = newNimNode(nnkStmtList) 193 | 194 | var isFunc = true 195 | 196 | block doneParsing: 197 | for c1 in system: 198 | if c1.kind in {nnkProcDef, nnkFuncDef}: 199 | isFunc = c1.kind == nnkFuncDef 200 | for c2 in c1: 201 | case c2.kind 202 | of nnkIdent: 203 | systemName = c2 204 | 205 | of nnkFormalParams: 206 | for c3 in c2: 207 | if c3.kind == nnkIdentDefs: 208 | for i, c4 in c3: 209 | if i == 0 and itemName.kind == nnkNilLit: 210 | itemName = c4 211 | break 212 | 213 | elif i == 0 and dataName.kind == nnkNilLit: 214 | dataName = c4 215 | 216 | elif i == 1 and dataType.kind == nnkNilLit: 217 | dataType = c4 218 | break 219 | 220 | of nnkStmtList: 221 | systemBody = c2 222 | break doneParsing 223 | 224 | else: discard 225 | else: 226 | beforeSystem.add(c1) 227 | 228 | for component in components: 229 | var 230 | componentName = "" 231 | componentTypeName = "" 232 | 233 | case component.kind 234 | of nnkIdent: 235 | componentName = firstLetterLower($component) 236 | componentTypeName = $component 237 | of nnkExprColonExpr: 238 | var i = 0 239 | for node in component.children: 240 | if i == 0: componentName = $node 241 | elif i == 1: componentTypeName = $node 242 | inc i 243 | else: 244 | error(&"Unsupported system component list kind '{component.kind}'") 245 | 246 | systemsignature.add(ident(toComponentKindName(componentTypeName))) 247 | 248 | let 249 | cn = toContainerName(componentTypeName) 250 | templateName = ident(componentName) 251 | containerName = ident(cn) 252 | componentIdent = ident(componentTypeName) 253 | templateComment = newCommentStmtNode(&"Expands to `ecs.{cn}[entity.idx]`") 254 | 255 | componentTypes.add(componentIdent) 256 | 257 | containerTemplates.add quote do: 258 | template `templateName`(): `componentIdent` = 259 | `templateComment` 260 | `itemName`.`ecsName`.`containerName`[`itemName`.`entityName`.idx] 261 | 262 | let key = $group 263 | if key notin systemDefinitions: 264 | systemDefinitions[key] = @[] 265 | 266 | let entireSystem = newNimNode(nnkStmtList) 267 | 268 | entireSystem.add quote do: 269 | `beforeSystem` 270 | 271 | if isFunc: 272 | if dataName.kind == nnkNilLit: 273 | entireSystem.add quote do: 274 | func `systemName`*(`itemName`: `itemType`) = 275 | `containerTemplates` 276 | `systemBody` 277 | else: 278 | entireSystem.add quote do: 279 | func `systemName`*(`itemName`: `itemType`; `dataName`: `dataType`) = 280 | `containerTemplates` 281 | `systemBody` 282 | else: 283 | if dataName.kind == nnkNilLit: 284 | entireSystem.add quote do: 285 | proc `systemName`*(`itemName`: `itemType`) = 286 | `containerTemplates` 287 | `systemBody` 288 | else: 289 | entireSystem.add quote do: 290 | proc `systemName`*(`itemName`: `itemType`; `dataName`: `dataType`) = 291 | `containerTemplates` 292 | `systemBody` 293 | 294 | let systemDefinition: SystemDefinition = ( 295 | name: systemName, 296 | signature: systemsignature, 297 | components: componentTypes, 298 | entireSystem: entireSystem, 299 | dataType: dataType, 300 | itemName: itemName 301 | ) 302 | systemDefinitions[key].add(systemDefinition) 303 | 304 | # when ecsDebugMacros: echo repr(entireSystem) 305 | 306 | macro createECS*(config: static[ECSConfig] = ECSConfig(maxEntities: 100)) = 307 | ## Generate all procedures, functions, templates, types and macros for 308 | ## all components and systems defined until this point in the code. 309 | ## 310 | ## After `createECS `_ has been called, it should **NOT** be called again. 311 | ## 312 | ## You can statically specify a `ECSConfig` with `maxEntities: int` that 313 | ## will determine the internal size of all arrays and will be the upper limit 314 | ## of the number of entities that can be alive at the same time. 315 | result = newNimNode(nnkStmtList) 316 | 317 | let 318 | existsComponentKind = ident(toComponentKindName("exists")) 319 | 320 | componentKindType = ident("ComponentKind") 321 | signatureType = ident("Signature") 322 | 323 | inspectLabelName = ident(toContainerName("ecsInspectLabel")) 324 | signaturesName = ident(toContainerName("signature")) 325 | usedLabelsName = ident("usedLabels") 326 | componentName = ident("component") 327 | highestIDName = ident("highestID") 328 | releaseName = ident("release") 329 | nextIDName = ident("nextID") 330 | queryName = ident("query") 331 | itemName = ident("item") 332 | kindName = ident("kind") 333 | idName = ident("id") 334 | 335 | maxEntities = config.maxEntities 336 | enumType = newNimNode(nnkEnumTy).add(newEmptyNode()) 337 | setBasedOnComponent = newStmtList() 338 | containerDefs = newNimNode(nnkRecList) 339 | 340 | toStringName = nnkAccQuoted.newTree( 341 | newIdentNode("$") 342 | ) 343 | 344 | result.add quote do: 345 | template idx*(`entityName`: Entity): int = 346 | ## Get the ID of `entity` 347 | `entityName`.int 348 | 349 | func `toStringName`*(`entityName`: Entity): string = 350 | ## Get a string representation of `entity`. Note that this representation cannot 351 | ## include the `entity`'s label since it is stored within the `ECS `_. 352 | ## See `inspect `_ for a string representation 353 | ## with the label included. 354 | result = $`entityName`.idx 355 | 356 | containerDefs.add nnkIdentDefs.newTree( 357 | nnkPostfix.newTree(ident("*"), nextIDName), 358 | ident("Entity"), 359 | newEmptyNode()) 360 | 361 | containerDefs.add nnkIdentDefs.newTree( 362 | nnkPostfix.newTree(ident("*"), highestIDName), 363 | ident("Entity"), 364 | newEmptyNode()) 365 | 366 | containerDefs.add nnkRecWhen.newTree( 367 | nnkElifBranch.newTree( 368 | nnkPrefix.newTree( 369 | ident("not"), 370 | nnkCall.newTree( 371 | ident("defined"), 372 | releaseName)), 373 | nnkRecList.newTree( 374 | nnkIdentDefs.newTree( 375 | usedLabelsName, 376 | nnkBracketExpr.newTree( 377 | ident("Table"), 378 | ident("string"), 379 | ident("bool")), newEmptyNode()), 380 | nnkIdentDefs.newTree( 381 | inspectLabelName, 382 | nnkBracketExpr.newTree( 383 | ident("array"), 384 | newIntLitNode(maxEntities), 385 | ident("string")), newEmptyNode())))) 386 | 387 | containerDefs.add nnkIdentDefs.newTree( 388 | nnkPostfix.newTree(ident("*"), signaturesName), 389 | nnkBracketExpr.newTree( 390 | ident("array"), 391 | newIntLitNode(maxEntities), 392 | signatureType), newEmptyNode()) 393 | 394 | enumType.add(existsComponentKind) 395 | 396 | for component in componentDefinitions: 397 | let 398 | componentKindName = ident(toComponentKindName($component.name)) 399 | componentObjectName = component.name 400 | 401 | enumType.add(componentKindName) 402 | 403 | let containerName = ident(toContainerName($component.name)) 404 | 405 | containerDefs.add(nnkIdentDefs.newTree( 406 | nnkPostfix.newTree( 407 | ident("*"), 408 | containerName 409 | ), 410 | nnkBracketExpr.newTree( 411 | ident("array"), 412 | newIntLitNode(maxEntities), 413 | componentObjectName 414 | ), newEmptyNode())) 415 | 416 | setBasedOnComponent.add quote do: 417 | if `componentName` of `componentObjectName`: 418 | `kindName` = `componentKindName` 419 | `ecsName`.`containerName`[`entityName`.idx] = `componentName` 420 | 421 | let ecsDef = nnkTypeSection.newTree( 422 | newNimNode(nnkTypeDef).add( 423 | nnkPostfix.newTree(ident("*"), ecsType), 424 | newEmptyNode(), 425 | nnkRefTy.newTree( 426 | nnkObjectTy.newTree( 427 | newEmptyNode(), 428 | newEmptyNode(), 429 | containerDefs)))) 430 | 431 | result.add quote do: 432 | type 433 | `componentKindType`* = `enumType` 434 | 435 | `signatureType`* = set[`componentKindType`] ## \ 436 | ## The bitset that identifies what Components each entity has. 437 | ## For each entity, a Signature is stored within the 438 | ## `ECS.signatureContainer` array. 439 | 440 | `ecsDef` 441 | 442 | type 443 | `itemType`* = tuple[`ecsName`: `ecsType`; `entityName`: Entity] ## \ 444 | ## An `Item` is a capsule of the `ECS `_ and an `Entity `_. Most functions and 445 | ## templates that follow the pattern `ecs.(entity, <...>)` can 446 | ## also be called using `(ecs, entity).(<...>)`. This becomes 447 | ## especially useful within systems since an item of this type is provided. 448 | ## 449 | ## Components can therefore be accessed using `item.` 450 | ## within systems. See `sys` macro for more details. 451 | 452 | result.add quote do: 453 | func newECS*(): `ecsType` = 454 | ## Create an `ECS `_ instance. The `ECS `_ contains arrays of containers 455 | ## for every component on every entity. It also contains every `Signature` 456 | ## of every entity. The `ECS `_ is used to create entities, register them 457 | ## as well as to modify their components. 458 | new(result) 459 | 460 | result.add quote do: 461 | func getSignature*(`ecsName`: `ecsType`; `entityName`: Entity): Signature = 462 | ## Get the set of `ComponentKind `_ that represents `entity` 463 | result = `ecsName`.`signaturesName`[`entityName`.idx] 464 | 465 | func getSignature*(`itemName`: (`ecsType`, Entity)): Signature = 466 | result = `itemName`[0].`signaturesName`[`itemName`[1].idx] 467 | 468 | result.add quote do: 469 | func inspect*(`ecsName`: `ecsType`; `entityName`: Entity): string = 470 | ## Get a string representation of `entity` including its debug-label. 471 | ## When `release` is defined, only the entity id is returned as a string. 472 | when not defined(release): 473 | result = `ecsName`.`inspectLabelName`[`entityName`.idx] & "[" 474 | result &= $`entityName`.idx 475 | result &= "]" 476 | else: 477 | result = $`entityName` 478 | 479 | func inspect*(`itemName`: `itemType`): string = 480 | ## same as `ecs.inspect(entity)` 481 | `itemName`[0].inspect(`itemName`[1]) 482 | 483 | func `toStringName`*(`itemName`: `itemType`): string = 484 | ## Same as `item.inspect()` and in turn `ecs.inspect(entity)` 485 | result = `itemName`[0].inspect(`itemName`[1]) 486 | 487 | result.add quote do: 488 | func newEntity*(`ecsName`: `ecsType`; label: string = "Entity"): Entity = 489 | ## Create an empty entity. This function creates an entity with a 490 | ## unique ID without any components added. Components can be added 491 | ## either using `addComponent()` (recommmended) or manually using 492 | ## `ecs.Container[entity.idx] = `. If components 493 | ## are added manually, don't forget to manually manage the entity's signature 494 | ## by manipulating `ecs.signatureContainer[entity.idx]` through including 495 | ## and excluding `ck` 496 | when not defined(release): 497 | var 498 | n = 0 499 | actualName = label 500 | 501 | while actualName in `ecsName`.`usedLabelsName`: 502 | inc n 503 | actualName = label & " (" & $n & ")" 504 | 505 | `ecsName`.`usedLabelsName`[actualName] = true 506 | 507 | var newId = -1 508 | for id in `ecsName`.`nextIDName`.idx .. high(`ecsName`.`signaturesName`): 509 | if `existsComponentKind` notin `ecsName`.`signaturesName`[id]: 510 | newId = id 511 | break 512 | 513 | if newId < 0: 514 | raise newException(IndexDefect, "Tried to instantiate Entity '" & 515 | label & "' but ran out of ID:s. You can increase the number of supported entities by supplying 'ECSConfig(maxEntities: )' to 'createECS()'") 516 | 517 | result = newId.Entity 518 | `ecsName`.`nextIDName` = `ecsName`.`nextIDName` + Entity(1) 519 | `ecsName`.`highestIDName` = max(result, `ecsName`.`highestIDName`) 520 | 521 | `ecsName`.`signaturesName`[newId].incl(`existsComponentKind`) 522 | 523 | when not defined(release): 524 | `ecsName`.`inspectLabelName`[newId] = actualName 525 | 526 | result.add quote do: 527 | func removeEntity*(`ecsName`: `ecsType`; `entityName`: Entity) = 528 | if `existsComponentKind` notin `ecsName`.`signaturesName`[`entityName`.idx]: 529 | {.line: instantiationInfo().}: 530 | raise newException(AssertionDefect, "Tried to remove Entity with ID '" & $`entityName`.idx & "' that doesn't exist.") 531 | 532 | `ecsName`.`signaturesName`[`entityName`.idx] = {} 533 | `ecsName`.`nextIDName` = min(`ecsName`.`nextIDName`, `entityName`) 534 | 535 | # If `entityName` is greated than highestID, something is wrong. 536 | # If `entityName` is less than highestID, we can deduct that 537 | # highestID still exists 538 | # so no '[..] or (`entityName` < highestIDName and ckExists notin `ecsName`.`signaturesName`[`highestIDName`.idx])' 539 | # is required here. 540 | if `ecsName`.`highestIDName` == `entityName`: 541 | var i = `ecsName`.`highestIDName`.idx 542 | while `existsComponentKind` notin `ecsName`.`signaturesName`[i]: 543 | if i == 0: break 544 | dec i 545 | 546 | `ecsName`.`highestIDName` = Entity(i) 547 | 548 | template removeEntity(`itemName`: Item) = 549 | removeEntity(`itemName`[0], `itemName`[1]) 550 | 551 | result.add quote do: 552 | func setSignature*(`ecsName`: `ecsType`; 553 | `entityName`: Entity; 554 | signature: Signature = {}) = 555 | ## Set the signature of an entity to the specified set of `ComponentKind`. 556 | ## It is not adviced to use this function since other functions keep track 557 | ## of and manage the entity's signature. For example, `addComponent()` ensures 558 | ## that the added component is included in the signature and vice versa for 559 | ## `removeComponent()`. 560 | `ecsName`.`signaturesName`[`entityName`.idx] = signature 561 | `ecsName`.`signaturesName`[`entityName`.idx].incl(`existsComponentKind`) 562 | 563 | func setSignature*(`itemName`: (`ecsType`, Entity); 564 | signature: Signature): Signature = 565 | let (ecs, entity) = `itemName` 566 | ecs.setSignature(entity, signature) 567 | 568 | result.add quote do: 569 | func extractComponents*(root: NimNode): seq[NimNode] {.compileTime.} = 570 | ## Internal 571 | root.expectKind(nnkStmtList) 572 | for c1 in root: 573 | case c1.kind 574 | of nnkTupleConstr: 575 | for c2 in c1: 576 | result.add(c2) 577 | of nnkPar: 578 | for c2 in c1: 579 | return @[c2] 580 | else: 581 | error("Could not extract component of kind '" & $c1.kind & "'", root) 582 | 583 | result.add quote do: 584 | func extractComponentName*(component: NimNode): string {.compileTime.} = 585 | ## Internal 586 | case component.kind 587 | of nnkObjConstr: 588 | for c1 in component: 589 | case c1.kind 590 | of nnkIdent: 591 | return $c1 592 | 593 | of nnkBracket: 594 | for c2 in c1: 595 | return $c2 596 | 597 | else: discard 598 | 599 | of nnkCommand, nnkCall: 600 | for c1 in component: 601 | c1.expectKind(nnkBracket) 602 | for c2 in c1: 603 | return $c2 604 | 605 | else: discard 606 | 607 | result.add quote do: 608 | macro declareSignature*(components: untyped) = 609 | ## Internal 610 | let curly = newNimNode(nnkCurly) 611 | 612 | curly.add(ident(toComponentKindName("exists"))) 613 | 614 | for component in extractComponents(components): 615 | let componentName = extractComponentName(component) 616 | curly.add(ident(toComponentKindName(componentName))) 617 | 618 | return nnkStmtList.newTree( 619 | nnkLetSection.newTree( 620 | nnkIdentDefs.newTree( 621 | ident("signature"), 622 | ident("Signature"), 623 | curly))) 624 | 625 | result.add quote do: 626 | func toProperComponent*(component: NimNode): NimNode {.compileTime.} = 627 | ## Internal 628 | case component.kind 629 | of nnkObjConstr: 630 | for c1 in component: 631 | if c1.kind == nnkIdent: return component 632 | break 633 | 634 | result = newNimNode(nnkTupleConstr) 635 | 636 | var 637 | isFirst = true 638 | componentIdent: NimNode 639 | 640 | for c1 in component: 641 | if isFirst: 642 | isFirst = false 643 | c1.expectKind(nnkBracket) 644 | for c2 in c1: 645 | componentIdent = c2 646 | break 647 | 648 | continue 649 | result.add(c1) 650 | 651 | result = nnkDotExpr.newTree(result, componentIdent) 652 | return result 653 | 654 | of nnkCommand, nnkCall: 655 | var componentIdent: NimNode 656 | 657 | for c1 in component: 658 | if c1.kind == nnkBracket: 659 | for c2 in c1: 660 | componentIdent = c2 661 | break 662 | continue 663 | 664 | return nnkDotExpr.newTree(c1, componentIdent) 665 | 666 | else: 667 | error("Component " & repr(component) & 668 | " can currently not be interpreted", component) 669 | 670 | for cd in componentDefinitions: 671 | let 672 | name = $cd.name 673 | lowerName = ident(firstLetterLower(name)) 674 | addName = ident("add" & firstLetterUpper(name)) 675 | componentType = ident(firstLetterUpper(name)) 676 | removeName = ident("remove" & firstLetterUpper(name)) 677 | cn = toContainerName(name) 678 | componentContainerName = ident(cn) 679 | ck = toComponentKindName(name) 680 | componentKind = ident(ck) 681 | 682 | templateComment = newCommentStmtNode(&"Expands to `ecs.{cn}[entity.idx]`") 683 | addComment = newCommentStmtNode(&"Add `{name} `_ to `entity` and update its signature by including `{ck} `_") 684 | removeComment = newCommentStmtNode(&"Remove `{name} `_ from `entity` and update its signature by excluding `{ck} `_") 685 | 686 | result.add quote do: 687 | template `lowerName`*(`itemName`: (`ecsType`, Entity)): `componentType` = 688 | `templateComment` 689 | if `componentKind` notin `itemName`.getSignature(): 690 | {.line: instantiationInfo().}: 691 | raise newException(AssertionDefect, "Entity '" & $`itemName` & "' Does not have a component of type '" & `name` & "'") 692 | 693 | `itemName`[0].`componentContainerName`[`itemName`[1].idx] 694 | 695 | result.add quote do: 696 | func addComponent*(`itemName`: (`ecsType`, Entity); 697 | `lowerName`: `componentType`) = 698 | `addComment` 699 | if `componentKind` in `itemName`.getSignature(): 700 | {.line: instantiationInfo().}: 701 | raise newException(AssertionDefect, "Entity '" & $`itemName` & "' Already has a component of type '" & `name` & "'") 702 | 703 | let (`ecsName`, `entityName`) = `itemName` 704 | `ecsName`.`componentContainerName`[`entityName`.idx] = `lowerName` 705 | `ecsName`.`signaturesName`[`entityName`.idx].incl(`componentKind`) 706 | 707 | func `addName`*(`itemName`: (`ecsType`, Entity); 708 | `lowerName`: `componentType`) = 709 | `addComment` 710 | if `componentKind` in `itemName`.getSignature(): 711 | {.line: instantiationInfo().}: 712 | raise newException(AssertionDefect, "Entity '" & $`itemName` & "' Already has a component of type '" & `name` & "'") 713 | 714 | let (`ecsName`, `entityName`) = `itemName` 715 | `ecsName`.`componentContainerName`[`entityName`.idx] = `lowerName` 716 | `ecsName`.`signaturesName`[`entityName`.idx].incl(`componentKind`) 717 | 718 | result.add quote do: 719 | func removeComponent*[T: `componentType`](`itemName`: (`ecsType`, Entity); 720 | t: typedesc[ 721 | T] = `componentType`) = 722 | `removeComment` 723 | if `componentKind` notin `itemName`.getSignature(): 724 | {.line: instantiationInfo().}: 725 | raise newException(AssertionDefect, "Entity '" & $`itemName` & "' Does not have a component of type '" & `name` & "'") 726 | 727 | `itemName`[0].`signaturesName`[`itemName`[1].idx].excl(`componentKind`) 728 | 729 | func `removeName`*(`itemName`: (`ecsType`, Entity)) = 730 | `removeComment` 731 | if `componentKind` notin `itemName`.getSignature(): 732 | {.line: instantiationInfo().}: 733 | raise newException(AssertionDefect,"Entity '" & $`itemName` & "' Does not have a component of type '" & `name` & "'") 734 | 735 | `itemName`[0].`signaturesName`[`itemName`[1].idx].excl(`componentKind`) 736 | 737 | result.add quote do: 738 | macro defineComponentAssignments*(`ecsName`: `ecsType`; 739 | `entityName`: untyped; 740 | components: untyped) = 741 | ## Internal. 742 | result = newStmtList() 743 | 744 | for component in extractComponents(components): 745 | let 746 | componentName = extractComponentName(component) 747 | containerIdent = ident(firstLetterLower(componentName) & "Container") 748 | 749 | let a = nnkStmtList.newTree( 750 | nnkAsgn.newTree( 751 | nnkBracketExpr.newTree( 752 | nnkDotExpr.newTree( 753 | `ecsName`, 754 | containerIdent 755 | ), nnkDotExpr.newTree(`entityName`, ident("idx"))), 756 | toProperComponent(component))) 757 | 758 | result.add a 759 | 760 | result.add quote do: 761 | template createEntity*(`ecsName`: `ecsType`; 762 | label: string; 763 | components: untyped): Entity = 764 | ## Create an entity with a label and components. This template 765 | ## makes it very easy to instantiate an entity with predefined 766 | ## components. See `newEntity` to create an entity without components 767 | 768 | var entity: Entity 769 | block: 770 | declareSignature(components) 771 | 772 | entity = ecs.newEntity(label) 773 | `ecsName`.setSignature(entity, signature) 774 | `ecsName`.defineComponentAssignments(entity, components) 775 | 776 | entity 777 | 778 | result.add quote do: 779 | iterator queryAll*(`ecsName`: `ecsType`; 780 | `queryName`: `signatureType` = {`existsComponentKind`}): Entity = 781 | ## Query and iterate over entities matching the query specified. The 782 | ## query must be a set of `ComponentKind` and all entities that have 783 | ## a signature that a superset of the query will be returned. 784 | ## 785 | ## **Example** 786 | ## 787 | ## .. code-block:: nim 788 | ## # assuming `Position` and `Velocity` components have been defined 789 | ## # and `createECS()` has been called.. 790 | ## 791 | ## for entity in ecs.queryAll({ckPosition, ckVelocity}): 792 | ## echo ecs.inspect(entity) 793 | var actualQuery = `queryName` 794 | actualQuery.incl(`existsComponentKind`) 795 | 796 | for id in 0 .. `ecsName`.`highestIDName`.idx: 797 | if `ecsName`.`signaturesName`[id] >= actualQuery: 798 | yield id.Entity 799 | 800 | for groupName, systems in systemDefinitions.pairs: 801 | let 802 | groupIdent = ident("run" & firstLetterUpper(groupName)) 803 | systemsDef = newNimNode(nnkStmtList) 804 | dataName = ident("data") 805 | 806 | var 807 | groupDataType = newNilLit() 808 | callings = newStmtList() 809 | 810 | for system in systems: 811 | let 812 | (name, signature, _, entireSystem, dataType, sysItemName) = system 813 | systemIdent = ident("run" & firstLetterUpper($name)) 814 | 815 | if groupDataType.kind == nnkNilLit: 816 | groupDataType = dataType 817 | else: 818 | if dataType.kind != nnkNilLit: 819 | doAssert $groupDataType == $dataType, "" 820 | 821 | result.add quote do: 822 | `entireSystem` 823 | 824 | let systemDefinition = newEmptyNode() 825 | 826 | if dataType.kind == nnkNilLit: 827 | result.add quote do: 828 | template `systemIdent`*(`ecsName`: `ecsType`) = 829 | for `sysItemName` in `ecsName`.queryAll(`signature`): 830 | let `sysItemName`: `itemType` = (`ecsName`, `sysItemName`) 831 | `name`(`itemname`) 832 | 833 | let calling = quote do: 834 | `systemIdent`(`ecsName`) 835 | 836 | callings.add(calling) 837 | 838 | else: 839 | result.add quote do: 840 | template `systemIdent`*(`ecsName`: `ecsType`, `dataName`: `groupDataType`) = 841 | for `sysItemName` in `ecsName`.queryAll(`signature`): 842 | let `sysItemName`: `itemType` = (`ecsName`, `sysItemName`) 843 | `name`(`itemname`, `dataName`) 844 | 845 | let calling = quote do: 846 | `systemIdent`(`ecsName`, `dataName`) 847 | 848 | callings.add(calling) 849 | 850 | 851 | if groupDataType.kind == nnkNilLit: 852 | result.add quote do: 853 | template `groupIdent`*(`ecsName`: `ecsType`) = 854 | `callings` 855 | else: 856 | result.add quote do: 857 | template `groupIdent`*(`ecsName`: `ecsType`, `dataName`: `groupDataType`) = 858 | `callings` 859 | 860 | when ecsDebugMacros: echo repr(result) 861 | 862 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path: "$projectDir/../src" 2 | -------------------------------------------------------------------------------- /tests/test_components.nim: -------------------------------------------------------------------------------- 1 | import unittest, easyess 2 | 3 | comp: 4 | type 5 | ObjectComponent = object 6 | data1: int 7 | data2: string 8 | 9 | TestComponent = tuple[a, b: int] 10 | 11 | const numberOfComponents = 2 12 | 13 | createECS() 14 | 15 | const suiteName = when defined(release): "release" else: "debug" 16 | 17 | suite "Components: " & suiteName: 18 | test "ComponentKind is declared": 19 | check declared(ComponentKind) 20 | 21 | test "ckExists is a declared": 22 | check declared(ckExists) 23 | check ckExists in {low(ComponentKind) .. high(ComponentKind)} 24 | check ord(ckExists) == 0 25 | 26 | test "Number of declared ComponentKind enums matches the number of declared components": 27 | check declared(ckObjectComponent) 28 | check declared(ckTestComponent) 29 | 30 | check len(low(ComponentKind) .. high(ComponentKind)) == 31 | numberOfComponents + 1 # ckExists... 32 | 33 | check ord(ckObjectComponent) == 1 34 | check ord(ckTestComponent) == 2 35 | 36 | test "Can define object component": 37 | var a: ObjectComponent = ObjectComponent(data1: 1, data2: "Hello, World!") 38 | check declared(a) 39 | 40 | test "addComponent and removeComponent declared": 41 | check declared(addObjectComponent) 42 | check declared(removeObjectComponent) 43 | check declared(addTestComponent) 44 | check declared(removeTestComponent) 45 | -------------------------------------------------------------------------------- /tests/test_entities.nim: -------------------------------------------------------------------------------- 1 | import unittest, easyess, intsets, strutils, sets 2 | 3 | comp: 4 | type 5 | DataFlag = enum 6 | dfOne, 7 | dfTwo, 8 | dfThree 9 | 10 | DataComponent = tuple 11 | data1: int 12 | data2: string 13 | 14 | ObjectComponent = object 15 | data1: int 16 | data2: string 17 | 18 | ObjectComponent2 = object 19 | data3: DataFlag 20 | data4: float 21 | 22 | Position = object 23 | x: float 24 | y: float 25 | 26 | Pos = tuple[x, y: float] 27 | 28 | Vel = tuple[x, y: float] 29 | 30 | Sprite = uint16 31 | 32 | Name = string 33 | 34 | 35 | createECS() 36 | 37 | const suiteName = when defined(release): "release" else: "debug" 38 | 39 | suite "Entities: " & suiteName: 40 | test "Can create entity": 41 | let 42 | ecs = newEcs() 43 | entity = ecs.newEntity("Potato") 44 | 45 | check entity.idx == 0 46 | 47 | test "Can remove enetity": 48 | let 49 | ecs = newEcs() 50 | entity = ecs.newEntity() 51 | 52 | check entity.idx == 0 53 | check ecs.nextID.idx == 1 54 | check ecs.highestID.idx == 0 55 | 56 | ecs.removeEntity(entity) 57 | 58 | check ecs.nextID.idx == 0 59 | check ecs.highestID.idx == 0 60 | check ckExists notin ecs.signatureContainer[entity.idx] 61 | 62 | test "Can remove enetity (more)": 63 | let 64 | ecs = newEcs() 65 | entity0 = ecs.newEntity() 66 | entity1 = ecs.newEntity() 67 | entity2 = ecs.newEntity() 68 | entity3 = ecs.newEntity() 69 | 70 | ecs.removeEntity(entity1) 71 | ecs.removeEntity(entity2) 72 | 73 | check ecs.nextID.idx == 1 74 | check ecs.highestID.idx == 3 75 | 76 | ecs.removeEntity(entity0) 77 | 78 | check ecs.nextID.idx == 0 79 | check ecs.highestID.idx == 3 80 | 81 | ecs.removeEntity(entity3) 82 | 83 | check ecs.nextID.idx == 0 84 | check ecs.highestID.idx == 0 85 | 86 | 87 | test "Cannot remove entity that doesn't exist": 88 | let 89 | ecs = newEcs() 90 | 91 | expect(AssertionDefect): 92 | ecs.removeEntity(Entity(12)) 93 | 94 | 95 | test "ECS nextID and highestID gets updated correctly": 96 | let ecs = newEcs() 97 | 98 | check ecs.nextID.idx == 0 99 | check ecs.highestID.idx == 0 100 | 101 | let entity00 = ecs.newEntity() 102 | 103 | check ecs.nextID.idx == 1 104 | check ecs.highestID.idx == 0 105 | 106 | ecs.removeEntity(entity00) 107 | 108 | check ecs.nextID.idx == 0 109 | check ecs.highestID.idx == 0 110 | 111 | let 112 | entity10 = ecs.newEntity() 113 | entity11 = ecs.newEntity() 114 | entity12 = ecs.newEntity() 115 | entity13 = ecs.newEntity() 116 | 117 | check ecs.nextID.idx == 4 118 | check ecs.highestID.idx == 3 119 | 120 | (ecs, entity11).removeEntity() # can be called as `Item` as well... 121 | 122 | check ecs.nextID.idx == 1 123 | check ecs.highestID.idx == 3 124 | 125 | ecs.removeEntity(entity12) 126 | 127 | check ecs.nextID.idx == 1 128 | check ecs.highestID.idx == 3 129 | 130 | ecs.removeEntity(entity13) 131 | 132 | check ecs.nextID.idx == 1 133 | check ecs.highestID.idx == 0 134 | 135 | 136 | test "Subsequent entities have unique IDs": 137 | let ecs = newEcs() 138 | var ids: IntSet 139 | 140 | for i in 0 .. high(ecs.signatureContainer): 141 | let currentID = ecs.newEntity("Potato").idx 142 | check currentID notin ids 143 | ids.incl(currentID) 144 | 145 | test "Error when last entity ID used": 146 | let ecs = newEcs() 147 | 148 | for i in 0 .. high(ecs.signatureContainer): 149 | let currentID = ecs.newEntity("Potato").idx 150 | 151 | expect(IndexDefect): 152 | let a = ecs.newEntity("Potato").idx 153 | 154 | when not defined(release): 155 | test "Debug label always unique for subsequent entities with same label": 156 | let 157 | ecs = newEcs() 158 | label = "Debug Entity" 159 | 160 | var labels: HashSet[string] 161 | 162 | for i in 0 .. 10: 163 | let 164 | entity = ecs.newEntity(label) 165 | actualLabel = ecs.inspect(entity) 166 | 167 | check actualLabel notin labels and label in actualLabel 168 | labels.incl(actualLabel) 169 | 170 | test "Debug label present in debug mode": 171 | let 172 | ecs = newEcs() 173 | label = "Debug Entity" 174 | entity = ecs.newEntity(label) 175 | 176 | check label in ecs.inspect(entity) 177 | else: 178 | test "Debug label contains ID in release mode": 179 | let 180 | ecs = newEcs() 181 | label = "Release Entity" 182 | 183 | var labels: HashSet[string] 184 | 185 | for i in 0 .. 10: 186 | let 187 | entity = ecs.newEntity(label) 188 | actualLabel = ecs.inspect(entity) 189 | check actualLabel notin labels and $entity.idx in actualLabel and 190 | label notin actualLabel 191 | 192 | labels.incl(actualLabel) 193 | 194 | test "Debug label not present release mode": 195 | let 196 | ecs = newEcs() 197 | label = "Release Entity" 198 | entity = ecs.newEntity(label) 199 | 200 | check label notin ecs.inspect(entity) 201 | 202 | test "Can register empty entity using template": 203 | let 204 | ecs = newEcs() 205 | entity = ecs.createEntity("Entity"): () 206 | 207 | check ecs.signatureContainer[entity.idx] == {ckExists} 208 | 209 | test "Can register empty entity manually": 210 | let 211 | ecs = newEcs() 212 | entity = ecs.newEntity("Entity") 213 | 214 | check ecs.signatureContainer[entity.idx] == {ckExists} 215 | 216 | test "Can register entity with only one component using template": 217 | let 218 | ecs = newEcs() 219 | entity = ecs.createEntity("Entity"): ( 220 | Position(x: 42.0, y: 0.0) 221 | ) 222 | entity2 = ecs.createEntity("Entity"): ( 223 | Position(x: 69.0, y: 0.0), 224 | ) 225 | entity3 = ecs.createEntity("Entity"): ([Name]"test") 226 | entity4 = ecs.createEntity("Entity"): ([Name]"test",) 227 | entity5 = ecs.createEntity("Entity"): ( 228 | [Name]"test" 229 | ) 230 | entity6 = ecs.createEntity("Entity"): ([DataComponent](data1: 10, data2: "test")) 231 | entity7 = ecs.createEntity("Entity"): ([DataComponent](data1: 10, data2: "test"),) 232 | 233 | check ecs.positionContainer[entity.idx].x == 42.0 234 | check ecs.positionContainer[entity2.idx].x == 69.0 235 | check (ecs, entity3).name == "test" 236 | check (ecs, entity4).name == "test" 237 | check (ecs, entity5).name == "test" 238 | check (ecs, entity6).dataComponent.data1 == 10 239 | check (ecs, entity7).dataComponent.data1 == 10 240 | 241 | test "Can register entity using template with non-object components": 242 | let 243 | ecs = newEcs() 244 | entity = ecs.createEntity("Entity"): ( 245 | [DataFlag]dfThree, 246 | [DataComponent](data1: 42, data2: "test") 247 | ) 248 | 249 | check ecs.signatureContainer[entity.idx] == {ckExists, ckDataFlag, ckDataComponent} 250 | check ecs.dataFlagContainer[entity.idx] == dfThree 251 | check ecs.dataComponentContainer[entity.idx].data1 == 42 252 | 253 | test "Can register entity using template with object component": 254 | let 255 | ecs = newEcs() 256 | entity = ecs.createEntity("Entity"): ( 257 | ObjectComponent(data1: 69, data2: "hello"), 258 | ObjectComponent2(data3: dfTwo, data4: 3.141592), 259 | [DataFlag]dfThree, 260 | [DataComponent](data1: 42, data2: "test") 261 | ) 262 | 263 | check ecs.signatureContainer[entity.idx] == 264 | {ckExists, ckDataFlag, ckDataComponent, ckObjectComponent, ckObjectComponent2} 265 | 266 | check ecs.objectComponentContainer[entity.idx].data1 == 69 267 | check ecs.objectComponent2Container[entity.idx].data3 == dfTwo 268 | check ecs.objectComponent2Container[entity.idx].data4 == 3.141592 269 | check ecs.dataFlagContainer[entity.idx] == dfThree 270 | check ecs.dataComponentContainer[entity.idx].data1 == 42 271 | 272 | test "Can create entity by adding components using functions": 273 | let 274 | ecs = newEcs() 275 | 276 | player = ecs.newEntity("Player") 277 | item = (ecs, player) 278 | 279 | item.addPos((50.0, 50.0)) 280 | item.addVel((10.0, 10.0)) 281 | item.addSprite(42) 282 | 283 | check ecs.posContainer[player.idx].x == 50.0 284 | check ecs.velContainer[player.idx].x == 10.0 285 | check ecs.spriteContainer[player.idx] == 42 286 | 287 | check item.pos.x == ecs.posContainer[player.idx].x 288 | check item.vel.x == ecs.velContainer[player.idx].x 289 | check item.sprite == ecs.spriteContainer[player.idx] 290 | 291 | test "Add components to entity using addComponet and add": 292 | for i in 1 .. 2: 293 | let 294 | ecs = newECS() 295 | entity = ecs.createEntity("Entity"): ( 296 | ObjectComponent(data1: 123, data2: "123") 297 | ) 298 | 299 | check ecs.signatureContainer[entity.idx] == {ckExists, ckObjectComponent} 300 | check ecs.objectComponentContainer[entity.idx].data1 == 123 301 | check ecs.positionContainer[entity.idx].x == 0.0 302 | 303 | # ecs.addComponent(entity, Position(x: 10.0, y: 10.0)) 304 | case i: 305 | of 1: (ecs, entity).addComponent(Position(x: 10.0, y: 10.0)) 306 | of 2: (ecs, entity).addPosition(Position(x: 10.0, y: 10.0)) 307 | else: discard 308 | 309 | check ecs.signatureContainer[entity.idx] == {ckExists, ckObjectComponent, ckPosition} 310 | check ecs.positionContainer[entity.idx].x == 10.0 311 | 312 | test "Remove components from entity using removeComponent and remove": 313 | for i in 1 .. 2: 314 | let 315 | ecs = newECS() 316 | entity = ecs.createEntity("Entity"): ( 317 | ObjectComponent(data1: 123, data2: "123"), 318 | Position(x: 10.0, y: 10.0) 319 | ) 320 | 321 | check ecs.signatureContainer[entity.idx] == {ckExists, ckObjectComponent, ckPosition} 322 | check ecs.objectComponentContainer[entity.idx].data1 == 123 323 | check ecs.positionContainer[entity.idx].x == 10.0 324 | 325 | case i: 326 | of 1: (ecs, entity).removeComponent(Position) 327 | of 2: (ecs, entity).removePosition() 328 | else: discard 329 | 330 | check ecs.signatureContainer[entity.idx] == {ckExists, ckObjectComponent} 331 | 332 | test "Cannot add component that already exists": 333 | let 334 | ecs = newECS() 335 | entity = ecs.createEntity("Entity"): ( 336 | ObjectComponent(data1: 123, data2: "123"), 337 | ) 338 | 339 | expect(AssertionDefect): 340 | (ecs, entity).addObjectComponent(ObjectComponent(data1: 456, data2: "456")) 341 | (ecs, entity).addPosition(Position(x: 10.0, y: 10.0)) 342 | 343 | test "Cannot remove component that doesn't exists": 344 | let 345 | ecs = newECS() 346 | entity = ecs.createEntity("Entity"): ( 347 | ObjectComponent(data1: 123, data2: "123"), 348 | ) 349 | 350 | expect(AssertionDefect): 351 | (ecs, entity).removePosition() 352 | (ecs, entity).removeObjectComponent() 353 | 354 | test "Cannot access component that doesn't exists": 355 | let 356 | ecs = newECS() 357 | entity = ecs.createEntity("Entity"): ( 358 | ObjectComponent(data1: 123, data2: "123"), 359 | ) 360 | 361 | expect(AssertionDefect): 362 | discard (ecs, entity).position 363 | 364 | discard (ecs, entity).objectComponent 365 | -------------------------------------------------------------------------------- /tests/test_systems.nim: -------------------------------------------------------------------------------- 1 | 2 | import easyess, unittest 3 | 4 | 5 | type 6 | Data = int 7 | 8 | comp: 9 | type 10 | Position = object 11 | x: float 12 | y: float 13 | 14 | Velocity = object 15 | dx: float 16 | dy: float 17 | 18 | Sprite = tuple 19 | id: int 20 | 21 | TupleComponent = tuple 22 | test: string 23 | 24 | CustomFlag = enum 25 | cfTest 26 | cfPotato 27 | 28 | Name = string 29 | 30 | Health = int 31 | 32 | Die = bool 33 | 34 | ToBeRemoved = bool 35 | 36 | ComponentWithVeryLongNameThatIsCumbersome = object 37 | value: int 38 | 39 | 40 | const 41 | systemsGroup = "systems" 42 | renderingGroup = "rendering" 43 | 44 | 45 | sys [Position, Velocity], systemsGroup: 46 | func moveSystem(item: Item) = 47 | let (ecs, entity) = item 48 | 49 | let oldPosition = position 50 | 51 | position.x += velocity.dx 52 | position.y += velocity.dy 53 | 54 | sys [Die], systemsGroup: 55 | func isDeadSystem(item: Item) = 56 | discard 57 | 58 | sys [CustomFlag], systemsGroup: 59 | func customFlagSystem(item: Item) = 60 | case customFlag: 61 | of cfTest: customFlag = cfPotato 62 | else: customFlag = cfTest 63 | 64 | 65 | sys [Sprite], renderingGroup: 66 | var oneGlobalValue = 0 67 | 68 | proc renderSpriteSystem(item: Item, data: var Data) = 69 | inc oneGlobalValue 70 | inc data 71 | sprite = (id: 360) 72 | 73 | sys [ToBeRemoved], systemsGroup: 74 | func toBeRemovedSystem(item: Item) = 75 | item.removeEntity() 76 | 77 | sys [flag: CustomFlag, remove: ToBeRemoved], systemsGroup: 78 | func customFlagSystem2(item: Item) = 79 | debugEcho remove 80 | 81 | case flag: 82 | of cfTest: flag = flag 83 | else: flag = flag 84 | 85 | sys [component: ComponentWithVeryLongNameThatIsCumbersome], "longNames": 86 | func longNameSystem(item: Item) = 87 | inc component.value 88 | 89 | 90 | createECS(ECSConfig(maxEntities: 100)) 91 | 92 | const suiteName = when defined(release): "release" else: "debug" 93 | 94 | suite "Systems: " & suiteName: 95 | test "Can use custom names for components in systems": 96 | let 97 | ecs = newEcs() 98 | entity = ecs.createEntity("Entity"): ( 99 | ComponentWithVeryLongNameThatIsCumbersome(value: 0), 100 | ) 101 | item = (ecs, entity) 102 | 103 | check item.componentWithVeryLongNameThatIsCumbersome.value == 0 104 | for i in 1 .. 10: 105 | ecs.runLongNameSystem() 106 | check item.componentWithVeryLongNameThatIsCumbersome.value == 10 107 | 108 | 109 | test "Simple system gets executed everytime 'run()' is called": 110 | let 111 | ecs = newEcs() 112 | entity = ecs.createEntity("Entity"): ( 113 | Position(x: 0.0, y: 0.0), 114 | Velocity(dx: 10.0, dy: -10.0), 115 | ) 116 | 117 | check ecs.positionContainer[entity.idx].x == 0.0 118 | check ecs.positionContainer[entity.idx].y == 0.0 119 | 120 | for i in 1 .. 10: 121 | ecs.runSystems() 122 | check ecs.positionContainer[entity.idx].x == 10.0 * toFloat(i) 123 | check ecs.positionContainer[entity.idx].y == -10.0 * toFloat(i) 124 | 125 | test "Can run system group without running other group": 126 | let 127 | ecs = newEcs() 128 | entity = ecs.createEntity("Entity"): ( 129 | Position(x: 0.0, y: 0.0), 130 | Velocity(dx: 10.0, dy: -10.0), 131 | [Sprite](id: 10), 132 | [CustomFlag]cfTest 133 | ) 134 | 135 | var data = 20 136 | 137 | check ecs.positionContainer[entity.idx].x == 0.0 138 | check ecs.positionContainer[entity.idx].y == 0.0 139 | check ecs.customFlagContainer[entity.idx] == cfTest 140 | check ecs.spriteContainer[entity.idx].id == 10 141 | check data == 20 142 | 143 | ecs.runSystems() 144 | check ecs.positionContainer[entity.idx].x == 10.0 145 | check ecs.positionContainer[entity.idx].y == -10.0 146 | check ecs.customFlagContainer[entity.idx] == cfPotato 147 | check ecs.spriteContainer[entity.idx].id == 10 148 | check data == 20 149 | 150 | ecs.runRendering(data) 151 | check ecs.spriteContainer[entity.idx].id == 360 152 | check ecs.positionContainer[entity.idx].x == 10.0 153 | check ecs.customFlagContainer[entity.idx] == cfPotato 154 | check ecs.positionContainer[entity.idx].y == -10.0 155 | check data == 21 156 | 157 | test "A System can remove an entity": 158 | let 159 | ecs = newECS() 160 | entity1 = ecs.createEntity("test"): ([Name]"potato",[Health](100)) 161 | 162 | check (ecs, entity1).health == 100 163 | 164 | ecs.runSystems() 165 | for i in 0 .. 100: 166 | check (ecs, entity1).health == 100 167 | 168 | expect(AssertionDefect): 169 | discard (ecs, entity1).toBeRemoved 170 | 171 | (ecs, entity1).addComponent(toBeRemoved=true) 172 | check (ecs, entity1).toBeRemoved == true 173 | 174 | ecs.runSystems() 175 | expect(AssertionDefect): 176 | discard (ecs, entity1).health 177 | 178 | check ckExists notin (ecs, entity1).getSignature() 179 | 180 | test "More in-depth entity removal test": 181 | let 182 | ecs = newECS() 183 | entity0 = ecs.createEntity("Entity"): ([Name]"potato",[Health](100)) 184 | item0 = (ecs, entity0) 185 | entity1 = ecs.createEntity("Entity"): ([Name]"potato",[Health](100)) 186 | item1 = (ecs, entity1) 187 | entity2 = ecs.createEntity("Entity"): ([Name]"potato",[Health](100)) 188 | item2 = (ecs, entity2) 189 | entity3 = ecs.createEntity("Entity"): ([Name]"potato",[Health](100)) 190 | item3 = (ecs, entity3) 191 | items = [item0, item1, item2, item3] 192 | items12 = [item1, item2] 193 | items03 = [item0, item3] 194 | 195 | for i in 0 .. 10: 196 | ecs.runSystems() 197 | 198 | for item in items: 199 | check item.name == "potato" 200 | check ckExists in item.getSignature() 201 | 202 | for item in items12: 203 | item.addToBeRemoved(true) 204 | 205 | ecs.runSystems() 206 | 207 | for item in items12: 208 | expect(AssertionDefect): 209 | discard item.name 210 | check ckExists notin item.getSignature() 211 | 212 | check ecs.nextID == 1.Entity 213 | check ecs.highestID == 3.Entity 214 | 215 | for item in items03: 216 | check item.name == "potato" 217 | check ckExists in item.getSignature() 218 | 219 | item0.addToBeRemoved(true) 220 | ecs.runSystems() 221 | expect(AssertionDefect): 222 | discard item0.name 223 | check ckExists notin item0.getSignature() 224 | 225 | check ecs.nextID == 0.Entity 226 | check ecs.highestID == 3.Entity 227 | 228 | item3.addToBeRemoved(true) 229 | ecs.runSystems() 230 | expect(AssertionDefect): 231 | discard item3.name 232 | check ckExists notin item3.getSignature() 233 | 234 | check ecs.nextID == 0.Entity 235 | check ecs.highestID == 0.Entity 236 | 237 | test "Can run systems individually, regardless of group": 238 | let 239 | ecs = newEcs() 240 | entity = ecs.createEntity("Entity"): ( 241 | Position(x: 0.0, y: 0.0), 242 | Velocity(dx: 10.0, dy: -10.0), 243 | [Sprite](id: 10), 244 | [CustomFlag]cfTest 245 | ) 246 | item = (ecs, entity) 247 | 248 | var data = 20 249 | check item.position.x == 0.0 250 | check item.position.y == 0.0 251 | check item.customFlag == cfTest 252 | check item.sprite.id == 10 253 | check data == 20 254 | 255 | for theEntity in ecs.queryAll({ckPosition, ckVelocity}): 256 | let theItem = (ecs, theEntity) 257 | moveSystem(theItem) 258 | 259 | check item.position.x == 10.0 260 | check item.position.y == -10.0 261 | check item.customFlag == cfTest 262 | check item.sprite.id == 10 263 | check data == 20 264 | 265 | ecs.runCustomFlagSystem() 266 | check item.position.x == 10.0 267 | check item.position.y == -10.0 268 | check item.customFlag == cfPotato 269 | check item.sprite.id == 10 270 | check data == 20 271 | 272 | ecs.runRenderSpriteSystem(data) 273 | check item.position.x == 10.0 274 | check item.position.y == -10.0 275 | check item.customFlag == cfPotato 276 | check item.sprite.id == 360 277 | check data == 21 278 | --------------------------------------------------------------------------------