├── .github └── workflows │ ├── nrs.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── zero_ecs ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── zero_ecs_build ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── code_collection.rs │ ├── code_generation.rs │ ├── file.rs │ ├── lib.rs │ └── macros.rs ├── zero_ecs_macros ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── zero_ecs_testbed ├── .gitignore ├── Cargo.toml ├── build.rs └── src └── main.rs /.github/workflows/nrs.yml: -------------------------------------------------------------------------------- 1 | name: Not Rocket Science 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | build_and_test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 # Necessary for a complete history for merges. 14 | 15 | - name: Check if can be fast-forwarded 16 | run: | 17 | git fetch origin ${{ github.base_ref }} 18 | MERGE_BASE=$(git merge-base HEAD FETCH_HEAD) 19 | if [ $(git rev-parse HEAD) != $(git rev-parse $MERGE_BASE) ] && [ $(git rev-parse FETCH_HEAD) != $(git rev-parse $MERGE_BASE) ]; then 20 | echo "Cannot be fast-forwarded." 21 | exit 1 22 | fi 23 | 24 | - name: Set up Rust 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | profile: minimal 29 | override: true 30 | 31 | - name: Build 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: build 35 | args: --verbose 36 | 37 | - name: Run tests 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: --verbose 42 | 43 | - name: Check with clippy 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: clippy 47 | args: -- -D warnings -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | 12 | only-build: 13 | if: "contains(github.event.head_commit.message, 'chore')" 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | override: true 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --verbose 27 | 28 | build-publish: 29 | if: "!contains(github.event.head_commit.message, 'chore')" 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Configure Git # copied from here: https://stackoverflow.com/questions/69839851/github-actions-copy-git-user-name-and-user-email-from-last-commit 35 | run: | 36 | git config user.name "$(git log -n 1 --pretty=format:%an)" 37 | git config user.email "$(git log -n 1 --pretty=format:%ae)" 38 | - name: Readme cp 39 | run: | 40 | cp README.md zero_ecs/README.md 41 | cp README.md zero_ecs_build/README.md 42 | cp README.md zero_ecs_macros/README.md 43 | 44 | - name: Check for changes 45 | id: git-check 46 | run: | 47 | git status 48 | if [ -n "$(git status --porcelain)" ]; then 49 | echo "::set-output name=changed::true" 50 | fi 51 | - name: Commit changes if any 52 | if: steps.git-check.outputs.changed == 'true' 53 | run: | 54 | git add zero_ecs/README.md 55 | git add zero_ecs_build/README.md 56 | git add zero_ecs_macros/README.md 57 | git commit -m "chore: sync README.md" 58 | git push 59 | 60 | - name: Setup Rust 61 | uses: actions-rs/toolchain@v1 62 | with: 63 | toolchain: stable 64 | override: true 65 | 66 | - name: Build 67 | run: cargo build --verbose 68 | - name: Run tests 69 | run: cargo test --verbose 70 | 71 | - name: Install cargo-release 72 | run: cargo install cargo-release 73 | - name: Release 74 | env: 75 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 76 | run: cargo release --workspace patch --execute --no-confirm --no-verify -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "crossbeam-deque" 7 | version = "0.8.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 10 | dependencies = [ 11 | "crossbeam-epoch", 12 | "crossbeam-utils", 13 | ] 14 | 15 | [[package]] 16 | name = "crossbeam-epoch" 17 | version = "0.9.18" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 20 | dependencies = [ 21 | "crossbeam-utils", 22 | ] 23 | 24 | [[package]] 25 | name = "crossbeam-utils" 26 | version = "0.8.19" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 29 | 30 | [[package]] 31 | name = "either" 32 | version = "1.10.0" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 35 | 36 | [[package]] 37 | name = "glob" 38 | version = "0.3.1" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 41 | 42 | [[package]] 43 | name = "itertools" 44 | version = "0.12.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 47 | dependencies = [ 48 | "either", 49 | ] 50 | 51 | [[package]] 52 | name = "proc-macro2" 53 | version = "1.0.79" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 56 | dependencies = [ 57 | "unicode-ident", 58 | ] 59 | 60 | [[package]] 61 | name = "quote" 62 | version = "1.0.35" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 65 | dependencies = [ 66 | "proc-macro2", 67 | ] 68 | 69 | [[package]] 70 | name = "rayon" 71 | version = "1.9.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" 74 | dependencies = [ 75 | "either", 76 | "rayon-core", 77 | ] 78 | 79 | [[package]] 80 | name = "rayon-core" 81 | version = "1.12.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 84 | dependencies = [ 85 | "crossbeam-deque", 86 | "crossbeam-utils", 87 | ] 88 | 89 | [[package]] 90 | name = "syn" 91 | version = "2.0.52" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 94 | dependencies = [ 95 | "proc-macro2", 96 | "quote", 97 | "unicode-ident", 98 | ] 99 | 100 | [[package]] 101 | name = "unicode-ident" 102 | version = "1.0.12" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 105 | 106 | [[package]] 107 | name = "zero_ecs" 108 | version = "0.2.22" 109 | dependencies = [ 110 | "itertools", 111 | "rayon", 112 | "zero_ecs_macros", 113 | ] 114 | 115 | [[package]] 116 | name = "zero_ecs_build" 117 | version = "0.2.22" 118 | dependencies = [ 119 | "glob", 120 | "itertools", 121 | "quote", 122 | "syn", 123 | "zero_ecs_macros", 124 | ] 125 | 126 | [[package]] 127 | name = "zero_ecs_macros" 128 | version = "0.2.22" 129 | dependencies = [ 130 | "quote", 131 | "syn", 132 | ] 133 | 134 | [[package]] 135 | name = "zero_ecs_testbed" 136 | version = "0.2.22" 137 | dependencies = [ 138 | "zero_ecs", 139 | "zero_ecs_build", 140 | ] 141 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["zero_ecs", "zero_ecs_macros", "zero_ecs_build", "zero_ecs_testbed"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JohanNorberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zero ECS 2 | 3 | Zero ECS is an Entity Component System that is written with 4 goals 4 | 1. Only use zero cost abstractions - no use of dyn and Box and stuff [zero-cost-abstractions](https://doc.rust-lang.org/beta/embedded-book/static-guarantees/zero-cost-abstractions.html). 5 | 2. No use of unsafe rust code. 6 | 3. Be very user friendly. The user should write as little boilerplate as possible. 7 | 4. Be very fast 8 | 9 | It achieves this by generating all code at compile time, using a combination of macros and build scripts. 10 | 11 | ## Instructions 12 | 13 | Create a new project 14 | 15 | ```sh 16 | cargo new zero_ecs_example 17 | cd zero_ecs_example 18 | ``` 19 | 20 | Add the dependencies 21 | 22 | ```sh 23 | cargo add zero_ecs 24 | cargo add zero_ecs_build --build 25 | ``` 26 | 27 | Your Cargo.toml should look something like this: 28 | 29 | ```sh 30 | [dependencies] 31 | zero_ecs = "*" 32 | 33 | [build-dependencies] 34 | zero_ecs_build = "*" 35 | ``` 36 | 37 | Create `build.rs` 38 | 39 | ```sh 40 | touch build.rs 41 | ``` 42 | 43 | Edit `build.rs` to call the zero_ecs's build generation code. 44 | 45 | ```rust 46 | use zero_ecs_build::*; 47 | fn main() { 48 | generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs 49 | } 50 | ``` 51 | 52 | This will generate the entity component system based on the component, entities and systems in main.rs. 53 | It accepts a glob so you can use wild cards. 54 | 55 | ```rust 56 | use zero_ecs_build::*; 57 | fn main() { 58 | generate_ecs("src/**/*.rs"); // look in all *.rs files in src. 59 | } 60 | ``` 61 | 62 | ## Using the ECS 63 | 64 | ### Include ECS 65 | 66 | In `main.rs` 67 | Include the ECS like so: 68 | 69 | ```rust 70 | include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs")); 71 | ``` 72 | 73 | ### Components 74 | 75 | Define some components: 76 | 77 | Position and velocity has x and y 78 | 79 | ```rust 80 | #[component] 81 | struct Position(f32, f32); 82 | 83 | #[component] 84 | struct Velocity(f32, f32); 85 | ``` 86 | 87 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 88 | 89 | ```rust 90 | #[component] 91 | struct EnemyComponent; 92 | 93 | #[component] 94 | struct PlayerComponent; 95 | ``` 96 | 97 | ### Entities 98 | 99 | Entities are a collection of components, they may also be referred to as archetypes, or bundles, or game objects. 100 | Note that once "in" the ECS. An Entity is simply an ID that can be copied. 101 | 102 | In our example, we define an enemy and a player, they both have position and velocity but can be differentiated by their "tag" components. 103 | 104 | ```rust 105 | #[entity] 106 | struct Enemy { 107 | position: Position, 108 | velocity: Velocity, 109 | enemy_component: EnemyComponent, 110 | } 111 | 112 | #[entity] 113 | struct Player { 114 | position: Position, 115 | velocity: Velocity, 116 | player_component: PlayerComponent, 117 | } 118 | ``` 119 | 120 | ### Systems 121 | 122 | Systems run the logic for the application. They can accept references, mutable references and queries. 123 | 124 | In our example we can create a system that simply prints the position of all entities 125 | 126 | ```rust 127 | #[system] 128 | fn print_positions(world: &World, query: Query<&Position>) { 129 | world.with_query(query).iter().for_each(|position| { 130 | println!("Position: {:?}", position); 131 | }); 132 | } 133 | ``` 134 | 135 | #### Explained: 136 | 137 | - world: &World - Since the system doesn't modify anything, it can be an immutable reference 138 | - query: Query<&Position> - We want to query the world for all positions 139 | - world.with_query(query).iter() - creates an iterator over all Position components 140 | 141 | ### Creating entities and calling system 142 | 143 | In our `fn main` change it to create 10 enemies and 10 players, 144 | Also add the `systems_main(&world);` to call all systems. 145 | 146 | ```rust 147 | fn main() { 148 | let mut world = World::default(); 149 | 150 | for i in 0..10 { 151 | world.create(Enemy { 152 | position: Position(i as f32, 5.0), 153 | velocity: Velocity(0.0, 1.0), 154 | ..Default::default() 155 | }); 156 | 157 | world.create(Player { 158 | position: Position(5.0, i as f32), 159 | velocity: Velocity(1.0, 0.0), 160 | ..Default::default() 161 | }); 162 | } 163 | 164 | systems_main(&world); 165 | } 166 | ``` 167 | 168 | Running the program now, will print the positions of the entities. 169 | 170 | ## More advanced 171 | 172 | Continuing our example 173 | 174 | ### mutating systems 175 | 176 | Most systems will mutate the world state and needs additional resources, like texture managers, time managers, input managers etc. 177 | A good practice is to group them in a Resources struct. (But Not nescessary) 178 | 179 | ```rust 180 | struct Resources { 181 | delta_time: f32, 182 | } 183 | 184 | #[system] 185 | fn apply_velocity( 186 | world: &mut World, // world mut be mutable 187 | resources: &Resources, // we need the delta time 188 | query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not. 189 | ) { 190 | world 191 | .with_query_mut(query) // we call with_query_mut because it's a mutable query 192 | .iter_mut() // iterating mutable 193 | .for_each(|(position, velocity)| { 194 | position.0 += velocity.0 * resources.delta_time; 195 | position.1 += velocity.1 * resources.delta_time; 196 | }); 197 | } 198 | 199 | ``` 200 | 201 | We also have to change the main function to include resources in the call. 202 | 203 | ```rust 204 | let resources = Resources { delta_time: 0.1 }; 205 | 206 | systems_main(&resources, &mut world); 207 | ``` 208 | 209 | 210 | #### Destroying entities 211 | 212 | Let's say we want to create a rule that if player and enemies get within 3 units of eachother they should both be destroyed. 213 | This is how we might implement that: 214 | 215 | ```rust 216 | #[system] 217 | fn collide_enemy_and_players( 218 | world: &mut World, // we are destorying entities so it needs to be mutable 219 | players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities 220 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies 221 | ) { 222 | let mut entities_to_destroy: Vec = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration. 223 | 224 | world 225 | .with_query(players) 226 | .iter() 227 | .for_each(|(player_entity, player_position, _)| { 228 | world 229 | .with_query(enemies) 230 | .iter() 231 | .for_each(|(enemy_entity, enemy_position, _)| { 232 | if (player_position.0 - enemy_position.0).abs() < 3.0 233 | && (player_position.1 - enemy_position.1).abs() < 3.0 234 | { 235 | entities_to_destroy.push(*player_entity); 236 | entities_to_destroy.push(*enemy_entity); 237 | } 238 | }); 239 | }); 240 | 241 | for entity in entities_to_destroy { 242 | world.destroy(entity); 243 | } 244 | } 245 | ``` 246 | 247 | #### Get & At entities 248 | 249 | Get is identical to query but takes an Entity. 250 | At is identical to query but takes an index. 251 | 252 | Let's say you wanted an entity that follows a player. This is how you could implement that: 253 | 254 | Define a component for the companion 255 | ```rust 256 | #[component] 257 | struct CompanionComponent { 258 | target_entity: Option, 259 | } 260 | ``` 261 | 262 | Define the Companion Entity. It has a position and a companion component: 263 | ```rust 264 | #[entity] 265 | struct Companion { 266 | position: Position, 267 | companion_component: CompanionComponent, 268 | } 269 | ``` 270 | 271 | Now we need to write the companion system. 272 | For every companion we need to check if it has a target. 273 | If it has a target we need to check if target exists (it could have been deleted). 274 | If the target exists we get the *value of* target's position and set the companion's position with that value. 275 | 276 | We need to query for companions and their position as mutable. And we need to query for every entity that has a position. This means a companion could technically follow it self. 277 | 278 | ```rust 279 | #[system] 280 | fn companion_follow( 281 | world: &mut World, 282 | companions: Query<(&mut Position, &CompanionComponent)>, 283 | positions: Query<&Position>, 284 | ) { 285 | ``` 286 | 287 | Implementation: 288 | We can't simply iterate through the companions, get the target position and update the position because we can only have one borrow if the borrow is mutable (unless we use unsafe code). 289 | 290 | We can do what we did with destroying entities, but it will be slow. 291 | 292 | The solution is to iterate using index, only borrowing what we need for a short time: 293 | 294 | 295 | ```rust 296 | #[system] 297 | fn companion_follow( 298 | world: &mut World, 299 | companions: Query<(&mut Position, &CompanionComponent)>, 300 | positions: Query<&Position>, 301 | ) { 302 | for companion_idx in 0..world.with_query_mut(companions).len() { 303 | // iterate the count of companions 304 | if let Some(target_position) = world 305 | .with_query_mut(companions) 306 | .at_mut(companion_idx) // get the companion at index companion_idx 307 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 308 | .and_then(|companion_target_entity| { 309 | // then get the VALUE of target position (meaning we don't use a reference to the position) 310 | world 311 | .with_query(positions) 312 | .get(companion_target_entity) // get the position for the companion_target_entity 313 | .map(|p| (p.0, p.1)) // map to get the VALUE 314 | }) 315 | { 316 | if let Some((companion_position, _)) = 317 | world.with_query_mut(companions).at_mut(companion_idx) 318 | // Then simply get the companion position 319 | { 320 | // and update it to the target's position 321 | companion_position.0 = target_position.0; 322 | companion_position.1 = target_position.1; 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | # TODO: 330 | - [ ] Re use IDs of deleted entities 331 | 332 | -------------------------------------------------------------------------------- /zero_ecs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs" 3 | version = "0.2.22" 4 | edition = "2021" 5 | description = "Entity Component System (ECS), using only zero-cost abstractions" 6 | repository = "https://github.com/JohanNorberg/zero_ecs" 7 | documentation = "https://github.com/JohanNorberg/zero_ecs#readme" 8 | license = "MIT OR Apache-2.0" 9 | authors = ["Johan Norberg "] 10 | keywords = ["ecs", "zero_ecs"] 11 | categories = ["game-development", "data-structures"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | zero_ecs_macros = { path = "../zero_ecs_macros", version = "0.2.22" } 16 | itertools = "0.12.1" 17 | rayon = "1.9.0" 18 | -------------------------------------------------------------------------------- /zero_ecs/README.md: -------------------------------------------------------------------------------- 1 | # Zero ECS 2 | 3 | Zero ECS is an Entity Component System that is written with 4 goals 4 | 1. Only use zero cost abstractions - no use of dyn and Box and stuff [zero-cost-abstractions](https://doc.rust-lang.org/beta/embedded-book/static-guarantees/zero-cost-abstractions.html). 5 | 2. No use of unsafe rust code. 6 | 3. Be very user friendly. The user should write as little boilerplate as possible. 7 | 4. Be very fast 8 | 9 | It achieves this by generating all code at compile time, using a combination of macros and build scripts. 10 | 11 | ## Instructions 12 | 13 | Create a new project 14 | 15 | ```sh 16 | cargo new zero_ecs_example 17 | cd zero_ecs_example 18 | ``` 19 | 20 | Add the dependencies 21 | 22 | ```sh 23 | cargo add zero_ecs 24 | cargo add zero_ecs_build --build 25 | ``` 26 | 27 | Your Cargo.toml should look something like this: 28 | 29 | ```sh 30 | [dependencies] 31 | zero_ecs = "*" 32 | 33 | [build-dependencies] 34 | zero_ecs_build = "*" 35 | ``` 36 | 37 | Create `build.rs` 38 | 39 | ```sh 40 | touch build.rs 41 | ``` 42 | 43 | Edit `build.rs` to call the zero_ecs's build generation code. 44 | 45 | ```rust 46 | use zero_ecs_build::*; 47 | fn main() { 48 | generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs 49 | } 50 | ``` 51 | 52 | This will generate the entity component system based on the component, entities and systems in main.rs. 53 | It accepts a glob so you can use wild cards. 54 | 55 | ```rust 56 | use zero_ecs_build::*; 57 | fn main() { 58 | generate_ecs("src/**/*.rs"); // look in all *.rs files in src. 59 | } 60 | ``` 61 | 62 | ## Using the ECS 63 | 64 | ### Include ECS 65 | 66 | In `main.rs` 67 | Include the ECS like so: 68 | 69 | ```rust 70 | include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs")); 71 | ``` 72 | 73 | ### Components 74 | 75 | Define some components: 76 | 77 | Position and velocity has x and y 78 | 79 | ```rust 80 | #[component] 81 | struct Position(f32, f32); 82 | 83 | #[component] 84 | struct Velocity(f32, f32); 85 | ``` 86 | 87 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 88 | 89 | ```rust 90 | #[component] 91 | struct EnemyComponent; 92 | 93 | #[component] 94 | struct PlayerComponent; 95 | ``` 96 | 97 | ### Entities 98 | 99 | Entities are a collection of components, they may also be referred to as archetypes, or bundles, or game objects. 100 | Note that once "in" the ECS. An Entity is simply an ID that can be copied. 101 | 102 | In our example, we define an enemy and a player, they both have position and velocity but can be differentiated by their "tag" components. 103 | 104 | ```rust 105 | #[entity] 106 | struct Enemy { 107 | position: Position, 108 | velocity: Velocity, 109 | enemy_component: EnemyComponent, 110 | } 111 | 112 | #[entity] 113 | struct Player { 114 | position: Position, 115 | velocity: Velocity, 116 | player_component: PlayerComponent, 117 | } 118 | ``` 119 | 120 | ### Systems 121 | 122 | Systems run the logic for the application. They can accept references, mutable references and queries. 123 | 124 | In our example we can create a system that simply prints the position of all entities 125 | 126 | ```rust 127 | #[system] 128 | fn print_positions(world: &World, query: Query<&Position>) { 129 | world.with_query(query).iter().for_each(|position| { 130 | println!("Position: {:?}", position); 131 | }); 132 | } 133 | ``` 134 | 135 | #### Explained: 136 | 137 | - world: &World - Since the system doesn't modify anything, it can be an immutable reference 138 | - query: Query<&Position> - We want to query the world for all positions 139 | - world.with_query(query).iter() - creates an iterator over all Position components 140 | 141 | ### Creating entities and calling system 142 | 143 | In our `fn main` change it to create 10 enemies and 10 players, 144 | Also add the `systems_main(&world);` to call all systems. 145 | 146 | ```rust 147 | fn main() { 148 | let mut world = World::default(); 149 | 150 | for i in 0..10 { 151 | world.create(Enemy { 152 | position: Position(i as f32, 5.0), 153 | velocity: Velocity(0.0, 1.0), 154 | ..Default::default() 155 | }); 156 | 157 | world.create(Player { 158 | position: Position(5.0, i as f32), 159 | velocity: Velocity(1.0, 0.0), 160 | ..Default::default() 161 | }); 162 | } 163 | 164 | systems_main(&world); 165 | } 166 | ``` 167 | 168 | Running the program now, will print the positions of the entities. 169 | 170 | ## More advanced 171 | 172 | Continuing our example 173 | 174 | ### mutating systems 175 | 176 | Most systems will mutate the world state and needs additional resources, like texture managers, time managers, input managers etc. 177 | A good practice is to group them in a Resources struct. (But Not nescessary) 178 | 179 | ```rust 180 | struct Resources { 181 | delta_time: f32, 182 | } 183 | 184 | #[system] 185 | fn apply_velocity( 186 | world: &mut World, // world mut be mutable 187 | resources: &Resources, // we need the delta time 188 | query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not. 189 | ) { 190 | world 191 | .with_query_mut(query) // we call with_query_mut because it's a mutable query 192 | .iter_mut() // iterating mutable 193 | .for_each(|(position, velocity)| { 194 | position.0 += velocity.0 * resources.delta_time; 195 | position.1 += velocity.1 * resources.delta_time; 196 | }); 197 | } 198 | 199 | ``` 200 | 201 | We also have to change the main function to include resources in the call. 202 | 203 | ```rust 204 | let resources = Resources { delta_time: 0.1 }; 205 | 206 | systems_main(&resources, &mut world); 207 | ``` 208 | 209 | 210 | #### Destroying entities 211 | 212 | Let's say we want to create a rule that if player and enemies get within 3 units of eachother they should both be destroyed. 213 | This is how we might implement that: 214 | 215 | ```rust 216 | #[system] 217 | fn collide_enemy_and_players( 218 | world: &mut World, // we are destorying entities so it needs to be mutable 219 | players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities 220 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies 221 | ) { 222 | let mut entities_to_destroy: Vec = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration. 223 | 224 | world 225 | .with_query(players) 226 | .iter() 227 | .for_each(|(player_entity, player_position, _)| { 228 | world 229 | .with_query(enemies) 230 | .iter() 231 | .for_each(|(enemy_entity, enemy_position, _)| { 232 | if (player_position.0 - enemy_position.0).abs() < 3.0 233 | && (player_position.1 - enemy_position.1).abs() < 3.0 234 | { 235 | entities_to_destroy.push(*player_entity); 236 | entities_to_destroy.push(*enemy_entity); 237 | } 238 | }); 239 | }); 240 | 241 | for entity in entities_to_destroy { 242 | world.destroy(entity); 243 | } 244 | } 245 | ``` 246 | 247 | #### Get & At entities 248 | 249 | Get is identical to query but takes an Entity. 250 | At is identical to query but takes an index. 251 | 252 | Let's say you wanted an entity that follows a player. This is how you could implement that: 253 | 254 | Define a component for the companion 255 | ```rust 256 | #[component] 257 | struct CompanionComponent { 258 | target_entity: Option, 259 | } 260 | ``` 261 | 262 | Define the Companion Entity. It has a position and a companion component: 263 | ```rust 264 | #[entity] 265 | struct Companion { 266 | position: Position, 267 | companion_component: CompanionComponent, 268 | } 269 | ``` 270 | 271 | Now we need to write the companion system. 272 | For every companion we need to check if it has a target. 273 | If it has a target we need to check if target exists (it could have been deleted). 274 | If the target exists we get the *value of* target's position and set the companion's position with that value. 275 | 276 | We need to query for companions and their position as mutable. And we need to query for every entity that has a position. This means a companion could technically follow it self. 277 | 278 | ```rust 279 | #[system] 280 | fn companion_follow( 281 | world: &mut World, 282 | companions: Query<(&mut Position, &CompanionComponent)>, 283 | positions: Query<&Position>, 284 | ) { 285 | ``` 286 | 287 | Implementation: 288 | We can't simply iterate through the companions, get the target position and update the position because we can only have one borrow if the borrow is mutable (unless we use unsafe code). 289 | 290 | We can do what we did with destroying entities, but it will be slow. 291 | 292 | The solution is to iterate using index, only borrowing what we need for a short time: 293 | 294 | 295 | ```rust 296 | #[system] 297 | fn companion_follow( 298 | world: &mut World, 299 | companions: Query<(&mut Position, &CompanionComponent)>, 300 | positions: Query<&Position>, 301 | ) { 302 | for companion_idx in 0..world.with_query_mut(companions).len() { 303 | // iterate the count of companions 304 | if let Some(target_position) = world 305 | .with_query_mut(companions) 306 | .at_mut(companion_idx) // get the companion at index companion_idx 307 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 308 | .and_then(|companion_target_entity| { 309 | // then get the VALUE of target position (meaning we don't use a reference to the position) 310 | world 311 | .with_query(positions) 312 | .get(companion_target_entity) // get the position for the companion_target_entity 313 | .map(|p| (p.0, p.1)) // map to get the VALUE 314 | }) 315 | { 316 | if let Some((companion_position, _)) = 317 | world.with_query_mut(companions).at_mut(companion_idx) 318 | // Then simply get the companion position 319 | { 320 | // and update it to the target's position 321 | companion_position.0 = target_position.0; 322 | companion_position.1 = target_position.1; 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | # TODO: 330 | - [ ] Re use IDs of deleted entities 331 | 332 | -------------------------------------------------------------------------------- /zero_ecs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use itertools::chain; 2 | pub use itertools::izip; 3 | pub use rayon; 4 | pub use rayon::prelude::*; 5 | pub use zero_ecs_macros::component; 6 | pub use zero_ecs_macros::entity; 7 | pub use zero_ecs_macros::system; 8 | #[macro_export] 9 | macro_rules! izip_par { 10 | // @closure creates a tuple-flattening closure for .map() call. usage: 11 | // @closure partial_pattern => partial_tuple , rest , of , iterators 12 | // eg. izip!( @closure ((a, b), c) => (a, b, c) , dd , ee ) 13 | ( @closure $p:pat => $tup:expr ) => { 14 | |$p| $tup 15 | }; 16 | 17 | // The "b" identifier is a different identifier on each recursion level thanks to hygiene. 18 | ( @closure $p:pat => ( $($tup:tt)* ) , $_iter:expr $( , $tail:expr )* ) => { 19 | $crate::izip_par!(@closure ($p, b) => ( $($tup)*, b ) $( , $tail )*) 20 | }; 21 | 22 | // unary 23 | ($first:expr $(,)*) => { 24 | $crate::IntoParallelIterator::into_par_iter($first) 25 | }; 26 | 27 | // binary 28 | ($first:expr, $second:expr $(,)*) => { 29 | $crate::izip_par!($first) 30 | .zip($second) 31 | }; 32 | 33 | // n-ary where n > 2 34 | ( $first:expr $( , $rest:expr )* $(,)* ) => { 35 | $crate::izip_par!($first) 36 | $( 37 | .zip($rest) 38 | )* 39 | .map( 40 | $crate::izip!(@closure a => (a) $( , $rest )*) 41 | ) 42 | }; 43 | } 44 | #[macro_export] 45 | macro_rules! chain_par { 46 | () => { 47 | rayon::iter::empty() 48 | }; 49 | ($first:expr $(, $rest:expr )* $(,)?) => { 50 | { 51 | let iter = $crate::IntoParallelIterator::into_par_iter($first); 52 | $( 53 | let iter = 54 | ParallelIterator::chain( 55 | iter, 56 | $crate::IntoParallelIterator::into_par_iter($rest)); 57 | )* 58 | iter 59 | } 60 | }; 61 | } 62 | 63 | // found code for sum here: https://gist.github.com/jnordwick/1473d5533ca158d47ba4 64 | #[macro_export] 65 | macro_rules! sum { 66 | ($h:expr) => ($h); // so that this would be called, I ... 67 | ($h:expr, $($t:expr),*) => 68 | (sum!($h) + sum!($($t),*)); // ...call sum! on both sides of the operation 69 | } 70 | -------------------------------------------------------------------------------- /zero_ecs_build/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_build" 3 | version = "0.2.22" 4 | edition = "2021" 5 | description = "Build scripts for: ZeroECS: an Entity Component System (ECS), using only zero-cost abstractions" 6 | repository = "https://github.com/JohanNorberg/zero_ecs" 7 | documentation = "https://github.com/JohanNorberg/zero_ecs#readme" 8 | license = "MIT OR Apache-2.0" 9 | authors = ["Johan Norberg "] 10 | keywords = ["ecs", "zero_ecs"] 11 | categories = ["game-development", "data-structures"] 12 | readme = "README.md" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | zero_ecs_macros = { path = "../zero_ecs_macros", version = "0.2.22" } 18 | syn = { version = "2.0.51", features = ["full"] } 19 | quote = "1.0.35" 20 | itertools = "0.12.1" 21 | glob = "0.3.1" 22 | -------------------------------------------------------------------------------- /zero_ecs_build/README.md: -------------------------------------------------------------------------------- 1 | # Zero ECS 2 | 3 | Zero ECS is an Entity Component System that is written with 4 goals 4 | 1. Only use zero cost abstractions - no use of dyn and Box and stuff [zero-cost-abstractions](https://doc.rust-lang.org/beta/embedded-book/static-guarantees/zero-cost-abstractions.html). 5 | 2. No use of unsafe rust code. 6 | 3. Be very user friendly. The user should write as little boilerplate as possible. 7 | 4. Be very fast 8 | 9 | It achieves this by generating all code at compile time, using a combination of macros and build scripts. 10 | 11 | ## Instructions 12 | 13 | Create a new project 14 | 15 | ```sh 16 | cargo new zero_ecs_example 17 | cd zero_ecs_example 18 | ``` 19 | 20 | Add the dependencies 21 | 22 | ```sh 23 | cargo add zero_ecs 24 | cargo add zero_ecs_build --build 25 | ``` 26 | 27 | Your Cargo.toml should look something like this: 28 | 29 | ```sh 30 | [dependencies] 31 | zero_ecs = "*" 32 | 33 | [build-dependencies] 34 | zero_ecs_build = "*" 35 | ``` 36 | 37 | Create `build.rs` 38 | 39 | ```sh 40 | touch build.rs 41 | ``` 42 | 43 | Edit `build.rs` to call the zero_ecs's build generation code. 44 | 45 | ```rust 46 | use zero_ecs_build::*; 47 | fn main() { 48 | generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs 49 | } 50 | ``` 51 | 52 | This will generate the entity component system based on the component, entities and systems in main.rs. 53 | It accepts a glob so you can use wild cards. 54 | 55 | ```rust 56 | use zero_ecs_build::*; 57 | fn main() { 58 | generate_ecs("src/**/*.rs"); // look in all *.rs files in src. 59 | } 60 | ``` 61 | 62 | ## Using the ECS 63 | 64 | ### Include ECS 65 | 66 | In `main.rs` 67 | Include the ECS like so: 68 | 69 | ```rust 70 | include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs")); 71 | ``` 72 | 73 | ### Components 74 | 75 | Define some components: 76 | 77 | Position and velocity has x and y 78 | 79 | ```rust 80 | #[component] 81 | struct Position(f32, f32); 82 | 83 | #[component] 84 | struct Velocity(f32, f32); 85 | ``` 86 | 87 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 88 | 89 | ```rust 90 | #[component] 91 | struct EnemyComponent; 92 | 93 | #[component] 94 | struct PlayerComponent; 95 | ``` 96 | 97 | ### Entities 98 | 99 | Entities are a collection of components, they may also be referred to as archetypes, or bundles, or game objects. 100 | Note that once "in" the ECS. An Entity is simply an ID that can be copied. 101 | 102 | In our example, we define an enemy and a player, they both have position and velocity but can be differentiated by their "tag" components. 103 | 104 | ```rust 105 | #[entity] 106 | struct Enemy { 107 | position: Position, 108 | velocity: Velocity, 109 | enemy_component: EnemyComponent, 110 | } 111 | 112 | #[entity] 113 | struct Player { 114 | position: Position, 115 | velocity: Velocity, 116 | player_component: PlayerComponent, 117 | } 118 | ``` 119 | 120 | ### Systems 121 | 122 | Systems run the logic for the application. They can accept references, mutable references and queries. 123 | 124 | In our example we can create a system that simply prints the position of all entities 125 | 126 | ```rust 127 | #[system] 128 | fn print_positions(world: &World, query: Query<&Position>) { 129 | world.with_query(query).iter().for_each(|position| { 130 | println!("Position: {:?}", position); 131 | }); 132 | } 133 | ``` 134 | 135 | #### Explained: 136 | 137 | - world: &World - Since the system doesn't modify anything, it can be an immutable reference 138 | - query: Query<&Position> - We want to query the world for all positions 139 | - world.with_query(query).iter() - creates an iterator over all Position components 140 | 141 | ### Creating entities and calling system 142 | 143 | In our `fn main` change it to create 10 enemies and 10 players, 144 | Also add the `systems_main(&world);` to call all systems. 145 | 146 | ```rust 147 | fn main() { 148 | let mut world = World::default(); 149 | 150 | for i in 0..10 { 151 | world.create(Enemy { 152 | position: Position(i as f32, 5.0), 153 | velocity: Velocity(0.0, 1.0), 154 | ..Default::default() 155 | }); 156 | 157 | world.create(Player { 158 | position: Position(5.0, i as f32), 159 | velocity: Velocity(1.0, 0.0), 160 | ..Default::default() 161 | }); 162 | } 163 | 164 | systems_main(&world); 165 | } 166 | ``` 167 | 168 | Running the program now, will print the positions of the entities. 169 | 170 | ## More advanced 171 | 172 | Continuing our example 173 | 174 | ### mutating systems 175 | 176 | Most systems will mutate the world state and needs additional resources, like texture managers, time managers, input managers etc. 177 | A good practice is to group them in a Resources struct. (But Not nescessary) 178 | 179 | ```rust 180 | struct Resources { 181 | delta_time: f32, 182 | } 183 | 184 | #[system] 185 | fn apply_velocity( 186 | world: &mut World, // world mut be mutable 187 | resources: &Resources, // we need the delta time 188 | query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not. 189 | ) { 190 | world 191 | .with_query_mut(query) // we call with_query_mut because it's a mutable query 192 | .iter_mut() // iterating mutable 193 | .for_each(|(position, velocity)| { 194 | position.0 += velocity.0 * resources.delta_time; 195 | position.1 += velocity.1 * resources.delta_time; 196 | }); 197 | } 198 | 199 | ``` 200 | 201 | We also have to change the main function to include resources in the call. 202 | 203 | ```rust 204 | let resources = Resources { delta_time: 0.1 }; 205 | 206 | systems_main(&resources, &mut world); 207 | ``` 208 | 209 | 210 | #### Destroying entities 211 | 212 | Let's say we want to create a rule that if player and enemies get within 3 units of eachother they should both be destroyed. 213 | This is how we might implement that: 214 | 215 | ```rust 216 | #[system] 217 | fn collide_enemy_and_players( 218 | world: &mut World, // we are destorying entities so it needs to be mutable 219 | players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities 220 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies 221 | ) { 222 | let mut entities_to_destroy: Vec = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration. 223 | 224 | world 225 | .with_query(players) 226 | .iter() 227 | .for_each(|(player_entity, player_position, _)| { 228 | world 229 | .with_query(enemies) 230 | .iter() 231 | .for_each(|(enemy_entity, enemy_position, _)| { 232 | if (player_position.0 - enemy_position.0).abs() < 3.0 233 | && (player_position.1 - enemy_position.1).abs() < 3.0 234 | { 235 | entities_to_destroy.push(*player_entity); 236 | entities_to_destroy.push(*enemy_entity); 237 | } 238 | }); 239 | }); 240 | 241 | for entity in entities_to_destroy { 242 | world.destroy(entity); 243 | } 244 | } 245 | ``` 246 | 247 | #### Get & At entities 248 | 249 | Get is identical to query but takes an Entity. 250 | At is identical to query but takes an index. 251 | 252 | Let's say you wanted an entity that follows a player. This is how you could implement that: 253 | 254 | Define a component for the companion 255 | ```rust 256 | #[component] 257 | struct CompanionComponent { 258 | target_entity: Option, 259 | } 260 | ``` 261 | 262 | Define the Companion Entity. It has a position and a companion component: 263 | ```rust 264 | #[entity] 265 | struct Companion { 266 | position: Position, 267 | companion_component: CompanionComponent, 268 | } 269 | ``` 270 | 271 | Now we need to write the companion system. 272 | For every companion we need to check if it has a target. 273 | If it has a target we need to check if target exists (it could have been deleted). 274 | If the target exists we get the *value of* target's position and set the companion's position with that value. 275 | 276 | We need to query for companions and their position as mutable. And we need to query for every entity that has a position. This means a companion could technically follow it self. 277 | 278 | ```rust 279 | #[system] 280 | fn companion_follow( 281 | world: &mut World, 282 | companions: Query<(&mut Position, &CompanionComponent)>, 283 | positions: Query<&Position>, 284 | ) { 285 | ``` 286 | 287 | Implementation: 288 | We can't simply iterate through the companions, get the target position and update the position because we can only have one borrow if the borrow is mutable (unless we use unsafe code). 289 | 290 | We can do what we did with destroying entities, but it will be slow. 291 | 292 | The solution is to iterate using index, only borrowing what we need for a short time: 293 | 294 | 295 | ```rust 296 | #[system] 297 | fn companion_follow( 298 | world: &mut World, 299 | companions: Query<(&mut Position, &CompanionComponent)>, 300 | positions: Query<&Position>, 301 | ) { 302 | for companion_idx in 0..world.with_query_mut(companions).len() { 303 | // iterate the count of companions 304 | if let Some(target_position) = world 305 | .with_query_mut(companions) 306 | .at_mut(companion_idx) // get the companion at index companion_idx 307 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 308 | .and_then(|companion_target_entity| { 309 | // then get the VALUE of target position (meaning we don't use a reference to the position) 310 | world 311 | .with_query(positions) 312 | .get(companion_target_entity) // get the position for the companion_target_entity 313 | .map(|p| (p.0, p.1)) // map to get the VALUE 314 | }) 315 | { 316 | if let Some((companion_position, _)) = 317 | world.with_query_mut(companions).at_mut(companion_idx) 318 | // Then simply get the companion position 319 | { 320 | // and update it to the target's position 321 | companion_position.0 = target_position.0; 322 | companion_position.1 = target_position.1; 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | # TODO: 330 | - [ ] Re use IDs of deleted entities 331 | 332 | -------------------------------------------------------------------------------- /zero_ecs_build/src/code_collection.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | fs, 4 | hash::{DefaultHasher, Hash, Hasher}, 5 | }; 6 | 7 | use quote::ToTokens; 8 | use syn::{Fields, Item, ItemFn, Meta, PathArguments, Type}; 9 | 10 | use crate::debug; 11 | 12 | #[derive(Debug)] 13 | pub struct EntityDef { 14 | pub name: String, 15 | pub fields: Vec, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Field { 20 | pub name: String, 21 | pub data_type: String, 22 | } 23 | 24 | #[derive(Debug, Default, Clone)] 25 | pub struct SystemDefParamReference { 26 | pub name: String, 27 | pub ty: String, 28 | pub mutable: bool, 29 | } 30 | 31 | #[derive(Debug)] 32 | pub enum SystemDefParam { 33 | Query(String), 34 | Reference(SystemDefParamReference), 35 | } 36 | 37 | #[derive(Debug, Default)] 38 | pub struct SystemDef { 39 | pub name: String, 40 | pub group: String, 41 | pub params: Vec, 42 | } 43 | 44 | #[derive(Debug, Default)] 45 | pub struct CollectedData { 46 | pub entities: Vec, 47 | pub queries: Vec, 48 | pub systems: Vec, 49 | } 50 | #[derive(Debug, Clone)] 51 | pub struct Query { 52 | pub mutable_fields: Vec, 53 | pub const_fields: Vec, 54 | } 55 | 56 | impl PartialEq for Query { 57 | fn eq(&self, other: &Self) -> bool { 58 | self.mutable_fields == other.mutable_fields && self.const_fields == other.const_fields 59 | } 60 | } 61 | 62 | impl Eq for Query {} 63 | 64 | impl Hash for Query { 65 | fn hash(&self, state: &mut H) { 66 | self.mutable_fields.hash(state); 67 | self.const_fields.hash(state); 68 | } 69 | } 70 | 71 | impl CollectedData { 72 | pub fn retain_unique_queries(&mut self) { 73 | let mut seen = HashSet::new(); 74 | self.queries.retain(|query| { 75 | let mut hasher = DefaultHasher::new(); 76 | query.hash(&mut hasher); 77 | let hash = hasher.finish(); 78 | seen.insert(hash) 79 | }); 80 | } 81 | } 82 | 83 | #[allow(clippy::single_match)] 84 | // I don't understand why this would complain. Looks good to me so added allow clippy 85 | #[allow(clippy::collapsible_match)] 86 | pub fn collect_data(path: &str) -> CollectedData { 87 | let mut entities = vec![]; 88 | let mut queries = vec![]; 89 | let mut systems = vec![]; 90 | 91 | let content = 92 | fs::read_to_string(path).unwrap_or_else(|_| panic!("Unable to read file {}", path)); 93 | 94 | let parsed_file = 95 | syn::parse_file(&content).unwrap_or_else(|_| panic!("Unable to parse file {}", path)); 96 | 97 | for item in parsed_file.items { 98 | match item { 99 | Item::Struct(item_struct) => { 100 | item_struct.attrs.iter().for_each(|attr| match &attr.meta { 101 | Meta::Path(path) => { 102 | if path.is_ident("entity") { 103 | let mut fields = vec![]; 104 | if let Fields::Named(named_fields) = &item_struct.fields { 105 | for field in &named_fields.named { 106 | let field = field.to_token_stream().to_string(); 107 | let field = field.split(':').collect::>(); 108 | 109 | let field_name = field[0].trim().to_string(); 110 | 111 | // split on space and take last element 112 | let field_name = 113 | field_name.split(' ').last().unwrap().to_string(); 114 | 115 | fields.push(Field { 116 | name: field_name, 117 | data_type: field[1].trim().to_string(), 118 | }); 119 | } 120 | } 121 | entities.push(EntityDef { 122 | name: item_struct.ident.to_string(), 123 | fields, 124 | }); 125 | } 126 | } 127 | _ => {} 128 | }); 129 | } 130 | Item::Fn(item_fn) => { 131 | let function_name = item_fn.sig.ident.to_string(); 132 | let system_group = is_system(&item_fn); 133 | if let Some(system_group) = system_group { 134 | let mut system_def = SystemDef { 135 | group: system_group, 136 | name: function_name.to_string(), 137 | params: vec![], 138 | }; 139 | 140 | //debug!("system: {}, group: {}", function_name, &system_def.group); 141 | // get all function parameters 142 | for input in &item_fn.sig.inputs { 143 | match input { 144 | syn::FnArg::Receiver(_) => {} 145 | syn::FnArg::Typed(pat_type) => { 146 | let param_name = pat_type.pat.to_token_stream().to_string(); 147 | let ty = pat_type.ty.clone(); 148 | match *ty { 149 | Type::Path(typed_path) => { 150 | for segment in typed_path.path.segments.iter() { 151 | let name = segment.ident.to_string(); 152 | 153 | //debug!("param: {}: {}", param_name, name); 154 | 155 | if name == "Query" { 156 | match &segment.arguments { 157 | PathArguments::AngleBracketed(arguments) => { 158 | if let Some(arg) = 159 | &arguments.args.iter().next() 160 | { 161 | if let syn::GenericArgument::Type(ty) = 162 | arg 163 | { 164 | if let Some(query) = 165 | collect_query(ty) 166 | { 167 | queries.push(query); 168 | system_def.params.push( 169 | SystemDefParam::Query( 170 | param_name.to_string(), 171 | ), 172 | ); 173 | } 174 | } 175 | } 176 | } 177 | _ => {} 178 | } 179 | } else { 180 | panic!( 181 | "unsupported type: {} for param: {}", 182 | name, param_name 183 | ); 184 | } 185 | } 186 | } 187 | Type::Reference(type_reference) => { 188 | let mutable = type_reference.mutability.is_some(); 189 | let elem_str = 190 | type_reference.elem.to_token_stream().to_string(); 191 | // debug!( 192 | // "reference: {}, of type: {}, mutable: {}", 193 | // param_name, elem_str, mutable 194 | // ); 195 | 196 | system_def.params.push(SystemDefParam::Reference( 197 | SystemDefParamReference { 198 | name: param_name.to_string(), 199 | ty: elem_str, 200 | mutable, 201 | }, 202 | )); 203 | } 204 | _ => { 205 | debug!("not typed: "); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | systems.push(system_def); 213 | } 214 | } 215 | _ => {} 216 | } 217 | } 218 | 219 | CollectedData { 220 | entities, 221 | queries, 222 | systems, 223 | } 224 | } 225 | 226 | fn collect_query(ty: &Type) -> Option { 227 | // handle reference, they have one value 228 | // "& mut Position" 229 | // "& Velocity" 230 | // also handle tuples, they have multiple, examples 231 | // (& mut Position , & Velocity) 232 | 233 | let query = match ty { 234 | Type::Reference(type_reference) => { 235 | let mut mutable_fields = vec![]; 236 | let mut const_fields = vec![]; 237 | let ty = type_reference.elem.clone(); 238 | if let Type::Path(type_path) = *ty { 239 | if type_reference.mutability.is_some() { 240 | mutable_fields.push(type_path.to_token_stream().to_string()); 241 | } else { 242 | const_fields.push(type_path.to_token_stream().to_string()); 243 | } 244 | } 245 | Some(Query { 246 | mutable_fields, 247 | const_fields, 248 | }) 249 | } 250 | Type::Tuple(type_tuple) => { 251 | let mut mutable_fields = vec![]; 252 | let mut const_fields = vec![]; 253 | for elem in &type_tuple.elems { 254 | if let Type::Reference(type_reference) = elem { 255 | if type_reference.mutability.is_some() { 256 | mutable_fields.push(type_reference.elem.to_token_stream().to_string()); 257 | } else { 258 | const_fields.push(type_reference.elem.to_token_stream().to_string()); 259 | } 260 | } 261 | } 262 | Some(Query { 263 | mutable_fields, 264 | const_fields, 265 | }) 266 | } 267 | _ => None, 268 | }; 269 | 270 | if let Some(query) = query { 271 | if query.mutable_fields.is_empty() && query.const_fields.is_empty() { 272 | None 273 | } else { 274 | Some(query) 275 | } 276 | } else { 277 | None 278 | } 279 | } 280 | 281 | //only used for debugging 282 | //pub fn _print_type(ty: &Type) { 283 | // match ty { 284 | // // Double dereference to get the `Type` from `&Box` 285 | // Type::Array(_) => debug!("Array"), 286 | // Type::BareFn(_) => debug!("BareFn"), 287 | // Type::Group(_) => debug!("Group"), 288 | // Type::ImplTrait(_) => debug!("ImplTrait"), 289 | // Type::Infer(_) => debug!("Infer"), 290 | // Type::Macro(_) => debug!("Macro"), 291 | // Type::Never(_) => debug!("Never"), 292 | // Type::Paren(_) => debug!("Paren"), 293 | // Type::Path(_) => debug!("Path"), 294 | // Type::Ptr(_) => debug!("Ptr"), 295 | // Type::Reference(_) => debug!("Reference"), 296 | // Type::Slice(_) => debug!("Slice"), 297 | // Type::TraitObject(_) => debug!("TraitObject"), 298 | // Type::Tuple(_) => debug!("Tuple"), 299 | // Type::Verbatim(_) => debug!("Verbatim"), 300 | // _ => {} // Add new variants here as needed 301 | // } 302 | //} 303 | 304 | fn is_system(item_fn: &ItemFn) -> Option { 305 | for attr in &item_fn.attrs { 306 | match &attr.meta { 307 | Meta::Path(path) => { 308 | if path.is_ident("system") { 309 | return Some("main".into()); 310 | } 311 | } 312 | Meta::List(list) => { 313 | if list.path.is_ident("system") { 314 | let tokens_str = list.tokens.to_string(); 315 | 316 | let mut kvp = tokens_str.split('='); 317 | 318 | if let Some(key) = kvp.next() { 319 | if let Some(value) = kvp.next() { 320 | let key = key.trim(); 321 | if key == "group" { 322 | // debug!("group: {}", value); 323 | 324 | return Some(value.trim().into()); 325 | } 326 | } 327 | } 328 | 329 | return Some("main".into()); 330 | } 331 | } 332 | Meta::NameValue(name_value) => { 333 | if name_value.path.is_ident("system") { 334 | debug!("name value: {}", name_value.to_token_stream()); 335 | return Some("main".into()); 336 | } 337 | } 338 | } 339 | } 340 | None 341 | } 342 | -------------------------------------------------------------------------------- /zero_ecs_build/src/code_generation.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | unused_attributes, 3 | dead_code, 4 | unused_imports, 5 | unused_variables, 6 | unused_macro_rules, 7 | unused_macros, 8 | unused_mut 9 | )] 10 | use core::arch; 11 | use itertools::Itertools; 12 | use quote::format_ident; 13 | use quote::quote; 14 | use quote::ToTokens; 15 | use std::collections::HashMap; 16 | use std::fs::File; 17 | use std::io::Write; 18 | use std::process::Command; 19 | use std::{env, fs, path::Path}; 20 | use syn::{Fields, Item, ItemFn, Meta, PatType, PathArguments, Type}; 21 | 22 | use crate::*; 23 | pub fn generate_default_queries(out_dir: &str) -> String { 24 | let file_name = "queries.rs"; 25 | 26 | let code_rs = quote! { 27 | 28 | use std::marker::PhantomData; 29 | 30 | 31 | #[derive(Default, Debug)] 32 | pub struct Query { 33 | phantom: PhantomData, 34 | } 35 | 36 | impl Clone for Query { 37 | fn clone(&self) -> Self { 38 | Query { 39 | phantom: PhantomData, 40 | } 41 | } 42 | } 43 | 44 | #[allow(dead_code)] 45 | impl Query { 46 | fn new() -> Query { 47 | Query { 48 | phantom: PhantomData, 49 | } 50 | } 51 | } 52 | 53 | impl<'a, T: 'a + Send> Query 54 | { 55 | pub fn iter(&self, world: &'a World) -> impl Iterator + 'a 56 | where 57 | World: QueryFrom<'a, T>, 58 | { 59 | world.query_from() 60 | } 61 | } 62 | impl<'a, T: 'a + Send> Query 63 | { 64 | pub fn par_iter(&self, world: &'a World) -> impl ParallelIterator + 'a 65 | where 66 | World: QueryFrom<'a, T>, 67 | { 68 | world.par_query_from() 69 | } 70 | } 71 | impl<'a, T: 'a + Send> Query { 72 | pub fn iter_mut(&self, world: &'a mut World) -> impl Iterator + 'a 73 | where 74 | World: QueryMutFrom<'a, T>, 75 | { 76 | world.query_mut_from() 77 | } 78 | } 79 | impl<'a, T: 'a + Send> Query 80 | { 81 | pub fn par_iter_mut(&self, world: &'a mut World) -> impl ParallelIterator + 'a 82 | where 83 | World: QueryMutFrom<'a, T>, 84 | { 85 | world.par_query_mut_from() 86 | } 87 | } 88 | impl<'a, T: 'a + Send> Query { 89 | pub fn get(&self, world: &'a World, entity: Entity) -> Option 90 | where 91 | World: QueryFrom<'a, T>, 92 | { 93 | world.get_from(entity) 94 | } 95 | } 96 | impl<'a, T: 'a + Send> Query { 97 | pub fn get_mut(&self, world: &'a mut World, entity: Entity) -> Option 98 | where 99 | World: QueryMutFrom<'a, T>, 100 | { 101 | world.get_mut_from(entity) 102 | } 103 | } 104 | 105 | // implement len 106 | impl<'a, T: 'a + Send> Query { 107 | pub fn len(&self, world: &'a World) -> usize 108 | where 109 | World: LenFrom<'a, T>, 110 | { 111 | world.len() 112 | } 113 | } 114 | 115 | // impl at_mut 116 | impl<'a, T: 'a + Send> Query { 117 | pub fn at_mut(&self, world: &'a mut World, index: usize) -> Option 118 | where 119 | World: QueryMutFrom<'a, T>, 120 | { 121 | world.at_mut(index) 122 | } 123 | } 124 | 125 | // impl at 126 | impl<'a, T: 'a + Send> Query { 127 | pub fn at(&self, world: &'a World, index: usize) -> Option 128 | where 129 | World: QueryFrom<'a, T>, 130 | { 131 | world.at(index) 132 | } 133 | } 134 | 135 | 136 | pub struct WithQueryMut<'a, T> { 137 | query: Query, 138 | world: &'a mut World, 139 | } 140 | pub struct WithQuery<'a, T> { 141 | query: Query, 142 | world: &'a World, 143 | } 144 | 145 | #[allow(dead_code)] 146 | impl<'a, T> WithQueryMut<'a, T> 147 | where World: QueryMutFrom<'a, T>, 148 | World: LenFrom<'a, T>, 149 | T: 'a + Send, 150 | { 151 | pub fn iter_mut(&'a mut self) -> impl Iterator + 'a { 152 | self.query.iter_mut(self.world) 153 | } 154 | pub fn par_iter_mut(&'a mut self) -> impl ParallelIterator + 'a { 155 | self.query.par_iter_mut(self.world) 156 | } 157 | pub fn get_mut(&'a mut self, entity: Entity) -> Option { 158 | self.query.get_mut(self.world, entity) 159 | } 160 | pub fn len(&'a mut self) -> usize { 161 | self.query.len(self.world) 162 | } 163 | pub fn at_mut(&'a mut self, index: usize) -> Option { 164 | self.query.at_mut(self.world, index) 165 | } 166 | pub fn is_empty(&'a mut self) -> bool { 167 | self.query.len(self.world) == 0 168 | } 169 | } 170 | 171 | #[allow(dead_code)] 172 | impl<'a, T> WithQuery<'a, T> 173 | where World: QueryFrom<'a, T>, 174 | World: LenFrom<'a, T>, 175 | T: 'a + Send, 176 | { 177 | pub fn iter(&'a self) -> impl Iterator + 'a { 178 | self.query.iter(self.world) 179 | } 180 | pub fn par_iter(&'a self) -> impl ParallelIterator + 'a { 181 | self.query.par_iter(self.world) 182 | } 183 | pub fn get(&'a self, entity: Entity) -> Option { 184 | self.query.get(self.world, entity) 185 | } 186 | pub fn len(&'a self) -> usize { 187 | self.query.len(self.world) 188 | } 189 | pub fn at(&'a self, index: usize) -> Option { 190 | self.query.at(self.world, index) 191 | } 192 | pub fn is_empty(&'a self) -> bool { 193 | self.query.len(self.world) == 0 194 | } 195 | } 196 | 197 | #[allow(dead_code)] 198 | impl World { 199 | pub fn with_query_mut<'a, T: 'a + Send>(&'a mut self, query: Query) -> WithQueryMut<'a, T> 200 | where 201 | World: QueryMutFrom<'a, T>, 202 | { 203 | WithQueryMut { 204 | query, 205 | world: self, 206 | } 207 | } 208 | } 209 | #[allow(dead_code)] 210 | impl World { 211 | pub fn with_query<'a, T: 'a + Send>(&'a self, query: Query) -> WithQuery<'a, T> 212 | where 213 | World: QueryFrom<'a, T>, 214 | { 215 | WithQuery { 216 | query, 217 | world: self, 218 | } 219 | } 220 | } 221 | }; 222 | write_token_stream_to_file(out_dir, file_name, &code_rs.to_string()) 223 | } 224 | 225 | pub fn generate_world_rs( 226 | out_dir: &str, 227 | include_files: &mut Vec, 228 | collected: &CollectedData, 229 | ) { 230 | let mut world_rs = vec![]; 231 | 232 | let mut world_fields = vec![]; 233 | 234 | let mut entity_types = collected.entities.iter().map(|entity| { 235 | let entity_name = &entity.name; 236 | fident!(entity_name) 237 | }); 238 | 239 | world_rs.push(quote! { 240 | #[allow(unused_imports)] 241 | use zero_ecs::*; 242 | #[derive(Debug, Clone, Copy)] 243 | enum EntityType { 244 | #(#entity_types),* 245 | } 246 | 247 | #[allow(dead_code)] 248 | #[derive(Debug, Clone, Copy)] 249 | pub struct Entity { 250 | entity_type: EntityType, 251 | id: usize 252 | } 253 | #[allow(dead_code)] 254 | impl World { 255 | pub fn query_mut<'a, T: 'a + Send>(&'a mut self) -> impl Iterator + 'a 256 | where 257 | World: QueryMutFrom<'a, T>, 258 | { 259 | QueryMutFrom::::query_mut_from(self) 260 | } 261 | pub fn par_query_mut<'a, T: 'a + Send>(&'a mut self) -> impl ParallelIterator + 'a 262 | where 263 | World: QueryMutFrom<'a, T>, 264 | { 265 | QueryMutFrom::::par_query_mut_from(self) 266 | } 267 | 268 | pub fn get_mut<'a, T: 'a + Send>(&'a mut self, entity: Entity) -> Option 269 | where 270 | World: QueryMutFrom<'a, T>, 271 | { 272 | QueryMutFrom::::get_mut_from(self, entity) 273 | } 274 | } 275 | #[allow(dead_code)] 276 | impl World { 277 | pub fn query<'a, T: 'a + Send>(&'a self) -> impl Iterator + 'a 278 | where 279 | World: QueryFrom<'a, T>, 280 | { 281 | QueryFrom::::query_from(self) 282 | } 283 | pub fn par_query<'a, T: 'a + Send>(&'a self) -> impl ParallelIterator + 'a 284 | where 285 | World: QueryFrom<'a, T>, 286 | { 287 | QueryFrom::::par_query_from(self) 288 | } 289 | pub fn get<'a, T: 'a + Send>(&'a self, entity: Entity) -> Option 290 | where 291 | World: QueryFrom<'a, T>, 292 | { 293 | QueryFrom::::get_from(self, entity) 294 | } 295 | } 296 | pub trait WorldCreate { 297 | fn create(&mut self, e: T) -> Entity; 298 | } 299 | }); 300 | 301 | let mut match_destroy_rs = vec![]; 302 | 303 | for entity in collected.entities.iter() { 304 | let entity_name = &entity.name; 305 | let field_name = fident!(singular_to_plural(&pascal_case_to_snake_case(entity_name))); 306 | let archetype_type = fident!(singular_to_plural(entity_name)); 307 | 308 | world_fields.push(quote! { 309 | #field_name: #archetype_type, 310 | }); 311 | 312 | let archetype_fields = entity.fields.iter().map(|field| { 313 | let field_name = format_ident!("{}", singular_to_plural(&field.name)); 314 | let field_type = format_ident!("{}", &field.data_type); 315 | quote! { 316 | #field_name: Vec<#field_type>, 317 | } 318 | }); 319 | 320 | world_rs.push(quote! { 321 | 322 | #[derive(Default, Debug)] 323 | struct #archetype_type { 324 | #(#archetype_fields)* 325 | next_id: usize, 326 | index_lookup: Vec>, 327 | } 328 | 329 | #[allow(dead_code)] 330 | impl #archetype_type { 331 | fn len(&self) -> usize { 332 | self.entities.len() 333 | } 334 | } 335 | }); 336 | 337 | world_rs.push(quote! { 338 | #[allow(dead_code)] 339 | impl #archetype_type { 340 | fn query_mut<'a, T: 'a>(&'a mut self) -> impl Iterator + 'a 341 | where 342 | #archetype_type: QueryMutFrom<'a, T>, 343 | T: 'a + Send, 344 | { 345 | QueryMutFrom::::query_mut_from(self) 346 | } 347 | fn par_query_mut<'a, T: 'a>(&'a mut self) -> impl ParallelIterator + 'a 348 | where 349 | #archetype_type: QueryMutFrom<'a, T>, 350 | T: 'a + Send, 351 | { 352 | QueryMutFrom::::par_query_mut_from(self) 353 | } 354 | fn get_mut<'a, T: 'a>(&'a mut self, entity: Entity) -> Option 355 | where 356 | #archetype_type: QueryMutFrom<'a, T>, 357 | T: 'a + Send, 358 | { 359 | QueryMutFrom::::get_mut_from(self, entity) 360 | } 361 | } 362 | }); 363 | world_rs.push(quote! { 364 | #[allow(dead_code)] 365 | impl #archetype_type { 366 | fn query<'a, T: 'a>(&'a self) -> impl Iterator + 'a 367 | where 368 | #archetype_type: QueryFrom<'a, T>, 369 | T: 'a + Send, 370 | { 371 | QueryFrom::::query_from(self) 372 | } 373 | fn par_query<'a, T: 'a>(&'a self) -> impl ParallelIterator + 'a 374 | where 375 | #archetype_type: QueryFrom<'a, T>, 376 | T: 'a + Send, 377 | { 378 | QueryFrom::::par_query_from(self) 379 | } 380 | fn get<'a, T: 'a>(&'a self, entity: Entity) -> Option 381 | where 382 | #archetype_type: QueryFrom<'a, T>, 383 | T: 'a + Send, 384 | { 385 | QueryFrom::::get_from(self, entity) 386 | } 387 | } 388 | }); 389 | 390 | let push_lines = entity 391 | .fields 392 | .iter() 393 | .filter(|e| e.data_type != "Entity") 394 | .map(|field| { 395 | let component_field_name = format_ident!("{}", singular_to_plural(&field.name)); 396 | let component_name = fident!(&field.name); 397 | 398 | quote! { 399 | self.#field_name.#component_field_name.push(e.#component_name); 400 | } 401 | }); 402 | 403 | let entity_name = fident!(entity_name); 404 | 405 | world_rs.push(quote! { 406 | impl WorldCreate<#entity_name> for World { 407 | fn create(&mut self, e: #entity_name) -> Entity { 408 | self.#field_name.index_lookup.push(Some(self.#field_name.entities.len())); 409 | let entity = Entity { 410 | entity_type: EntityType::#entity_name, 411 | id: self.#field_name.next_id, 412 | }; 413 | self.#field_name.entities.push(entity); 414 | #(#push_lines)* 415 | self.#field_name.next_id += 1; 416 | entity 417 | } 418 | } 419 | }); 420 | 421 | let pop_and_drop_code = entity.fields.iter().map(|field| { 422 | let field_name = format_ident!("{}", singular_to_plural(&field.name)); 423 | let component_field_name = format_ident!("{}", singular_to_plural(&field.name)); 424 | quote! { 425 | self.#component_field_name.swap(old_index, last_index); 426 | self.#component_field_name.pop(); 427 | } 428 | }); 429 | 430 | let pop_and_drop_code_copy = pop_and_drop_code.clone(); 431 | 432 | world_rs.push(quote! { 433 | #[allow(dead_code)] 434 | impl #archetype_type { 435 | fn destroy(&mut self, entity: Entity) { 436 | if let Some(&Some(old_index)) = self.index_lookup.get(entity.id) { 437 | self.index_lookup[entity.id] = None; 438 | 439 | let last_index = self.entities.len() - 1; 440 | 441 | if old_index != last_index { 442 | let last_entity = self.entities[last_index]; 443 | 444 | #(#pop_and_drop_code)* 445 | 446 | self.index_lookup[last_entity.id] = Some(old_index); 447 | } else { 448 | #(#pop_and_drop_code_copy)* 449 | } 450 | } 451 | } 452 | } 453 | }); 454 | 455 | match_destroy_rs.push(quote! { 456 | EntityType::#entity_name => self.#field_name.destroy(entity), 457 | }); 458 | } 459 | 460 | world_rs.push(quote! { 461 | #[allow(dead_code)] 462 | impl World { 463 | fn destroy(&mut self, entity: Entity) { 464 | match entity.entity_type { 465 | #(#match_destroy_rs)* 466 | } 467 | } 468 | } 469 | }); 470 | 471 | world_rs.push(quote! { 472 | #[derive(Default, Debug)] 473 | pub struct World { 474 | #(#world_fields)* 475 | } 476 | }); 477 | 478 | let world_rs = quote! { 479 | #(#world_rs)* 480 | }; 481 | 482 | include_files.push(write_token_stream_to_file( 483 | out_dir, 484 | "world.rs", 485 | &world_rs.to_string(), 486 | )); 487 | } 488 | 489 | pub fn singular_to_plural(name: &str) -> String { 490 | let last_char = name.chars().last().unwrap(); 491 | if last_char == 'y' { 492 | format!("{}ies", &name[0..name.len() - 1]) 493 | } else { 494 | format!("{}s", name) 495 | } 496 | } 497 | pub fn pascal_case_to_snake_case(name: &str) -> String { 498 | // This function formats SomeString to some_string 499 | 500 | let mut result = String::new(); 501 | for (i, c) in name.chars().enumerate() { 502 | if c.is_uppercase() { 503 | if i > 0 { 504 | result.push('_'); 505 | } 506 | result.push(c.to_lowercase().next().unwrap()); 507 | } else { 508 | result.push(c); 509 | } 510 | } 511 | result 512 | } 513 | 514 | pub fn generate_queries(out_dir: &str, include_files: &mut Vec, collected: &CollectedData) { 515 | let mut code_rs = vec![]; 516 | 517 | code_rs.push(quote! { 518 | //use zero_ecs::ParallelIterator; 519 | #[allow(unused_imports)] 520 | use zero_ecs::izip; 521 | #[allow(unused_imports)] 522 | use zero_ecs::chain; 523 | 524 | pub trait LenFrom<'a, T> 525 | where 526 | T: 'a + Send 527 | { 528 | fn len(&'a self) -> usize; 529 | fn is_empty(&'a self) -> bool { 530 | self.len() == 0 531 | } 532 | } 533 | 534 | 535 | pub trait QueryFrom<'a, T> 536 | where 537 | T: 'a + Send 538 | { 539 | fn query_from(&'a self) -> impl Iterator; 540 | fn par_query_from(&'a self) -> impl ParallelIterator; 541 | fn get_from(&'a self, entity: Entity) -> Option; 542 | fn at(&'a self, index: usize) -> Option; 543 | } 544 | pub trait QueryMutFrom<'a, T> 545 | where 546 | T: 'a + Send 547 | { 548 | fn query_mut_from(&'a mut self) -> impl Iterator; 549 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator; 550 | fn get_mut_from(&'a mut self, entity: Entity) -> Option; 551 | fn at_mut(&'a mut self, index: usize) -> Option; 552 | } 553 | }); 554 | 555 | for query in collected.queries.iter() { 556 | let mutable = !query.mutable_fields.is_empty(); 557 | let matching_entities: Vec<&EntityDef> = collected 558 | .entities 559 | .iter() 560 | .filter(|entity| { 561 | let mut all_fields_present = true; 562 | 563 | for query_field in query.mutable_fields.iter() { 564 | if !entity 565 | .fields 566 | .iter() 567 | .any(|entity_field| entity_field.data_type == *query_field) 568 | { 569 | all_fields_present = false; 570 | break; 571 | } 572 | } 573 | for query_field in query.const_fields.iter() { 574 | if !entity 575 | .fields 576 | .iter() 577 | .any(|entity_field| entity_field.data_type == *query_field) 578 | { 579 | all_fields_present = false; 580 | break; 581 | } 582 | } 583 | all_fields_present 584 | }) 585 | .collect(); 586 | let mut data_types = vec![]; 587 | 588 | for field in query.mutable_fields.iter() { 589 | let field_data_type = fident!(field); 590 | data_types.push(quote! { 591 | &'a mut #field_data_type 592 | }); 593 | } 594 | for field in query.const_fields.iter() { 595 | let field_data_type = fident!(field); 596 | 597 | data_types.push(quote! { 598 | &'a #field_data_type 599 | }); 600 | } 601 | 602 | let mut match_get_rs = vec![]; 603 | 604 | for entity in matching_entities.iter() { 605 | let entity_name = fident!(entity.name); 606 | 607 | let mut field_quotes = vec![]; 608 | let mut par_field_quotes = vec![]; 609 | let mut get_quotes = vec![]; 610 | 611 | for field in query.mutable_fields.iter() { 612 | let field_name = fident!(singular_to_plural( 613 | entity 614 | .fields 615 | .iter() 616 | .find(|f| f.data_type == *field) 617 | .unwrap() 618 | .name 619 | .as_str() 620 | )); 621 | 622 | field_quotes.push(quote! { 623 | self.#field_name.iter_mut() 624 | }); 625 | par_field_quotes.push(quote! { 626 | self.#field_name.par_iter_mut() 627 | }); 628 | get_quotes.push(quote! { 629 | self.#field_name.get_mut(index)? 630 | }); 631 | } 632 | for field in query.const_fields.iter() { 633 | let field_name = fident!(singular_to_plural( 634 | entity 635 | .fields 636 | .iter() 637 | .find(|f| f.data_type == *field) 638 | .unwrap() 639 | .name 640 | .as_str() 641 | )); 642 | 643 | field_quotes.push(quote! { 644 | self.#field_name.iter() 645 | }); 646 | par_field_quotes.push(quote! { 647 | self.#field_name.par_iter() 648 | }); 649 | get_quotes.push(quote! { 650 | self.#field_name.get(index)? 651 | }); 652 | } 653 | 654 | let archetype_type = fident!(singular_to_plural(&entity.name)); 655 | let archetype_field_name = 656 | fident!(singular_to_plural(&pascal_case_to_snake_case(&entity.name))); 657 | 658 | code_rs.push(quote! { 659 | #[allow(unused_parens)] 660 | impl<'a> LenFrom<'a, (#(#data_types),*)> for #archetype_type { 661 | fn len(&'a self) -> usize { 662 | self.entities.len() 663 | } 664 | } 665 | }); 666 | 667 | if mutable { 668 | code_rs.push(quote! { 669 | #[allow(unused_parens, clippy::needless_question_mark, clippy::double_parens)] 670 | impl<'a> QueryMutFrom<'a, (#(#data_types),*)> for #archetype_type { 671 | fn query_mut_from(&'a mut self) -> impl Iterator { 672 | izip!(#(#field_quotes),*) 673 | } 674 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator { 675 | izip_par!(#(#par_field_quotes),*) 676 | } 677 | fn get_mut_from(&'a mut self, entity: Entity) -> Option<(#(#data_types),*)> { 678 | if let Some(&Some(index)) = self.index_lookup.get(entity.id) { 679 | Some((#(#get_quotes),*)) 680 | } else { 681 | None 682 | } 683 | } 684 | fn at_mut(&'a mut self, index: usize) -> Option<(#(#data_types),*)> 685 | { 686 | Some((#(#get_quotes),*)) 687 | } 688 | } 689 | }); 690 | match_get_rs.push(quote! { 691 | EntityType::#entity_name => self.#archetype_field_name.get_mut_from(entity), 692 | }); 693 | } else { 694 | code_rs.push(quote! { 695 | #[allow(unused_parens, clippy::needless_question_mark, clippy::double_parens)] 696 | impl<'a> QueryFrom<'a, (#(#data_types),*)> for #archetype_type { 697 | fn query_from(&'a self) -> impl Iterator { 698 | izip!(#(#field_quotes),*) 699 | } 700 | fn par_query_from(&'a self) -> impl ParallelIterator { 701 | izip_par!(#(#par_field_quotes),*) 702 | } 703 | fn get_from(&'a self, entity: Entity) -> Option<(#(#data_types),*)> { 704 | if let Some(&Some(index)) = self.index_lookup.get(entity.id) { 705 | Some((#(#get_quotes),*)) 706 | } else { 707 | None 708 | } 709 | } 710 | fn at(&'a self, index: usize) -> Option<(#(#data_types),*)> 711 | { 712 | Some((#(#get_quotes),*)) 713 | } 714 | } 715 | }); 716 | match_get_rs.push(quote! { 717 | EntityType::#entity_name => self.#archetype_field_name.get_from(entity), 718 | }); 719 | } 720 | } 721 | let sum_args: Vec<_> = matching_entities 722 | .iter() 723 | .map(|entity| { 724 | let property_name = format_ident!( 725 | "{}", 726 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 727 | ); 728 | quote! { self.#property_name.len() } 729 | }) 730 | .collect(); 731 | 732 | if !sum_args.is_empty() { 733 | code_rs.push(quote! { 734 | #[allow(unused_parens, unused_variables, unused_assignments)] 735 | impl<'a> LenFrom<'a, (#(#data_types),*)> for World { 736 | fn len(&'a self) -> usize { 737 | sum!(#(#sum_args),*) 738 | } 739 | } 740 | }); 741 | } else { 742 | code_rs.push(quote! { 743 | #[allow(unused_parens, unused_variables, unused_assignments)] 744 | impl<'a> LenFrom<'a, (#(#data_types),*)> for World { 745 | fn len(&'a self) -> usize { 746 | 0 747 | } 748 | } 749 | }); 750 | } 751 | 752 | if mutable { 753 | let chain_args: Vec<_> = matching_entities 754 | .iter() 755 | .map(|entity| { 756 | let property_name = format_ident!( 757 | "{}", 758 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 759 | ); 760 | quote! { self.#property_name.query_mut() } 761 | }) 762 | .collect(); 763 | let par_chain_args: Vec<_> = matching_entities 764 | .iter() 765 | .map(|entity| { 766 | let property_name = format_ident!( 767 | "{}", 768 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 769 | ); 770 | quote! { self.#property_name.par_query_mut() } 771 | }) 772 | .collect(); 773 | let at_mut_args: Vec<_> = matching_entities 774 | .iter() 775 | .map(|entity| { 776 | let property_name = format_ident!( 777 | "{}", 778 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 779 | ); 780 | quote! { 781 | { 782 | let len = self.#property_name.len(); 783 | if index < len { 784 | return self.#property_name.at_mut(index); 785 | } 786 | index -= len; 787 | } 788 | } 789 | }) 790 | .collect(); 791 | 792 | code_rs.push(quote! { 793 | #[allow(unused_parens, unused_variables, unused_assignments)] 794 | impl<'a> QueryMutFrom<'a, (#(#data_types),*)> for World { 795 | fn query_mut_from(&'a mut self) -> impl Iterator { 796 | chain!(#(#chain_args),*) 797 | } 798 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator { 799 | chain_par!(#(#par_chain_args),*) 800 | } 801 | #[allow(unreachable_patterns, clippy::match_single_binding)] 802 | fn get_mut_from(&'a mut self, entity: Entity) -> Option<(#(#data_types),*)> { 803 | match entity.entity_type { 804 | #(#match_get_rs)* 805 | _ => None 806 | } 807 | } 808 | #[allow(unused_mut)] 809 | fn at_mut(&'a mut self, index: usize) -> Option<(#(#data_types),*)> 810 | { 811 | let mut index = index; 812 | #(#at_mut_args)* 813 | None 814 | } 815 | } 816 | }) 817 | } else { 818 | let chain_args: Vec<_> = matching_entities 819 | .iter() 820 | .map(|entity| { 821 | let property_name = format_ident!( 822 | "{}", 823 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 824 | ); 825 | quote! { self.#property_name.query() } 826 | }) 827 | .collect(); 828 | let par_chain_args: Vec<_> = matching_entities 829 | .iter() 830 | .map(|entity| { 831 | let property_name = format_ident!( 832 | "{}", 833 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 834 | ); 835 | quote! { self.#property_name.par_query() } 836 | }) 837 | .collect(); 838 | let at_args: Vec<_> = matching_entities 839 | .iter() 840 | .map(|entity| { 841 | let property_name = format_ident!( 842 | "{}", 843 | singular_to_plural(&pascal_case_to_snake_case(&entity.name)) 844 | ); 845 | quote! { 846 | { 847 | let len = self.#property_name.len(); 848 | if index < len { 849 | return self.#property_name.at(index); 850 | } 851 | index -= len; 852 | } 853 | } 854 | }) 855 | .collect(); 856 | code_rs.push(quote! { 857 | #[allow(unused_parens, unused_variables, unused_assignments)] 858 | impl<'a> QueryFrom<'a, (#(#data_types),*)> for World { 859 | fn query_from(&'a self) -> impl Iterator { 860 | chain!(#(#chain_args),*) 861 | } 862 | fn par_query_from(&'a self) -> impl ParallelIterator { 863 | chain_par!(#(#par_chain_args),*) 864 | } 865 | #[allow(unreachable_patterns, clippy::match_single_binding)] 866 | fn get_from(&'a self, entity: Entity) -> Option<(#(#data_types),*)> { 867 | match entity.entity_type { 868 | #(#match_get_rs)* 869 | _ => None 870 | } 871 | } 872 | 873 | #[allow(unused_mut)] 874 | fn at(&'a self, index: usize) -> Option<(#(#data_types),*)> 875 | { 876 | let mut index = index; 877 | #(#at_args)* 878 | None 879 | } 880 | } 881 | }) 882 | } 883 | } 884 | 885 | let code_rs = quote! { 886 | #(#code_rs)* 887 | }; 888 | 889 | include_files.push(write_token_stream_to_file( 890 | out_dir, 891 | "implementations.rs", 892 | &code_rs.to_string(), 893 | )); 894 | } 895 | pub fn generate_systems(out_dir: &str, include_files: &mut Vec, collected: &CollectedData) { 896 | let mut code_rs = vec![]; 897 | 898 | // distinct groups 899 | let groups: Vec<_> = collected 900 | .systems 901 | .iter() 902 | .map(|s| &s.group) 903 | .unique() 904 | .collect(); 905 | 906 | //debug!("{:?}", groups); 907 | 908 | for group in groups.iter() { 909 | let mut calls = vec![]; 910 | 911 | let mut call_params: HashMap<(String, String), SystemDefParamReference> = HashMap::new(); 912 | 913 | for system in collected 914 | .systems 915 | .iter() 916 | .filter(|s| &s.group == *group) 917 | .sorted_by(|a, b| a.name.cmp(&b.name)) 918 | { 919 | let mut params_rs = vec![]; 920 | 921 | for param in system.params.iter() { 922 | match param { 923 | SystemDefParam::Query(name) => { 924 | params_rs.push(quote! { 925 | Query::new(), 926 | }); 927 | } 928 | SystemDefParam::Reference(reference) => { 929 | let name = fident!(reference.name); 930 | 931 | params_rs.push(quote! { 932 | #name, 933 | }); 934 | 935 | let key = (reference.name.clone(), reference.ty.clone()); 936 | let item = reference.clone(); 937 | call_params 938 | .entry(key) 939 | .and_modify(|e| { 940 | if reference.mutable { 941 | e.mutable = true; 942 | } 943 | }) 944 | .or_insert(item); 945 | } 946 | } 947 | } 948 | 949 | let system_function_name = fident!(&system.name); 950 | 951 | calls.push(quote! { 952 | #system_function_name(#(#params_rs)*); 953 | }) 954 | } 955 | 956 | let function_name = format_ident!("systems_{}", group); 957 | 958 | // get values of call_params, ignoring the key 959 | let call_params: Vec<_> = call_params.values().collect(); 960 | 961 | // order call_params by name 962 | let call_params = call_params 963 | .iter() 964 | .sorted_by(|a, b| a.name.cmp(&b.name)) 965 | .collect::>(); 966 | 967 | let call_params_rs = call_params.iter().map(|r| { 968 | let name = fident!(r.name); 969 | let ty = fident!(r.ty); 970 | 971 | if r.mutable { 972 | quote! { 973 | #name: &mut #ty 974 | } 975 | } else { 976 | quote! { 977 | 978 | #name: &#ty 979 | } 980 | } 981 | }); 982 | 983 | code_rs.push(quote! { 984 | #[allow(private_interfaces)] 985 | pub fn #function_name(#(#call_params_rs),*) { 986 | #(#calls)* 987 | } 988 | }) 989 | } 990 | 991 | let code_rs = quote! { 992 | #(#code_rs)* 993 | }; 994 | 995 | include_files.push(write_token_stream_to_file( 996 | out_dir, 997 | "systems.rs", 998 | &code_rs.to_string(), 999 | )); 1000 | } 1001 | pub fn generate_copy_traits( 1002 | out_dir: &str, 1003 | include_files: &mut Vec, 1004 | collected: &CollectedData, 1005 | ) { 1006 | let mut code_rs = vec![]; 1007 | 1008 | code_rs.push(quote! {}); 1009 | 1010 | for q in collected.queries.iter() { 1011 | let mut data_types = vec![]; 1012 | 1013 | for field in q.mutable_fields.iter() { 1014 | let field_data_type = fident!(field); 1015 | data_types.push(quote! { 1016 | &mut #field_data_type 1017 | }); 1018 | } 1019 | 1020 | for field in q.const_fields.iter() { 1021 | let field_data_type = fident!(field); 1022 | data_types.push(quote! { 1023 | &#field_data_type 1024 | }); 1025 | } 1026 | 1027 | if data_types.len() > 1 { 1028 | code_rs.push(quote! { 1029 | impl Copy for Query<(#(#data_types),*)> {} 1030 | }); 1031 | } else if let Some(data_type) = data_types.first() { 1032 | code_rs.push(quote! { 1033 | impl Copy for Query<#data_type> {} 1034 | }); 1035 | } 1036 | } 1037 | 1038 | let code_rs = quote! { 1039 | #(#code_rs)* 1040 | }; 1041 | 1042 | include_files.push(write_token_stream_to_file( 1043 | out_dir, 1044 | "copy_traits.rs", 1045 | &code_rs.to_string(), 1046 | )); 1047 | } 1048 | -------------------------------------------------------------------------------- /zero_ecs_build/src/file.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path, process::Command}; 2 | 3 | pub fn write_token_stream_to_file(out_dir: &str, file_name: &str, code: &str) -> String { 4 | let dest_path = Path::new(&out_dir).join(file_name); 5 | fs::write(&dest_path, code) 6 | .unwrap_or_else(|_| panic!("failed to write to file: {}", file_name)); 7 | format_file(dest_path.to_str().unwrap()); 8 | format!("/{}", file_name) 9 | } 10 | pub fn format_file(file_name: &str) { 11 | Command::new("rustfmt") 12 | .arg(file_name) 13 | .output() 14 | .expect("failed to execute rustfmt"); 15 | } 16 | -------------------------------------------------------------------------------- /zero_ecs_build/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod code_collection; 2 | mod code_generation; 3 | mod file; 4 | mod macros; 5 | 6 | use quote::quote; 7 | use std::io::Write; 8 | use std::{env, fs, path::Path}; 9 | 10 | pub use code_collection::*; 11 | pub use code_generation::*; 12 | pub use file::*; 13 | use glob::glob; 14 | 15 | pub fn generate_ecs(source_glob: &str) { 16 | let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("could not get manifest dir"); 17 | let pattern = format!("{}/{}", manifest_dir, source_glob); 18 | let mut include_files = vec![]; 19 | 20 | let out_dir = std::env::var("OUT_DIR").unwrap(); 21 | 22 | let mut collected_data = CollectedData::default(); 23 | 24 | for file in glob(&pattern).expect("invalid glob") { 25 | match file { 26 | Ok(path) => { 27 | let path_str = path.display().to_string(); 28 | 29 | let collected = collect_data(&path_str); 30 | 31 | collected_data.entities.extend(collected.entities); 32 | collected_data.queries.extend(collected.queries); 33 | collected_data.systems.extend(collected.systems); 34 | } 35 | Err(e) => eprintln!("Error processing path: {}", e), 36 | } 37 | } 38 | 39 | collected_data.entities.iter_mut().for_each(|entity| { 40 | entity.fields.push(Field { 41 | name: "entity".into(), 42 | data_type: "Entity".into(), 43 | }) 44 | }); 45 | 46 | collected_data.retain_unique_queries(); 47 | 48 | //debug!("{:?}", collected_data); 49 | 50 | include_files.push(generate_default_queries(&out_dir)); 51 | generate_world_rs(&out_dir, &mut include_files, &collected_data); 52 | generate_queries(&out_dir, &mut include_files, &collected_data); 53 | generate_systems(&out_dir, &mut include_files, &collected_data); 54 | generate_copy_traits(&out_dir, &mut include_files, &collected_data); 55 | 56 | let main_file = Path::new(&out_dir).join("zero_ecs.rs"); 57 | 58 | let mut include_rs = vec![]; 59 | for file in include_files { 60 | include_rs.push(quote! { 61 | include!(concat!(env!("OUT_DIR"), #file)); 62 | }); 63 | } 64 | 65 | let zero_ecs_rs = quote! { 66 | #(#include_rs)* 67 | }; 68 | 69 | let mut f = fs::File::create(main_file).expect("Unable to create file"); 70 | 71 | write!(f, "{}", zero_ecs_rs).expect("Unable to write data to file"); 72 | } 73 | -------------------------------------------------------------------------------- /zero_ecs_build/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_macros, unused_imports)] 2 | pub use quote::format_ident; 3 | #[macro_export] 4 | macro_rules! debug { 5 | ($($arg:tt)*) => { 6 | println!("cargo:warning={}", format_args!($($arg)*)); 7 | }; 8 | } 9 | #[macro_export] 10 | macro_rules! fident { 11 | ($name:expr) => { 12 | format_ident!("{}", $name) 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /zero_ecs_macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_macros" 3 | version = "0.2.22" 4 | edition = "2021" 5 | description = "Procedural macros for Build scripts for: ZeroECS: an Entity Component System (ECS), using only zero-cost abstractions" 6 | repository = "https://github.com/JohanNorberg/zero_ecs" 7 | documentation = "https://github.com/JohanNorberg/zero_ecs#readme" 8 | license = "MIT OR Apache-2.0" 9 | authors = ["Johan Norberg "] 10 | keywords = ["ecs", "zero_ecs"] 11 | categories = ["game-development", "data-structures"] 12 | readme = "README.md" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = { version = "2.0.51", features = ["full"] } 19 | quote = "1.0.35" 20 | -------------------------------------------------------------------------------- /zero_ecs_macros/README.md: -------------------------------------------------------------------------------- 1 | # Zero ECS 2 | 3 | Zero ECS is an Entity Component System that is written with 4 goals 4 | 1. Only use zero cost abstractions - no use of dyn and Box and stuff [zero-cost-abstractions](https://doc.rust-lang.org/beta/embedded-book/static-guarantees/zero-cost-abstractions.html). 5 | 2. No use of unsafe rust code. 6 | 3. Be very user friendly. The user should write as little boilerplate as possible. 7 | 4. Be very fast 8 | 9 | It achieves this by generating all code at compile time, using a combination of macros and build scripts. 10 | 11 | ## Instructions 12 | 13 | Create a new project 14 | 15 | ```sh 16 | cargo new zero_ecs_example 17 | cd zero_ecs_example 18 | ``` 19 | 20 | Add the dependencies 21 | 22 | ```sh 23 | cargo add zero_ecs 24 | cargo add zero_ecs_build --build 25 | ``` 26 | 27 | Your Cargo.toml should look something like this: 28 | 29 | ```sh 30 | [dependencies] 31 | zero_ecs = "*" 32 | 33 | [build-dependencies] 34 | zero_ecs_build = "*" 35 | ``` 36 | 37 | Create `build.rs` 38 | 39 | ```sh 40 | touch build.rs 41 | ``` 42 | 43 | Edit `build.rs` to call the zero_ecs's build generation code. 44 | 45 | ```rust 46 | use zero_ecs_build::*; 47 | fn main() { 48 | generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs 49 | } 50 | ``` 51 | 52 | This will generate the entity component system based on the component, entities and systems in main.rs. 53 | It accepts a glob so you can use wild cards. 54 | 55 | ```rust 56 | use zero_ecs_build::*; 57 | fn main() { 58 | generate_ecs("src/**/*.rs"); // look in all *.rs files in src. 59 | } 60 | ``` 61 | 62 | ## Using the ECS 63 | 64 | ### Include ECS 65 | 66 | In `main.rs` 67 | Include the ECS like so: 68 | 69 | ```rust 70 | include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs")); 71 | ``` 72 | 73 | ### Components 74 | 75 | Define some components: 76 | 77 | Position and velocity has x and y 78 | 79 | ```rust 80 | #[component] 81 | struct Position(f32, f32); 82 | 83 | #[component] 84 | struct Velocity(f32, f32); 85 | ``` 86 | 87 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 88 | 89 | ```rust 90 | #[component] 91 | struct EnemyComponent; 92 | 93 | #[component] 94 | struct PlayerComponent; 95 | ``` 96 | 97 | ### Entities 98 | 99 | Entities are a collection of components, they may also be referred to as archetypes, or bundles, or game objects. 100 | Note that once "in" the ECS. An Entity is simply an ID that can be copied. 101 | 102 | In our example, we define an enemy and a player, they both have position and velocity but can be differentiated by their "tag" components. 103 | 104 | ```rust 105 | #[entity] 106 | struct Enemy { 107 | position: Position, 108 | velocity: Velocity, 109 | enemy_component: EnemyComponent, 110 | } 111 | 112 | #[entity] 113 | struct Player { 114 | position: Position, 115 | velocity: Velocity, 116 | player_component: PlayerComponent, 117 | } 118 | ``` 119 | 120 | ### Systems 121 | 122 | Systems run the logic for the application. They can accept references, mutable references and queries. 123 | 124 | In our example we can create a system that simply prints the position of all entities 125 | 126 | ```rust 127 | #[system] 128 | fn print_positions(world: &World, query: Query<&Position>) { 129 | world.with_query(query).iter().for_each(|position| { 130 | println!("Position: {:?}", position); 131 | }); 132 | } 133 | ``` 134 | 135 | #### Explained: 136 | 137 | - world: &World - Since the system doesn't modify anything, it can be an immutable reference 138 | - query: Query<&Position> - We want to query the world for all positions 139 | - world.with_query(query).iter() - creates an iterator over all Position components 140 | 141 | ### Creating entities and calling system 142 | 143 | In our `fn main` change it to create 10 enemies and 10 players, 144 | Also add the `systems_main(&world);` to call all systems. 145 | 146 | ```rust 147 | fn main() { 148 | let mut world = World::default(); 149 | 150 | for i in 0..10 { 151 | world.create(Enemy { 152 | position: Position(i as f32, 5.0), 153 | velocity: Velocity(0.0, 1.0), 154 | ..Default::default() 155 | }); 156 | 157 | world.create(Player { 158 | position: Position(5.0, i as f32), 159 | velocity: Velocity(1.0, 0.0), 160 | ..Default::default() 161 | }); 162 | } 163 | 164 | systems_main(&world); 165 | } 166 | ``` 167 | 168 | Running the program now, will print the positions of the entities. 169 | 170 | ## More advanced 171 | 172 | Continuing our example 173 | 174 | ### mutating systems 175 | 176 | Most systems will mutate the world state and needs additional resources, like texture managers, time managers, input managers etc. 177 | A good practice is to group them in a Resources struct. (But Not nescessary) 178 | 179 | ```rust 180 | struct Resources { 181 | delta_time: f32, 182 | } 183 | 184 | #[system] 185 | fn apply_velocity( 186 | world: &mut World, // world mut be mutable 187 | resources: &Resources, // we need the delta time 188 | query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not. 189 | ) { 190 | world 191 | .with_query_mut(query) // we call with_query_mut because it's a mutable query 192 | .iter_mut() // iterating mutable 193 | .for_each(|(position, velocity)| { 194 | position.0 += velocity.0 * resources.delta_time; 195 | position.1 += velocity.1 * resources.delta_time; 196 | }); 197 | } 198 | 199 | ``` 200 | 201 | We also have to change the main function to include resources in the call. 202 | 203 | ```rust 204 | let resources = Resources { delta_time: 0.1 }; 205 | 206 | systems_main(&resources, &mut world); 207 | ``` 208 | 209 | 210 | #### Destroying entities 211 | 212 | Let's say we want to create a rule that if player and enemies get within 3 units of eachother they should both be destroyed. 213 | This is how we might implement that: 214 | 215 | ```rust 216 | #[system] 217 | fn collide_enemy_and_players( 218 | world: &mut World, // we are destorying entities so it needs to be mutable 219 | players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities 220 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies 221 | ) { 222 | let mut entities_to_destroy: Vec = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration. 223 | 224 | world 225 | .with_query(players) 226 | .iter() 227 | .for_each(|(player_entity, player_position, _)| { 228 | world 229 | .with_query(enemies) 230 | .iter() 231 | .for_each(|(enemy_entity, enemy_position, _)| { 232 | if (player_position.0 - enemy_position.0).abs() < 3.0 233 | && (player_position.1 - enemy_position.1).abs() < 3.0 234 | { 235 | entities_to_destroy.push(*player_entity); 236 | entities_to_destroy.push(*enemy_entity); 237 | } 238 | }); 239 | }); 240 | 241 | for entity in entities_to_destroy { 242 | world.destroy(entity); 243 | } 244 | } 245 | ``` 246 | 247 | #### Get & At entities 248 | 249 | Get is identical to query but takes an Entity. 250 | At is identical to query but takes an index. 251 | 252 | Let's say you wanted an entity that follows a player. This is how you could implement that: 253 | 254 | Define a component for the companion 255 | ```rust 256 | #[component] 257 | struct CompanionComponent { 258 | target_entity: Option, 259 | } 260 | ``` 261 | 262 | Define the Companion Entity. It has a position and a companion component: 263 | ```rust 264 | #[entity] 265 | struct Companion { 266 | position: Position, 267 | companion_component: CompanionComponent, 268 | } 269 | ``` 270 | 271 | Now we need to write the companion system. 272 | For every companion we need to check if it has a target. 273 | If it has a target we need to check if target exists (it could have been deleted). 274 | If the target exists we get the *value of* target's position and set the companion's position with that value. 275 | 276 | We need to query for companions and their position as mutable. And we need to query for every entity that has a position. This means a companion could technically follow it self. 277 | 278 | ```rust 279 | #[system] 280 | fn companion_follow( 281 | world: &mut World, 282 | companions: Query<(&mut Position, &CompanionComponent)>, 283 | positions: Query<&Position>, 284 | ) { 285 | ``` 286 | 287 | Implementation: 288 | We can't simply iterate through the companions, get the target position and update the position because we can only have one borrow if the borrow is mutable (unless we use unsafe code). 289 | 290 | We can do what we did with destroying entities, but it will be slow. 291 | 292 | The solution is to iterate using index, only borrowing what we need for a short time: 293 | 294 | 295 | ```rust 296 | #[system] 297 | fn companion_follow( 298 | world: &mut World, 299 | companions: Query<(&mut Position, &CompanionComponent)>, 300 | positions: Query<&Position>, 301 | ) { 302 | for companion_idx in 0..world.with_query_mut(companions).len() { 303 | // iterate the count of companions 304 | if let Some(target_position) = world 305 | .with_query_mut(companions) 306 | .at_mut(companion_idx) // get the companion at index companion_idx 307 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 308 | .and_then(|companion_target_entity| { 309 | // then get the VALUE of target position (meaning we don't use a reference to the position) 310 | world 311 | .with_query(positions) 312 | .get(companion_target_entity) // get the position for the companion_target_entity 313 | .map(|p| (p.0, p.1)) // map to get the VALUE 314 | }) 315 | { 316 | if let Some((companion_position, _)) = 317 | world.with_query_mut(companions).at_mut(companion_idx) 318 | // Then simply get the companion position 319 | { 320 | // and update it to the target's position 321 | companion_position.0 = target_position.0; 322 | companion_position.1 = target_position.1; 323 | } 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | # TODO: 330 | - [ ] Re use IDs of deleted entities 331 | 332 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::{parse_macro_input, ItemStruct}; 5 | 6 | #[proc_macro_attribute] 7 | pub fn entity(_: TokenStream, input: TokenStream) -> TokenStream { 8 | let input_struct = parse_macro_input!(input as ItemStruct); 9 | 10 | quote! { 11 | #[derive(Default, Debug)] 12 | #input_struct 13 | } 14 | .into() 15 | } 16 | 17 | #[proc_macro_attribute] 18 | pub fn component(_: TokenStream, input: TokenStream) -> TokenStream { 19 | let input_struct = parse_macro_input!(input as ItemStruct); 20 | 21 | quote! { 22 | #[derive(Default, Debug)] 23 | #input_struct 24 | } 25 | .into() 26 | } 27 | 28 | #[proc_macro_attribute] 29 | #[allow(non_snake_case)] 30 | pub fn system(_: TokenStream, input: TokenStream) -> TokenStream { 31 | input 32 | } 33 | -------------------------------------------------------------------------------- /zero_ecs_testbed/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_testbed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_testbed" 3 | version = "0.2.22" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | zero_ecs = { path = "../zero_ecs", version = "0.2.22" } 11 | 12 | [build-dependencies] 13 | zero_ecs_build = { path = "../zero_ecs_build", version = "0.2.22" } 14 | -------------------------------------------------------------------------------- /zero_ecs_testbed/build.rs: -------------------------------------------------------------------------------- 1 | use zero_ecs_build::*; 2 | fn main() { 3 | generate_ecs("src/**/*.rs"); 4 | } 5 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/main.rs: -------------------------------------------------------------------------------- 1 | // include main_ecs.rs 2 | include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs")); 3 | 4 | use zero_ecs::{component, entity, system}; 5 | 6 | #[component] 7 | pub struct Position(f32, f32); 8 | 9 | #[component] 10 | struct Velocity(f32, f32); 11 | 12 | #[component] 13 | struct Name(String); 14 | 15 | #[component] 16 | struct EnemyTag; 17 | 18 | #[component] 19 | struct FlowerTag; 20 | 21 | #[entity] 22 | struct Enemy { 23 | position: Position, 24 | velocity: Velocity, 25 | name: Name, 26 | enemy_tag: EnemyTag, 27 | } 28 | 29 | #[entity] 30 | struct NameEntity { 31 | name: Name, 32 | } 33 | 34 | #[entity] 35 | struct Flower { 36 | position: Position, 37 | flower_tag: FlowerTag, 38 | } 39 | 40 | #[entity] 41 | pub struct EntityWithPosition { 42 | pub position: Position, 43 | } 44 | 45 | #[system] 46 | fn print_positions(world: &mut World, query: Query<&Position>) { 47 | world.with_query(query).iter().for_each(|pos| { 48 | println!("Position: {:?}", pos); 49 | }); 50 | } 51 | #[system(group=last)] 52 | fn print_positions_copy(world: &mut World, query: Query<&Position>) { 53 | world.with_query(query).iter().for_each(|pos| { 54 | println!("Position: {:?}", pos); 55 | }); 56 | } 57 | 58 | #[system] 59 | fn apply_velocity(world: &mut World, query: Query<(&mut Position, &Velocity)>) { 60 | world 61 | .with_query_mut(query) 62 | .par_iter_mut() 63 | .for_each(|(pos, vel)| { 64 | pos.0 += vel.0; 65 | pos.1 += vel.1; 66 | }); 67 | } 68 | 69 | #[system] 70 | fn print_names(world: &mut World, query: Query<&Name>) { 71 | world.with_query(query).iter().for_each(|name| { 72 | println!("Name: {:?}", name.0); 73 | }); 74 | } 75 | 76 | #[derive(Debug, Default)] 77 | struct Resources { 78 | test: i32, 79 | } 80 | 81 | #[component] 82 | struct FollowerComponent { 83 | target_entity: Option, 84 | } 85 | 86 | #[entity] 87 | struct FollowerEntity { 88 | follower: FollowerComponent, 89 | position: Position, 90 | } 91 | 92 | #[component] 93 | struct MyUnused { 94 | _unused: i32, 95 | } 96 | 97 | #[system] 98 | fn unused_system(world: &mut World, le_query: Query<&mut MyUnused>) { 99 | world 100 | .with_query_mut(le_query) 101 | .iter_mut() 102 | .for_each(|unused| { 103 | unused._unused += 1; 104 | }); 105 | } 106 | 107 | #[system] 108 | fn follower_update_position( 109 | world: &mut World, 110 | followers: Query<(&mut Position, &FollowerComponent)>, 111 | positions: Query<&Position>, // This is all entities with a position. Including the followers, meaning followers can follow followers, and even themselfs. 112 | ) { 113 | // Iterate all followers using idx so we don't borrow world 114 | for follower_idx in 0..world.with_query_mut(followers).len() { 115 | // Get the target entity of the follower. Entity is just a lightweight ID that is copied. 116 | if let Some(target_entity) = world 117 | .with_query_mut(followers) 118 | .at_mut(follower_idx) 119 | .and_then(|(_, follower)| follower.target_entity) 120 | { 121 | // If the target entity exists, get its position 122 | if let Some(target_position) = world 123 | .with_query(positions) 124 | .get(target_entity) 125 | .map(|p| (p.0, p.1)) 126 | { 127 | // Get the position component of the follower and update its position with the target_position. 128 | if let Some((follower_position, _)) = 129 | world.with_query_mut(followers).at_mut(follower_idx) 130 | { 131 | follower_position.0 = target_position.0; 132 | follower_position.1 = target_position.1; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | #[system(group = with_resources)] 140 | fn print_names_with_resources(world: &mut World, query: Query<&Name>, resources: &Resources) { 141 | world.with_query(query).iter().for_each(|name| { 142 | println!("Name: {:?}, test: {}", name, resources.test); 143 | }); 144 | } 145 | 146 | #[system] 147 | fn count_types(world: &mut World, enemy_query: Query<&EnemyTag>, flower_query: Query<&FlowerTag>) { 148 | let mut test = 0; 149 | for _ in world.with_query(enemy_query).iter() { 150 | test += 1; 151 | println!("enemy: {}", test); 152 | for _ in world.with_query(flower_query).iter() { 153 | test += 1; 154 | println!("flower: {}", test); 155 | } 156 | } 157 | } 158 | 159 | #[system] 160 | fn mutate_position_twice(world: &mut World, query: Query<&mut Position>) { 161 | // multiply all position with 0.99 162 | 163 | world.with_query_mut(query).iter_mut().for_each(|pos| { 164 | pos.0 *= 0.99; 165 | pos.1 *= 0.99; 166 | }); 167 | 168 | world.with_query_mut(query).iter_mut().for_each(|pos| { 169 | pos.0 *= 0.99; 170 | pos.1 *= 0.99; 171 | }); 172 | } 173 | 174 | fn main() { 175 | println!("Hello, world!"); 176 | 177 | let mut world = World::default(); 178 | let e = world.create(Enemy { 179 | position: Position(0.0, 0.0), 180 | velocity: Velocity(1.0, 1.0), 181 | ..Default::default() 182 | }); 183 | let f = world.create(Flower { 184 | position: Position(0.0, 0.0), 185 | ..Default::default() 186 | }); 187 | let f1 = world.create(Flower { 188 | position: Position(0.0, 0.0), 189 | ..Default::default() 190 | }); 191 | 192 | systems_main(&mut world); 193 | 194 | systems_last(&mut world); 195 | 196 | world.destroy(e); 197 | world.destroy(f); 198 | world.destroy(f1); 199 | } 200 | 201 | // create some unit tests 202 | #[cfg(test)] 203 | mod tests { 204 | use super::*; 205 | 206 | #[test] 207 | fn test_followers() { 208 | // create 10 entities, and 10 followers and make sure they get their target's position 209 | let mut world = World::default(); 210 | 211 | let targets: Vec<_> = (0..10) 212 | .map(|i| { 213 | world.create(EntityWithPosition { 214 | position: Position(i as f32, i as f32), 215 | }) 216 | }) 217 | .collect(); 218 | 219 | let followers = (0..10) 220 | .map(|i| { 221 | world.create(FollowerEntity { 222 | follower: FollowerComponent { 223 | target_entity: Some(targets[i]), 224 | }, 225 | position: Position(0.0, 0.0), 226 | }) 227 | }) 228 | .collect::>(); 229 | 230 | follower_update_position(&mut world, Query::new(), Query::new()); 231 | 232 | for (i, follower) in followers.iter().enumerate() { 233 | let target_position: &Position = world.get_from(targets[i]).unwrap(); 234 | let follower_position: &Position = world.get_from(*follower).unwrap(); 235 | assert_eq!(target_position.0, follower_position.0); 236 | assert_eq!(target_position.1, follower_position.1); 237 | } 238 | } 239 | 240 | #[test] 241 | fn test_parallel_iteration() { 242 | // create an Enemy with position, run apply velocity, check that position is updated 243 | let mut world = World::default(); 244 | let e = world.create(Enemy { 245 | position: Position(0.0, 0.0), 246 | velocity: Velocity(1.0, 1.0), 247 | ..Default::default() 248 | }); 249 | 250 | apply_velocity(&mut world, Query::new()); 251 | 252 | let pos: Option<&Position> = world.get_from(e); 253 | assert!(pos.is_some()); 254 | let pos = pos.unwrap(); 255 | assert_eq!(1.0, pos.0); 256 | assert_eq!(1.0, pos.1); 257 | } 258 | 259 | #[test] 260 | fn test_create_entities() { 261 | let mut world = World::default(); 262 | let e = world.create(Enemy { 263 | position: Position(0.0, 0.0), 264 | velocity: Velocity(1.0, 1.0), 265 | name: Name("test".into()), 266 | ..Default::default() 267 | }); 268 | let f = world.create(Flower { 269 | position: Position(1.0, 0.0), 270 | ..Default::default() 271 | }); 272 | let f1 = world.create(Flower { 273 | position: Position(0.0, 0.0), 274 | ..Default::default() 275 | }); 276 | 277 | assert!(matches!(e.entity_type, EntityType::Enemy)); 278 | assert!(matches!(f.entity_type, EntityType::Flower)); 279 | assert!(matches!(f1.entity_type, EntityType::Flower)); 280 | 281 | assert_eq!(0, e.id); 282 | assert_eq!(0, f.id); 283 | assert_eq!(1, f1.id); 284 | 285 | let name: Option<&Name> = world.get_from(e); 286 | assert!(name.is_some()); 287 | let name = &name.unwrap().0; 288 | assert_eq!("test", name); 289 | let name: Option<&Name> = world.get_from(f); 290 | assert!(name.is_none()); 291 | } 292 | 293 | #[test] 294 | fn test_create_and_destroy_entity() { 295 | let mut world = World::default(); 296 | // test creating 5 enemies, 100 times, 297 | for _ in 0..100 { 298 | let entity_ids: Vec<_> = (0..5) 299 | .map(|index| { 300 | let enemy_name = format!("enemy_{}", index); 301 | world.create(NameEntity { 302 | name: Name(enemy_name), 303 | ..Default::default() 304 | }) 305 | }) 306 | .collect(); 307 | 308 | { 309 | let entity_id = entity_ids[1]; 310 | world.destroy(entity_id); 311 | let try_get: Option<&Name> = world.get_from(entity_id); 312 | assert!(try_get.is_none()); 313 | } 314 | { 315 | let entity_id = entity_ids[3]; 316 | world.destroy(entity_id); 317 | let try_get: Option<&Name> = world.get_from(entity_id); 318 | assert!(try_get.is_none()); 319 | } 320 | 321 | // assert that get on destroyed enemies return None 322 | // assert that not destroyed enemies exist, and that they are named correctly 323 | for (index, entity_id) in entity_ids.iter().enumerate() { 324 | let name: Option<&Name> = world.get_from(*entity_id); 325 | 326 | if index == 1 || index == 3 { 327 | assert!(name.is_none()); 328 | } else { 329 | assert!(name.is_some()); 330 | let name = &name.unwrap().0; 331 | assert_eq!(&format!("enemy_{}", index), name); 332 | } 333 | } 334 | } 335 | } 336 | } 337 | --------------------------------------------------------------------------------