├── .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 |
--------------------------------------------------------------------------------