├── .gitignore ├── zero_ecs ├── .gitignore ├── Cargo.toml ├── src │ └── lib.rs └── README.md ├── zero_ecs_build ├── .gitignore ├── src │ └── build.rs ├── Cargo.toml └── README.md ├── zero_ecs_macros ├── .gitignore ├── src │ ├── helpers.rs │ ├── macros.rs │ ├── make_query_impl.rs │ ├── world_impl.rs │ ├── system_for_each_impl.rs │ ├── ecs_world_impl.rs │ ├── entity_impl.rs │ ├── default_queries.rs │ ├── system_impl.rs │ └── query_impl.rs ├── Cargo.toml └── README.md ├── zero_ecs_testbed ├── .gitignore ├── src │ ├── integration_tests │ │ ├── complex_tests │ │ │ ├── components.rs │ │ │ ├── world.rs │ │ │ ├── system.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── test_simple.rs │ │ └── test_systems_comprehensive.rs │ └── testbed.rs └── Cargo.toml ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ ├── nrs.yml │ └── rust.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | -------------------------------------------------------------------------------- /zero_ecs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_build/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_testbed/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zero_ecs_build/src/build.rs: -------------------------------------------------------------------------------- 1 | pub fn _do_nothing() {} 2 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/complex_tests/components.rs: -------------------------------------------------------------------------------- 1 | pub struct Value(pub usize); 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["zero_ecs", "zero_ecs_macros", "zero_ecs_build", "zero_ecs_testbed"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test_simple; 3 | 4 | #[cfg(test)] 5 | mod test_systems_comprehensive; 6 | 7 | #[cfg(test)] 8 | mod complex_tests; 9 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/complex_tests/world.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[entity] 4 | pub struct ValueEntity { 5 | pub value: Value, 6 | } 7 | 8 | ecs_world!(ValueEntity); 9 | -------------------------------------------------------------------------------- /zero_ecs_testbed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_testbed" 3 | version = "0.3.4" 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.3.4" } 11 | 12 | [[bin]] 13 | name = "zero_ecs_testbed" 14 | path = "src/testbed.rs" 15 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/complex_tests/system.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[system_for_each(World)] 4 | fn inc(value: &mut Value) { 5 | value.0 += 1; 6 | } 7 | 8 | #[system(World)] 9 | fn assert_value(world: &World, values: Query<&Value>, expected: usize) { 10 | let values = world.with_query(values); 11 | let value: &Value = values.at(0).unwrap(); 12 | 13 | assert_eq!(expected, value.0) 14 | } 15 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use convert_case::{Case, Casing}; 2 | use quote::format_ident; 3 | use syn::Ident; 4 | 5 | pub fn format_collection_name(ident: &impl ToString) -> Ident { 6 | format_ident!("__{}Collection", ident.to_string()) 7 | } 8 | 9 | pub fn format_field_name(ident: &impl ToString) -> Ident { 10 | let s = ident.to_string(); 11 | let s = s.to_case(Case::Snake); 12 | format_ident!("__{}", s) 13 | } 14 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/complex_tests/mod.rs: -------------------------------------------------------------------------------- 1 | // to test separating systems into other files 2 | 3 | mod components; 4 | mod system; 5 | mod world; 6 | use zero_ecs::*; 7 | 8 | use components::*; 9 | use system::*; 10 | use world::*; 11 | 12 | #[test] 13 | fn can_make_mutable_query() { 14 | let mut world = World::default(); 15 | let _ = world.create(ValueEntity { value: Value(0) }); 16 | let _ = world.create(ValueEntity { value: Value(0) }); 17 | let _ = world.create(ValueEntity { value: Value(0) }); 18 | let _ = world.create(ValueEntity { value: Value(0) }); 19 | 20 | world.inc(); 21 | world.inc(); 22 | world.inc(); 23 | 24 | world.assert_value(3); 25 | } 26 | -------------------------------------------------------------------------------- /zero_ecs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs" 3 | version = "0.3.4" 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.3.4" } 16 | itertools = "0.12.1" 17 | rayon = "1.9.0" 18 | macro_magic = { version = "0.6.0", features = ["proc_support"] } 19 | derive_more = { version = "2.0.1", features = ["full"] } 20 | 21 | extend = "1.2.0" 22 | -------------------------------------------------------------------------------- /zero_ecs_build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_build" 3 | version = "0.3.4" 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.3.4" } 18 | 19 | [lib] 20 | name = "zero_ecs_build" 21 | path = "src/build.rs" 22 | -------------------------------------------------------------------------------- /zero_ecs_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zero_ecs_macros" 3 | version = "0.3.4" 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 | name = "zero_ecs_macros" 17 | path = "src/macros.rs" 18 | 19 | [dependencies] 20 | proc-macro2 = "1.0.93" 21 | quote = "1.0.38" 22 | syn = { version = "2.0.98", features = ["full"] } 23 | macro_magic = { version = "0.6.0", features = ["proc_support"] } 24 | convert_case = "0.8.0" 25 | intehan_util_dump = "0.1.2" 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /zero_ecs_macros/src/macros.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use macro_magic::import_tokens_attr; 4 | use proc_macro::TokenStream; 5 | 6 | mod default_queries; 7 | mod ecs_world_impl; 8 | mod entity_impl; 9 | mod helpers; 10 | mod make_query_impl; 11 | mod query_impl; 12 | mod system_for_each_impl; 13 | mod system_impl; 14 | mod world_impl; 15 | 16 | #[proc_macro_attribute] 17 | pub fn entity(attr: TokenStream, item: TokenStream) -> TokenStream { 18 | entity_impl::entity(attr, item) 19 | } 20 | 21 | #[proc_macro] 22 | pub fn ecs_world(input: TokenStream) -> TokenStream { 23 | ecs_world_impl::ecs_world(input) 24 | } 25 | 26 | #[proc_macro_derive(World, attributes(tag_collection_field))] 27 | pub fn world_tag_collection_field(_item: TokenStream) -> TokenStream { 28 | TokenStream::new() 29 | } 30 | 31 | #[proc_macro_attribute] 32 | pub fn expand_world(attr: TokenStream, item: TokenStream) -> TokenStream { 33 | world_impl::expand_world(attr, item) 34 | } 35 | 36 | #[import_tokens_attr(zero_ecs::macro_magic)] 37 | #[proc_macro_attribute] 38 | pub fn tag_world(attr: TokenStream, item: TokenStream) -> TokenStream { 39 | world_impl::tag_world(attr, item) 40 | } 41 | 42 | #[import_tokens_attr(zero_ecs::macro_magic)] 43 | #[proc_macro_attribute] 44 | pub fn query(attr: TokenStream, item: TokenStream) -> TokenStream { 45 | query_impl::query(attr, item) 46 | } 47 | 48 | #[import_tokens_attr(zero_ecs::macro_magic)] 49 | #[proc_macro_attribute] 50 | pub fn system_for_each(attr: TokenStream, item: TokenStream) -> TokenStream { 51 | system_for_each_impl::system_for_each(attr, item) 52 | } 53 | 54 | #[import_tokens_attr(zero_ecs::macro_magic)] 55 | #[proc_macro_attribute] 56 | pub fn system(attr: TokenStream, item: TokenStream) -> TokenStream { 57 | system_impl::system(attr, item) 58 | } 59 | 60 | #[proc_macro] 61 | pub fn make_query(input: TokenStream) -> TokenStream { 62 | make_query_impl::make_query(input) 63 | } 64 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/make_query_impl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | parse_macro_input, Ident, Token, 6 | }; 7 | 8 | /// Represents a component in the query, which can be mutable or immutable 9 | struct ComponentSpec { 10 | is_mut: bool, 11 | ident: Ident, 12 | } 13 | 14 | impl Parse for ComponentSpec { 15 | fn parse(input: ParseStream) -> syn::Result { 16 | let is_mut = input.peek(Token![mut]); 17 | if is_mut { 18 | input.parse::()?; 19 | } 20 | let ident = input.parse::()?; 21 | Ok(ComponentSpec { is_mut, ident }) 22 | } 23 | } 24 | 25 | /// Input for the make_query macro 26 | /// Format: QueryName, [mut] Component1, [mut] Component2, ... 27 | struct MakeQueryInput { 28 | query_name: Ident, 29 | components: Vec, 30 | } 31 | 32 | impl Parse for MakeQueryInput { 33 | fn parse(input: ParseStream) -> syn::Result { 34 | let query_name = input.parse::()?; 35 | input.parse::()?; 36 | 37 | let mut components = Vec::new(); 38 | loop { 39 | components.push(input.parse::()?); 40 | if input.is_empty() { 41 | break; 42 | } 43 | input.parse::()?; 44 | if input.is_empty() { 45 | break; 46 | } 47 | } 48 | 49 | Ok(MakeQueryInput { 50 | query_name, 51 | components, 52 | }) 53 | } 54 | } 55 | 56 | pub fn make_query(input: TokenStream) -> TokenStream { 57 | let MakeQueryInput { 58 | query_name, 59 | components, 60 | } = parse_macro_input!(input as MakeQueryInput); 61 | 62 | // Generate the tuple fields for the struct 63 | let fields = components.iter().map(|comp| { 64 | let ident = &comp.ident; 65 | if comp.is_mut { 66 | quote! { &'a mut #ident } 67 | } else { 68 | quote! { &'a #ident } 69 | } 70 | }); 71 | 72 | let expanded = quote! { 73 | #[query(World)] 74 | struct #query_name<'a>(#(#fields),*); 75 | }; 76 | 77 | expanded.into() 78 | } 79 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /zero_ecs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use itertools::chain; 2 | pub use itertools::izip; 3 | pub use macro_magic; 4 | 5 | pub use macro_magic::export_tokens; 6 | pub use macro_magic::{import_tokens, import_tokens_attr, import_tokens_proc}; 7 | 8 | pub use extend::ext; 9 | pub use rayon; 10 | pub use rayon::prelude::*; 11 | pub use std::marker::PhantomData; 12 | pub use zero_ecs_macros::ecs_world; 13 | pub use zero_ecs_macros::entity; 14 | pub use zero_ecs_macros::expand_world; 15 | pub use zero_ecs_macros::make_query; 16 | pub use zero_ecs_macros::query; 17 | pub use zero_ecs_macros::system; 18 | pub use zero_ecs_macros::system_for_each; 19 | pub use zero_ecs_macros::tag_world; 20 | 21 | pub use derive_more; 22 | pub use derive_more::From; 23 | pub use derive_more::Into; 24 | 25 | #[macro_export] 26 | macro_rules! izip_par { 27 | // @closure creates a tuple-flattening closure for .map() call. usage: 28 | // @closure partial_pattern => partial_tuple , rest , of , iterators 29 | // eg. izip!( @closure ((a, b), c) => (a, b, c) , dd , ee ) 30 | ( @closure $p:pat => $tup:expr ) => { 31 | |$p| $tup 32 | }; 33 | 34 | // The "b" identifier is a different identifier on each recursion level thanks to hygiene. 35 | ( @closure $p:pat => ( $($tup:tt)* ) , $_iter:expr $( , $tail:expr )* ) => { 36 | $crate::izip_par!(@closure ($p, b) => ( $($tup)*, b ) $( , $tail )*) 37 | }; 38 | 39 | // unary 40 | ($first:expr $(,)*) => { 41 | $crate::IntoParallelIterator::into_par_iter($first) 42 | }; 43 | 44 | // binary 45 | ($first:expr, $second:expr $(,)*) => { 46 | $crate::izip_par!($first) 47 | .zip($second) 48 | }; 49 | 50 | // n-ary where n > 2 51 | ( $first:expr $( , $rest:expr )* $(,)* ) => { 52 | $crate::izip_par!($first) 53 | $( 54 | .zip($rest) 55 | )* 56 | .map( 57 | $crate::izip!(@closure a => (a) $( , $rest )*) 58 | ) 59 | }; 60 | } 61 | #[macro_export] 62 | macro_rules! chain_par { 63 | () => { 64 | rayon::iter::empty() 65 | }; 66 | ($first:expr $(, $rest:expr )* $(,)?) => { 67 | { 68 | let iter = $crate::IntoParallelIterator::into_par_iter($first); 69 | $( 70 | let iter = 71 | ParallelIterator::chain( 72 | iter, 73 | $crate::IntoParallelIterator::into_par_iter($rest)); 74 | )* 75 | iter 76 | } 77 | }; 78 | } 79 | 80 | // found code for sum here: https://gist.github.com/jnordwick/1473d5533ca158d47ba4 81 | #[macro_export] 82 | macro_rules! sum { 83 | ($h:expr) => ($h); // so that this would be called, I ... 84 | ($h:expr, $($t:expr),*) => 85 | (sum!($h) + sum!($($t),*)); // ...call sum! on both sides of the operation 86 | } 87 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/world_impl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{spanned::Spanned, Error, Fields, ItemStruct}; 4 | 5 | use crate::ecs_world_impl::StructList; 6 | 7 | pub fn expand_world(attr: TokenStream, item: TokenStream) -> TokenStream { 8 | let types = syn::parse_macro_input!(attr as StructList); 9 | 10 | let mut types: Vec<_> = types.0.iter().collect(); 11 | let Some(next_type) = types.pop() else { 12 | return item; 13 | }; 14 | 15 | let local_struct = syn::parse_macro_input!(item as ItemStruct); 16 | let Fields::Named(local_fields) = local_struct.fields else { 17 | return Error::new( 18 | local_struct.fields.span(), 19 | "unnamed fields are not supported", 20 | ) 21 | .to_compile_error() 22 | .into(); 23 | }; 24 | let local_fields = local_fields.named.iter(); 25 | let attrs = local_struct.attrs; 26 | let generics = local_struct.generics; 27 | let ident = local_struct.ident; 28 | let vis = local_struct.vis; 29 | 30 | quote! { 31 | #[tag_world(#next_type)] 32 | #[expand_world(#(#types),*)] 33 | #(#attrs) 34 | * 35 | #vis struct #ident<#generics> { 36 | #(#local_fields), 37 | * 38 | } 39 | } 40 | .into() 41 | } 42 | 43 | pub fn tag_world(attr: TokenStream, item: TokenStream) -> TokenStream { 44 | let foreign_struct = syn::parse_macro_input!(attr as ItemStruct); 45 | 46 | let local_struct = syn::parse_macro_input!(item as ItemStruct); 47 | let Fields::Named(local_fields) = local_struct.fields else { 48 | return Error::new( 49 | local_struct.fields.span(), 50 | "unnamed fields are not supported", 51 | ) 52 | .to_compile_error() 53 | .into(); 54 | }; 55 | let local_fields = local_fields.named.iter(); 56 | let attrs = local_struct.attrs; 57 | let generics = local_struct.generics; 58 | let ident = local_struct.ident; 59 | let vis = local_struct.vis; 60 | 61 | let Fields::Named(foreign_fields) = foreign_struct.fields else { 62 | return Error::new( 63 | foreign_struct.fields.span(), 64 | "unnamed fields are not supported", 65 | ) 66 | .to_compile_error() 67 | .into(); 68 | }; 69 | 70 | let foreign_struct_ident = foreign_struct.ident; 71 | 72 | let foreign_fields = foreign_fields.named.iter().map(|field| { 73 | let field_name = &field.ident; 74 | let field_type = &field.ty; 75 | let field_type_str = quote::ToTokens::to_token_stream(field_type).to_string(); 76 | let field_type_str = field_type_str.replace(' ', ""); // Remove spaces if necessary 77 | let field_name = format_ident!( 78 | "__twcf__{}__{}__{}", 79 | foreign_struct_ident, 80 | field_name.clone().expect("foreign fields field clone"), 81 | field_type_str 82 | ); 83 | quote! { 84 | #field_name: std::marker::PhantomData<()> 85 | } 86 | }); 87 | 88 | quote! { 89 | #(#attrs) 90 | * 91 | #vis struct #ident<#generics> { 92 | #(#foreign_fields,)* 93 | #(#local_fields), 94 | * 95 | } 96 | } 97 | .into() 98 | } 99 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/test_simple.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use zero_ecs::*; 4 | 5 | pub struct Value(usize); 6 | 7 | #[entity] 8 | pub struct ValueEntity { 9 | value: Value, 10 | } 11 | 12 | ecs_world!(ValueEntity); 13 | 14 | #[system_for_each(World)] 15 | fn inc(value: &mut Value) { 16 | value.0 += 1; 17 | } 18 | 19 | #[system(World)] 20 | fn assert_value(world: &World, values: Query<&Value>, expected: usize) { 21 | let values = world.with_query(values); 22 | let value: &Value = values.at(0).unwrap(); 23 | 24 | assert_eq!(expected, value.0) 25 | } 26 | 27 | #[system(World)] 28 | fn assert_value_for(world: &World, values: Query<&Value>, expected: usize, entity: Entity) { 29 | let values = world.with_query(values); 30 | let value: &Value = values.get(entity).unwrap(); 31 | 32 | assert_eq!(expected, value.0) 33 | } 34 | 35 | #[test] 36 | fn can_mutable_for_each() { 37 | let mut world = World::default(); 38 | let _ = world.create(ValueEntity { value: Value(0) }); 39 | 40 | world.inc(); 41 | world.inc(); 42 | world.inc(); 43 | 44 | world.assert_value(3); 45 | } 46 | 47 | make_query!(ManualValueMutable, mut Value); 48 | 49 | #[test] 50 | fn can_make_mutable_query() { 51 | let mut world = World::default(); 52 | let a = world.create(ValueEntity { value: Value(0) }); 53 | let b = world.create(ValueEntity { value: Value(0) }); 54 | let c = world.create(ValueEntity { value: Value(0) }); 55 | let d = world.create(ValueEntity { value: Value(0) }); 56 | 57 | { 58 | let mut q = world.with_query_mut(Query::::new()); 59 | let value: &mut Value = q.get_mut(c).unwrap(); 60 | 61 | value.0 += 5; 62 | } 63 | { 64 | let mut q = world.with_query_mut(Query::::new()); 65 | let value: &mut Value = q.get_mut(a).unwrap(); 66 | 67 | value.0 += 5; 68 | } 69 | { 70 | let mut q = world.with_query_mut(Query::::new()); 71 | let value: &mut Value = q.get_mut(a).unwrap(); 72 | 73 | value.0 += 5; 74 | } 75 | 76 | world.assert_value_for(5, c); 77 | world.assert_value_for(10, a); 78 | world.assert_value_for(0, b); 79 | world.assert_value_for(0, d); 80 | } 81 | 82 | struct ToDestroy(HashSet); 83 | 84 | #[system_for_each(World)] 85 | pub fn destroy_even_values(entity: &Entity, value: &Value, to_destroy: &mut ToDestroy) { 86 | if value.0 % 2 == 0 { 87 | to_destroy.0.insert(*entity); 88 | } 89 | } 90 | 91 | struct Sum(usize); 92 | 93 | #[system_for_each(World)] 94 | pub fn sum_values(value: &Value, sum: &mut Sum) { 95 | sum.0 += value.0; 96 | } 97 | 98 | #[test] 99 | fn can_destroy_specific() { 100 | let mut world = World::default(); 101 | let _ = world.create(ValueEntity { value: Value(0) }); 102 | let _ = world.create(ValueEntity { value: Value(1) }); 103 | let _ = world.create(ValueEntity { value: Value(2) }); 104 | let _ = world.create(ValueEntity { value: Value(3) }); 105 | let _ = world.create(ValueEntity { value: Value(4) }); 106 | let mut to_destroy = ToDestroy(HashSet::new()); 107 | world.destroy_even_values(&mut to_destroy); 108 | 109 | to_destroy.0.iter().for_each(|entity| { 110 | world.destroy(*entity); 111 | }); 112 | 113 | // 1+3 is 4 114 | let mut sum = Sum(0); 115 | world.sum_values(&mut sum); 116 | 117 | assert_eq!(4, sum.0); 118 | } 119 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/system_for_each_impl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use std::collections::HashSet; 4 | use syn::{FnArg, ItemFn, ItemStruct, Pat, PatIdent, PatType, Type}; 5 | 6 | use crate::query_impl::get_collection_component_fields; 7 | 8 | pub fn system_for_each(attr: TokenStream, item: TokenStream) -> TokenStream { 9 | let foreign_struct = syn::parse_macro_input!(attr as ItemStruct); 10 | let collection_component_fields = get_collection_component_fields(foreign_struct); 11 | let mut component_types: HashSet<_> = collection_component_fields 12 | .iter() 13 | .map(|ccf| &ccf.field_type) 14 | .collect(); 15 | let entity_name = "Entity".to_string(); 16 | component_types.insert(&entity_name); 17 | let input_fn = syn::parse_macro_input!(item as ItemFn); 18 | 19 | let fn_vis = &input_fn.vis; 20 | let fn_sig = &input_fn.sig; 21 | let fn_name = &fn_sig.ident; 22 | let fn_block = &input_fn.block; 23 | 24 | let mut query_fields = Vec::new(); 25 | let mut call_args = Vec::new(); 26 | let mut any_mutable_arguments = false; 27 | let mut resource_args = Vec::new(); 28 | let mut resource_params = Vec::new(); 29 | let mut all_args = Vec::new(); 30 | 31 | for arg in &fn_sig.inputs { 32 | match arg { 33 | FnArg::Receiver(_) => { 34 | panic!("#[system_for_each] functions cannot take self") 35 | } 36 | FnArg::Typed(PatType { pat, ty, .. }) => { 37 | // get the name (pat) 38 | let arg_ident = if let Pat::Ident(PatIdent { ident, .. }) = &**pat { 39 | ident.clone() 40 | } else { 41 | panic!("Unsupported argument pattern in #[system_for_each]"); 42 | }; 43 | 44 | // &** i don't understand but do what the compiler tells me. 45 | if let Type::Reference(ty) = &**ty { 46 | let is_mutable = ty.mutability.is_some(); 47 | 48 | let Type::Path(type_path) = &*ty.elem else { 49 | panic!("failed to get elem path #[system_for_each]"); 50 | }; 51 | 52 | let Some(type_path) = type_path.path.get_ident() else { 53 | panic!("failed to get type path #[system_for_each]") 54 | }; 55 | 56 | let type_path_str = type_path.to_string(); 57 | let is_component = component_types.iter().any(|ty| **ty == type_path_str); 58 | 59 | if is_component { 60 | call_args.push(arg_ident.clone()); 61 | if is_mutable { 62 | any_mutable_arguments = true; 63 | query_fields.push(quote! {&'a mut #type_path}); 64 | } else { 65 | query_fields.push(quote! {&'a #type_path}); 66 | } 67 | } else { 68 | resource_args.push(arg_ident.clone()); 69 | resource_params.push(arg); 70 | } 71 | all_args.push(arg_ident.clone()); 72 | } else { 73 | panic!("Only references in #[system_for_each]"); 74 | } 75 | } 76 | } 77 | } 78 | 79 | let fn_call = quote! { 80 | #fn_name(#(#all_args),*); 81 | }; 82 | 83 | let ext_name = format_ident!("__ext_{}", fn_name); 84 | let mut_code = quote! { 85 | self.with_query_mut(Query::::new()) 86 | .iter_mut() 87 | .for_each(|QueryObject(#(#call_args),*)| { 88 | #fn_call 89 | }); 90 | }; 91 | 92 | let ref_code = quote! { 93 | self.with_query(Query::::new()) 94 | .iter() 95 | .for_each(|QueryObject(#(#call_args),*)| { 96 | #fn_call 97 | }); 98 | }; 99 | 100 | let code = if any_mutable_arguments { 101 | mut_code 102 | } else { 103 | ref_code 104 | }; 105 | 106 | let resource_args_params = if resource_args.is_empty() { 107 | quote! {} 108 | } else { 109 | quote! { #(#resource_params),* } 110 | }; 111 | 112 | // Build the output 113 | let expanded = quote! { 114 | #fn_vis #fn_sig { 115 | #fn_block 116 | } 117 | 118 | #[ext(name = #ext_name)] 119 | pub impl World { 120 | fn #fn_name(&mut self, #resource_args_params) { 121 | #[query(World)] 122 | struct QueryObject<'a>(#(#query_fields),*); 123 | 124 | #code 125 | } 126 | } 127 | }; 128 | 129 | expanded.into() 130 | } 131 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/ecs_world_impl.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | default_queries::get_default_queries, 3 | helpers::{format_collection_name, format_field_name}, 4 | }; 5 | use proc_macro::TokenStream; 6 | use quote::quote; 7 | use syn::{ 8 | parse::{Parse, ParseStream}, 9 | punctuated::Punctuated, 10 | Ident, Token, 11 | }; 12 | 13 | pub struct StructList(pub Punctuated); 14 | 15 | impl Parse for StructList { 16 | fn parse(input: ParseStream) -> syn::Result { 17 | // Parse zero or more identifiers separated by commas. 18 | let list = Punctuated::::parse_terminated(input)?; 19 | Ok(StructList(list)) 20 | } 21 | } 22 | 23 | pub fn ecs_world(input: TokenStream) -> TokenStream { 24 | // Parse the input tokens as a list of identifiers separated by commas. 25 | let types = syn::parse_macro_input!(input as StructList); 26 | 27 | let fields = types.0.iter().map(|ty| { 28 | let field_name = format_field_name(ty); 29 | // type name should be collection 30 | let type_name = format_collection_name(ty); 31 | quote! { 32 | pub #field_name: #type_name 33 | } 34 | }); 35 | 36 | let enum_names: Vec<_> = types 37 | .0 38 | .iter() 39 | .map(|ty| { 40 | quote! { 41 | #ty 42 | } 43 | }) 44 | .collect(); 45 | 46 | let create_implementations = types.0.iter().map(|ty| { 47 | let collection_field_name = format_field_name(ty); 48 | quote! { 49 | impl WorldCreate<#ty> for World { 50 | fn create(&mut self, e: #ty) -> Entity { 51 | self.#collection_field_name.create(e) 52 | } 53 | } 54 | } 55 | }); 56 | 57 | let destroy_match_calls = types.0.iter().map(|ty| { 58 | let collection_field_name = format_field_name(ty); 59 | quote! { 60 | EntityType::#ty => self.#collection_field_name.destroy(e), 61 | } 62 | }); 63 | 64 | let destroy_implementation = quote! { 65 | impl WorldDestroy for World { 66 | fn destroy(&mut self, e: Entity) { 67 | match e.entity_type { 68 | #(#destroy_match_calls)* 69 | } 70 | } 71 | } 72 | }; 73 | 74 | let default_queries = get_default_queries(); 75 | // Generate the struct World with the computed fields. 76 | let expanded = quote! { 77 | #[expand_world(#(#enum_names),*)] 78 | #[export_tokens] 79 | #[derive(Default)] 80 | #[allow(non_camel_case_types)] 81 | #[allow(non_snake_case)] 82 | pub struct World { 83 | #(#fields,)* 84 | } 85 | 86 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 87 | pub enum EntityType { 88 | #(#enum_names,)* 89 | } 90 | 91 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 92 | pub struct Entity { 93 | pub entity_type: EntityType, 94 | pub id: usize 95 | } 96 | 97 | #default_queries 98 | 99 | #destroy_implementation 100 | 101 | // creates 102 | #(#create_implementations)* 103 | 104 | #[allow(dead_code)] 105 | impl World { 106 | pub fn query_mut<'a, T: 'a + Send>(&'a mut self) -> impl Iterator + 'a 107 | where 108 | World: QueryMutFrom<'a, T>, 109 | { 110 | QueryMutFrom::::query_mut_from(self) 111 | } 112 | pub fn par_query_mut<'a, T: 'a + Send>(&'a mut self) -> impl ParallelIterator + 'a 113 | where 114 | World: QueryMutFrom<'a, T>, 115 | { 116 | QueryMutFrom::::par_query_mut_from(self) 117 | } 118 | 119 | pub fn get_mut<'a, T: 'a + Send>(&'a mut self, entity: Entity) -> Option 120 | where 121 | World: QueryMutFrom<'a, T>, 122 | { 123 | QueryMutFrom::::get_mut_from(self, entity) 124 | } 125 | } 126 | 127 | #[allow(dead_code)] 128 | impl World { 129 | pub fn query<'a, T: 'a + Send>(&'a self) -> impl Iterator + 'a 130 | where 131 | World: QueryFrom<'a, T>, 132 | { 133 | QueryFrom::::query_from(self) 134 | } 135 | pub fn par_query<'a, T: 'a + Send>(&'a self) -> impl ParallelIterator + 'a 136 | where 137 | World: QueryFrom<'a, T>, 138 | { 139 | QueryFrom::::par_query_from(self) 140 | } 141 | pub fn get<'a, T: 'a + Send>(&'a self, entity: Entity) -> Option 142 | where 143 | World: QueryFrom<'a, T>, 144 | { 145 | QueryFrom::::get_from(self, entity) 146 | } 147 | } 148 | pub trait WorldCreate { 149 | fn create(&mut self, e: T) -> Entity; 150 | } 151 | pub trait WorldDestroy { 152 | fn destroy(&mut self, e: Entity); 153 | } 154 | }; 155 | 156 | expanded.into() 157 | } 158 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/entity_impl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{spanned::Spanned, Error, Fields, ItemStruct}; 4 | 5 | use crate::helpers::format_collection_name; 6 | 7 | pub fn entity(_: TokenStream, input: TokenStream) -> TokenStream { 8 | let input_struct = syn::parse_macro_input!(input as ItemStruct); 9 | 10 | let Fields::Named(fields) = input_struct.clone().fields else { 11 | return Error::new( 12 | input_struct.fields.span(), 13 | "unnamed fields are not supported", 14 | ) 15 | .to_compile_error() 16 | .into(); 17 | }; 18 | 19 | let collection_fields: Vec<_> = fields 20 | .named 21 | .iter() 22 | .map(|field| { 23 | let field_name = &field.ident; 24 | let field_type = &field.ty; 25 | 26 | quote! { 27 | #field_name: Vec<#field_type> 28 | } 29 | }) 30 | .collect(); 31 | 32 | let ident = &input_struct.ident; 33 | let vis = &input_struct.vis; 34 | let collection_name = format_collection_name(ident); 35 | 36 | let create_push_calls: Vec<_> = fields 37 | .named 38 | .iter() 39 | .map(|field| { 40 | let field_name = &field.ident; 41 | quote! { 42 | self.#field_name.push(e.#field_name); 43 | } 44 | }) 45 | .collect(); 46 | let destroy_swap_and_pops = fields.named.iter().map(|field| { 47 | let field_name = &field.ident; 48 | quote! { 49 | self.#field_name.swap(old_index, last_index); 50 | self.#field_name.pop(); 51 | } 52 | }); 53 | 54 | quote! { 55 | #[export_tokens] 56 | #input_struct 57 | 58 | #[export_tokens] 59 | #[derive(Default)] 60 | #vis struct #collection_name { 61 | #( pub #collection_fields, )* 62 | pub entity: Vec, 63 | pub next_id: usize, 64 | pub index_lookup: Vec>, 65 | } 66 | 67 | impl WorldCreate<#ident> for #collection_name { 68 | fn create(&mut self, e: #ident) -> Entity { 69 | self.index_lookup.push(Some(self.entity.len())); 70 | let entity = Entity { 71 | entity_type: EntityType::#ident, 72 | id: self.next_id, 73 | }; 74 | self.entity.push(entity); 75 | 76 | #(#create_push_calls)* 77 | 78 | self.next_id += 1; 79 | entity 80 | } 81 | } 82 | 83 | 84 | impl WorldDestroy for #collection_name { 85 | fn destroy(&mut self, e: Entity) { 86 | if let Some(&Some(old_index)) = self.index_lookup.get(e.id) { 87 | self.index_lookup[e.id] = None; 88 | let last_index = self.entity.len() - 1; 89 | let last_entity = self.entity[last_index]; 90 | let is_now_last = old_index == last_index; 91 | 92 | #(#destroy_swap_and_pops)* 93 | 94 | self.entity.swap(old_index, last_index); 95 | self.entity.pop(); 96 | 97 | if !is_now_last { 98 | self.index_lookup[last_entity.id] = Some(old_index); 99 | } 100 | } 101 | } 102 | } 103 | 104 | impl #collection_name { 105 | pub fn new() -> Self { 106 | Self::default() 107 | } 108 | 109 | pub fn len(&self) -> usize { 110 | self.entity.len() 111 | } 112 | 113 | pub fn query_mut<'a, T: 'a>(&'a mut self) -> impl Iterator + 'a 114 | where 115 | #collection_name: QueryMutFrom<'a, T>, 116 | T: 'a + Send, 117 | { 118 | QueryMutFrom::::query_mut_from(self) 119 | } 120 | fn par_query_mut<'a, T: 'a>(&'a mut self) -> impl ParallelIterator + 'a 121 | where 122 | #collection_name: QueryMutFrom<'a, T>, 123 | T: 'a + Send, 124 | { 125 | QueryMutFrom::::par_query_mut_from(self) 126 | } 127 | pub fn get_mut<'a, T: 'a>(&'a mut self, entity: Entity) -> Option 128 | where 129 | #collection_name: QueryMutFrom<'a, T>, 130 | T: 'a + Send, 131 | { 132 | QueryMutFrom::::get_mut_from(self, entity) 133 | } 134 | 135 | pub fn query<'a, T: 'a>(&'a self) -> impl Iterator + 'a 136 | where 137 | #collection_name: QueryFrom<'a, T>, 138 | T: 'a + Send, 139 | { 140 | QueryFrom::::query_from(self) 141 | } 142 | pub fn par_query<'a, T: 'a>(&'a self) -> impl ParallelIterator + 'a 143 | where 144 | #collection_name: QueryFrom<'a, T>, 145 | T: 'a + Send, 146 | { 147 | QueryFrom::::par_query_from(self) 148 | } 149 | pub fn get<'a, T: 'a>(&'a self, entity: Entity) -> Option 150 | where 151 | #collection_name: QueryFrom<'a, T>, 152 | T: 'a + Send, 153 | { 154 | QueryFrom::::get_from(self, entity) 155 | } 156 | } 157 | } 158 | .into() 159 | } 160 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/testbed.rs: -------------------------------------------------------------------------------- 1 | mod integration_tests; 2 | 3 | use std::collections::HashSet; 4 | 5 | use zero_ecs::*; 6 | 7 | #[derive(Default)] 8 | struct Position(f32, f32); 9 | 10 | #[derive(Default)] 11 | struct Velocity(f32, f32); 12 | 13 | #[derive(Default)] 14 | struct EnemyComponent; 15 | 16 | #[derive(Default)] 17 | struct PlayerComponent; 18 | 19 | #[entity] 20 | #[derive(Default)] 21 | struct EnemyEntity { 22 | position: Position, 23 | velocity: Velocity, 24 | enemy_component: EnemyComponent, 25 | } 26 | 27 | #[entity] 28 | #[derive(Default)] 29 | struct PlayerEntity { 30 | position: Position, 31 | velocity: Velocity, 32 | player_component: PlayerComponent, 33 | } 34 | 35 | struct DeltaTime(f32); 36 | 37 | // system_for_each calls the system once for each successful query. 38 | #[system_for_each(World)] 39 | fn print_positions(position: &Position) { 40 | println!("print_positions - x: {}, y: {}", position.0, position.1); 41 | } 42 | 43 | // The default way of using systems. Needs to accept world, 0-many queries and optional resources. 44 | #[system(World)] 45 | fn print_enemy_positions(world: &World, query: Query<(&Position, &EnemyComponent)>) { 46 | world.with_query(query).iter().for_each(|(pos, _)| { 47 | println!("print_enemy_positions - x: {}, y: {}", pos.0, pos.1); 48 | }); 49 | } 50 | 51 | // Example of how to create queries outside a system. Should rarely be used. 52 | fn print_player_positions(world: &World) { 53 | // Defines a query called PlayerPositionsQuery for components Position & PlayerComponent 54 | make_query!(PlayerPositionsQuery, Position, PlayerComponent); 55 | world 56 | .with_query(Query::::new()) 57 | .iter() 58 | .for_each(|(pos, _)| { 59 | println!("print_player_positions - x: {}, y: {}", pos.0, pos.1); 60 | }); 61 | } 62 | 63 | // Systems can also mutate and accept resources (DeltaTime) 64 | #[system_for_each(World)] 65 | fn apply_velocity(position: &mut Position, velocity: &Velocity, delta_time: &DeltaTime) { 66 | position.0 += velocity.0 * delta_time.0; 67 | position.1 += velocity.1 * delta_time.0; 68 | } 69 | 70 | // More complex system with multiple queries. Since they are not mutating, 71 | // it's fine to nest queries. 72 | #[system(World)] 73 | fn collide_enemy_and_players( 74 | world: &mut World, // we are destroying entities so it needs to be mutable 75 | players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities 76 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies 77 | ) { 78 | let mut entities_to_destroy: HashSet = HashSet::new(); // we can't (for obvious reasons) destroy entities from within an iteration. 79 | 80 | world 81 | .with_query(players) 82 | .iter() 83 | .for_each(|(player_entity, player_position, _)| { 84 | world 85 | .with_query(enemies) 86 | .iter() 87 | .for_each(|(enemy_entity, enemy_position, _)| { 88 | if (player_position.0 - enemy_position.0).abs() < 3.0 89 | && (player_position.1 - enemy_position.1).abs() < 3.0 90 | { 91 | entities_to_destroy.insert(*player_entity); 92 | entities_to_destroy.insert(*enemy_entity); 93 | } 94 | }); 95 | }); 96 | 97 | for entity in entities_to_destroy { 98 | world.destroy(entity); 99 | } 100 | } 101 | 102 | struct CompanionComponent { 103 | target_entity: Option, 104 | } 105 | 106 | #[entity] 107 | struct CompanionEntity { 108 | position: Position, 109 | companion_component: CompanionComponent, 110 | } 111 | 112 | // Defines the world, must include all entities. 113 | ecs_world!(EnemyEntity, PlayerEntity, CompanionEntity); 114 | 115 | #[system(World)] 116 | fn companion_follow( 117 | world: &mut World, 118 | companions: Query<(&mut Position, &CompanionComponent)>, 119 | positions: Query<&Position>, 120 | ) { 121 | for companion_idx in 0..world.with_query_mut(companions).len() { 122 | // iterate the count of companions 123 | if let Some(target_position) = world 124 | .with_query_mut(companions) 125 | .at_mut(companion_idx) // get the companion at index companion_idx 126 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 127 | .and_then(|companion_target_entity| { 128 | // then get the VALUE of target position (meaning we don't use a reference to the position) 129 | world 130 | .with_query(positions) 131 | .get(companion_target_entity) // get the position for the companion_target_entity 132 | .map(|p: &Position| (p.0, p.1)) // map to get the VALUE 133 | }) 134 | { 135 | if let Some((companion_position, _)) = 136 | world.with_query_mut(companions).at_mut(companion_idx) 137 | // Then simply get the companion position 138 | { 139 | // and update it to the target's position 140 | companion_position.0 = target_position.0; 141 | companion_position.1 = target_position.1; 142 | } 143 | } 144 | } 145 | } 146 | 147 | fn main() { 148 | let delta_time = DeltaTime(1.0); 149 | 150 | let mut world = World::default(); 151 | 152 | for i in 0..10 { 153 | world.create(EnemyEntity { 154 | position: Position(i as f32, 5.0), 155 | velocity: Velocity(0.0, 1.0), 156 | ..Default::default() 157 | }); 158 | 159 | world.create(PlayerEntity { 160 | position: Position(5.0, i as f32), 161 | velocity: Velocity(1.0, 0.0), 162 | ..Default::default() 163 | }); 164 | } 165 | 166 | { 167 | let player_entity = world.create(PlayerEntity { 168 | position: Position(55.0, 165.0), 169 | velocity: Velocity(100.0, 50.0), 170 | ..Default::default() 171 | }); 172 | 173 | world.create(CompanionEntity { 174 | position: Position(0.0, 0.0), 175 | companion_component: CompanionComponent { 176 | target_entity: Some(player_entity), 177 | }, 178 | }); 179 | } 180 | 181 | world.collide_enemy_and_players(); 182 | world.apply_velocity(&delta_time); 183 | world.companion_follow(); 184 | world.print_positions(); 185 | world.print_enemy_positions(); 186 | print_player_positions(&world); 187 | } 188 | -------------------------------------------------------------------------------- /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 | > **This is version `0.3.*`.** 12 | > It is almost a complete rewrite from `0.2.*`. And has breaking changes. 13 | 14 | ## Instructions 15 | 16 | Add the dependency: 17 | 18 | ``` 19 | cargo add zero_ecs 20 | ``` 21 | 22 | Your `Cargo.toml` should look something like this: 23 | 24 | ```toml 25 | [dependencies] 26 | zero_ecs = "0.3.*" 27 | ``` 28 | 29 | ## Using the ECS 30 | 31 | ### Use 32 | 33 | ```rust 34 | use zero_ecs::*; 35 | ``` 36 | 37 | ### Components 38 | 39 | Components are just regular structs. 40 | 41 | ```rust 42 | #[derive(Default)] 43 | struct Position(f32, f32); 44 | 45 | #[derive(Default)] 46 | struct Velocity(f32, f32); 47 | ``` 48 | 49 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 50 | 51 | ```rust 52 | #[derive(Default)] 53 | struct EnemyComponent; 54 | 55 | #[derive(Default)] 56 | struct PlayerComponent; 57 | ``` 58 | 59 | ### Entities & World 60 | 61 | Entities are a collection of components. Use the `#[entity]` attribute to define them. 62 | 63 | ```rust 64 | #[entity] 65 | #[derive(Default)] 66 | struct EnemyEntity { 67 | position: Position, 68 | velocity: Velocity, 69 | enemy_component: EnemyComponent, 70 | } 71 | 72 | #[entity] 73 | #[derive(Default)] 74 | struct PlayerEntity { 75 | position: Position, 76 | velocity: Velocity, 77 | player_component: PlayerComponent, 78 | } 79 | ``` 80 | 81 | Define the world using the `ecs_world!` macro. Must include all entities. 82 | 83 | World and entities must be defined in the same crate. 84 | 85 | ```rust 86 | ecs_world!(EnemyEntity, PlayerEntity); 87 | ``` 88 | 89 | You can now instantiate the world like this: 90 | 91 | ```rust 92 | let mut world = World::default(); 93 | ``` 94 | 95 | And create entities like this: 96 | 97 | ```rust 98 | let player_entity = world.create(PlayerEntity { 99 | position: Position(55.0, 165.0), 100 | velocity: Velocity(100.0, 50.0), 101 | ..Default::default() 102 | }); 103 | ``` 104 | 105 | ### Systems 106 | 107 | Systems run the logic for the application. There are two types of systems: `#[system]` and `#[system_for_each]`. 108 | 109 | #### system_for_each 110 | 111 | `#[system_for_each]` calls the system once for each successful query. This is the simplest way to write systems. 112 | 113 | ```rust 114 | #[system_for_each(World)] 115 | fn print_positions(position: &Position) { 116 | println!("x: {}, y: {}", position.0, position.1); 117 | } 118 | ``` 119 | 120 | Systems can also mutate and accept resources: 121 | 122 | ```rust 123 | struct DeltaTime(f32); 124 | 125 | #[system_for_each(World)] 126 | fn apply_velocity(position: &mut Position, velocity: &Velocity, delta_time: &DeltaTime) { 127 | position.0 += velocity.0 * delta_time.0; 128 | position.1 += velocity.1 * delta_time.0; 129 | } 130 | ``` 131 | 132 | #### system 133 | 134 | The default way of using systems. Needs to accept world, 0-many queries and optional resources. 135 | 136 | ```rust 137 | #[system(World)] 138 | fn print_enemy_positions(world: &World, query: Query<(&Position, &EnemyComponent)>) { 139 | world.with_query(query).iter().for_each(|(pos, _)| { 140 | println!("x: {}, y: {}", pos.0, pos.1); 141 | }); 142 | } 143 | ``` 144 | 145 | ### Creating entities and calling systems 146 | 147 | ```rust 148 | fn main() { 149 | let delta_time = DeltaTime(1.0); 150 | let mut world = World::default(); 151 | 152 | for i in 0..10 { 153 | world.create(EnemyEntity { 154 | position: Position(i as f32, 5.0), 155 | velocity: Velocity(0.0, 1.0), 156 | ..Default::default() 157 | }); 158 | 159 | world.create(PlayerEntity { 160 | position: Position(5.0, i as f32), 161 | velocity: Velocity(1.0, 0.0), 162 | ..Default::default() 163 | }); 164 | } 165 | 166 | world.apply_velocity(&delta_time); 167 | world.print_positions(); 168 | world.print_enemy_positions(); 169 | } 170 | ``` 171 | 172 | ## More advanced 173 | 174 | ### Destroying entities 175 | 176 | To destroy entities, query for `&Entity` to identify them. You can't destroy entities from within an iteration. 177 | 178 | ```rust 179 | #[system(World)] 180 | fn collide_enemy_and_players( 181 | world: &mut World, 182 | players: Query<(&Entity, &Position, &PlayerComponent)>, 183 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, 184 | ) { 185 | let mut entities_to_destroy: HashSet = HashSet::new(); 186 | 187 | world 188 | .with_query(players) 189 | .iter() 190 | .for_each(|(player_entity, player_position, _)| { 191 | world 192 | .with_query(enemies) 193 | .iter() 194 | .for_each(|(enemy_entity, enemy_position, _)| { 195 | if (player_position.0 - enemy_position.0).abs() < 3.0 196 | && (player_position.1 - enemy_position.1).abs() < 3.0 197 | { 198 | entities_to_destroy.insert(*player_entity); 199 | entities_to_destroy.insert(*enemy_entity); 200 | } 201 | }); 202 | }); 203 | 204 | for entity in entities_to_destroy { 205 | world.destroy(entity); 206 | } 207 | } 208 | ``` 209 | 210 | ### Get & At 211 | 212 | `get` is identical to query but takes an `Entity`. 213 | `at` is identical to query but takes an index. 214 | 215 | Let's say you wanted an entity that follows a player: 216 | 217 | ```rust 218 | struct CompanionComponent { 219 | target_entity: Option, 220 | } 221 | 222 | #[entity] 223 | struct CompanionEntity { 224 | position: Position, 225 | companion_component: CompanionComponent, 226 | } 227 | ``` 228 | 229 | We can't simply iterate through the companions and get the target position because we can only have one borrow if the borrow is mutable. The solution is to iterate using index, only borrowing what we need for a short time: 230 | 231 | ```rust 232 | #[system(World)] 233 | fn companion_follow( 234 | world: &mut World, 235 | companions: Query<(&mut Position, &CompanionComponent)>, 236 | positions: Query<&Position>, 237 | ) { 238 | for companion_idx in 0..world.with_query_mut(companions).len() { 239 | // iterate the count of companions 240 | if let Some(target_position) = world 241 | .with_query_mut(companions) 242 | .at_mut(companion_idx) // get the companion at index companion_idx 243 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 244 | .and_then(|companion_target_entity| { 245 | // then get the VALUE of target position (meaning we don't use a reference to the position) 246 | world 247 | .with_query(positions) 248 | .get(companion_target_entity) // get the position for the companion_target_entity 249 | .map(|p: &Position| (p.0, p.1)) // map to get the VALUE 250 | }) 251 | { 252 | if let Some((companion_position, _)) = 253 | world.with_query_mut(companions).at_mut(companion_idx) 254 | // Then simply get the companion position 255 | { 256 | // and update it to the target's position 257 | companion_position.0 = target_position.0; 258 | companion_position.1 = target_position.1; 259 | } 260 | } 261 | } 262 | } 263 | ``` 264 | 265 | ### Manual queries 266 | 267 | You can create queries outside systems using `make_query!`. Should rarely be used. 268 | 269 | ```rust 270 | fn print_player_positions(world: &World) { 271 | make_query!(PlayerPositionsQuery, Position, PlayerComponent); 272 | world 273 | .with_query(Query::::new()) 274 | .iter() 275 | .for_each(|(pos, _)| { 276 | println!("x: {}, y: {}", pos.0, pos.1); 277 | }); 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/default_queries.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | pub fn get_default_queries() -> TokenStream { 4 | quote! { 5 | #[derive(Default, Debug)] 6 | pub struct Query { 7 | phantom: PhantomData, 8 | } 9 | 10 | impl Copy for Query {} 11 | impl Clone for Query { 12 | fn clone(&self) -> Self { Query { phantom: PhantomData, } } 13 | } 14 | 15 | #[allow(dead_code)] 16 | impl Query { 17 | pub fn new() -> Query { 18 | Query { 19 | phantom: PhantomData, 20 | } 21 | } 22 | } 23 | pub trait LenFrom<'a, T> 24 | where 25 | T: 'a + Send, 26 | { 27 | fn len(&'a self) -> usize; 28 | fn is_empty(&'a self) -> bool { 29 | self.len() == 0 30 | } 31 | } 32 | 33 | pub trait QueryFrom<'a, T> 34 | where 35 | T: 'a + Send, 36 | { 37 | fn query_from(&'a self) -> impl Iterator; 38 | fn par_query_from(&'a self) -> impl ParallelIterator; 39 | fn get_from(&'a self, entity: Entity) -> Option; 40 | fn at(&'a self, index: usize) -> Option; 41 | } 42 | pub trait QueryMutFrom<'a, T> 43 | where 44 | T: 'a + Send, 45 | { 46 | fn query_mut_from(&'a mut self) -> impl Iterator; 47 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator; 48 | fn get_mut_from(&'a mut self, entity: Entity) -> Option; 49 | fn at_mut(&'a mut self, index: usize) -> Option; 50 | } 51 | 52 | impl<'a, T: 'a + Send> Query 53 | { 54 | pub fn iter(&self, world: &'a World) -> impl Iterator + 'a 55 | where 56 | World: QueryFrom<'a, T>, 57 | { 58 | world.query_from() 59 | } 60 | } 61 | impl<'a, T: 'a + Send> Query 62 | { 63 | pub fn par_iter(&self, world: &'a World) -> impl ParallelIterator + 'a 64 | where 65 | World: QueryFrom<'a, T>, 66 | { 67 | world.par_query_from() 68 | } 69 | } 70 | impl<'a, T: 'a + Send> Query { 71 | pub fn iter_mut(&self, world: &'a mut World) -> impl Iterator + 'a 72 | where 73 | World: QueryMutFrom<'a, T>, 74 | { 75 | world.query_mut_from() 76 | } 77 | } 78 | impl<'a, T: 'a + Send> Query 79 | { 80 | pub fn par_iter_mut(&self, world: &'a mut World) -> impl ParallelIterator + 'a 81 | where 82 | World: QueryMutFrom<'a, T>, 83 | { 84 | world.par_query_mut_from() 85 | } 86 | } 87 | impl<'a, T: 'a + Send> Query { 88 | pub fn get(&self, world: &'a World, entity: Entity) -> Option 89 | where 90 | World: QueryFrom<'a, T>, 91 | { 92 | world.get_from(entity) 93 | } 94 | } 95 | impl<'a, T: 'a + Send> Query { 96 | pub fn get_mut(&self, world: &'a mut World, entity: Entity) -> Option 97 | where 98 | World: QueryMutFrom<'a, T>, 99 | { 100 | world.get_mut_from(entity) 101 | } 102 | } 103 | 104 | // implement len 105 | impl<'a, T: 'a + Send> Query { 106 | pub fn len(&self, world: &'a World) -> usize 107 | where 108 | World: LenFrom<'a, T>, 109 | { 110 | world.len() 111 | } 112 | } 113 | 114 | // impl at_mut 115 | impl<'a, T: 'a + Send> Query { 116 | pub fn at_mut(&self, world: &'a mut World, index: usize) -> Option 117 | where 118 | World: QueryMutFrom<'a, T>, 119 | { 120 | world.at_mut(index) 121 | } 122 | } 123 | 124 | // impl at 125 | impl<'a, T: 'a + Send> Query { 126 | pub fn at(&self, world: &'a World, index: usize) -> Option 127 | where 128 | World: QueryFrom<'a, T>, 129 | { 130 | world.at(index) 131 | } 132 | } 133 | 134 | 135 | pub struct WithQueryMut<'a, T> { 136 | query: Query, 137 | world: &'a mut World, 138 | } 139 | pub struct WithQuery<'a, T> { 140 | query: Query, 141 | world: &'a World, 142 | } 143 | 144 | #[allow(dead_code)] 145 | impl<'a, T> WithQueryMut<'a, T> 146 | where World: QueryMutFrom<'a, T>, 147 | World: LenFrom<'a, T>, 148 | T: 'a + Send, 149 | { 150 | pub fn iter_mut(&'a mut self) -> impl Iterator + 'a 151 | where T: Into 152 | { 153 | self.query.iter_mut(self.world).map(|e|e.into()) 154 | } 155 | pub fn par_iter_mut(&'a mut self) -> impl ParallelIterator + 'a 156 | where T: Into, U: Send 157 | { 158 | self.query.par_iter_mut(self.world).map(|e|e.into()) 159 | } 160 | pub fn get_mut(&'a mut self, entity: Entity) -> Option 161 | where T: Into, 162 | { 163 | self.query.get_mut(self.world, entity).map(|e| e.into()) 164 | } 165 | 166 | pub fn len(&'a mut self) -> usize { 167 | self.query.len(self.world) 168 | } 169 | 170 | pub fn at_mut(&'a mut self, index: usize) -> Option 171 | where T: Into, 172 | { 173 | self.query.at_mut(self.world, index).map(|e| e.into()) 174 | } 175 | 176 | pub fn is_empty(&'a mut self) -> bool { 177 | self.query.len(self.world) == 0 178 | } 179 | } 180 | 181 | #[allow(dead_code)] 182 | impl<'a, T> WithQuery<'a, T> 183 | where World: QueryFrom<'a, T>, 184 | World: LenFrom<'a, T>, 185 | T: 'a + Send, 186 | { 187 | pub fn iter(&'a self) -> impl Iterator + 'a 188 | where T: Into, U: Send 189 | { 190 | self.query.iter(self.world).map(|e|e.into()) 191 | } 192 | pub fn par_iter(&'a self) -> impl ParallelIterator + 'a 193 | where T: Into, U: Send 194 | { 195 | self.query.par_iter(self.world).map(|e|e.into()) 196 | } 197 | pub fn get(&'a self, entity: Entity) -> Option 198 | where T: Into, U: Send 199 | { 200 | self.query.get(self.world, entity).map(|e|e.into()) 201 | } 202 | pub fn len(&'a self) -> usize { 203 | self.query.len(self.world) 204 | } 205 | pub fn at(&'a self, index: usize) -> Option 206 | where T: Into, U: Send 207 | { 208 | self.query.at(self.world, index).map(|e|e.into()) 209 | } 210 | pub fn is_empty(&'a self) -> bool { 211 | self.query.len(self.world) == 0 212 | } 213 | } 214 | 215 | #[allow(dead_code)] 216 | impl World { 217 | pub fn with_query_mut<'a, T: 'a + Send>(&'a mut self, query: Query) -> WithQueryMut<'a, T> 218 | where 219 | World: QueryMutFrom<'a, T>, 220 | { 221 | WithQueryMut { 222 | query, 223 | world: self, 224 | } 225 | } 226 | } 227 | #[allow(dead_code)] 228 | impl World { 229 | pub fn with_query<'a, T: 'a + Send>(&'a self, query: Query) -> WithQuery<'a, T> 230 | where 231 | World: QueryFrom<'a, T>, 232 | { 233 | WithQuery { 234 | query, 235 | world: self, 236 | } 237 | } 238 | } 239 | 240 | 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /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 | > **This is version `0.3.*`.** 12 | > It is almost a complete rewrite from `0.2.*`. And has breaking changes. 13 | 14 | ## Instructions 15 | 16 | Add the dependency: 17 | 18 | ``` 19 | cargo add zero_ecs 20 | ``` 21 | 22 | Your `Cargo.toml` should look something like this: 23 | 24 | ```toml 25 | [dependencies] 26 | zero_ecs = "0.3.*" 27 | ``` 28 | 29 | ## Using the ECS 30 | 31 | ### Use 32 | 33 | ```rust 34 | use zero_ecs::*; 35 | ``` 36 | 37 | ### Components 38 | 39 | Components are just regular structs. 40 | 41 | ```rust 42 | #[derive(Default)] 43 | struct Position(f32, f32); 44 | 45 | #[derive(Default)] 46 | struct Velocity(f32, f32); 47 | ``` 48 | 49 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 50 | 51 | ```rust 52 | #[derive(Default)] 53 | struct EnemyComponent; 54 | 55 | #[derive(Default)] 56 | struct PlayerComponent; 57 | ``` 58 | 59 | ### Entities & World 60 | 61 | Entities are a collection of components. Use the `#[entity]` attribute to define them. 62 | 63 | ```rust 64 | #[entity] 65 | #[derive(Default)] 66 | struct EnemyEntity { 67 | position: Position, 68 | velocity: Velocity, 69 | enemy_component: EnemyComponent, 70 | } 71 | 72 | #[entity] 73 | #[derive(Default)] 74 | struct PlayerEntity { 75 | position: Position, 76 | velocity: Velocity, 77 | player_component: PlayerComponent, 78 | } 79 | ``` 80 | 81 | Define the world using the `ecs_world!` macro. Must include all entities. 82 | 83 | World and entities must be defined in the same crate. 84 | 85 | ```rust 86 | ecs_world!(EnemyEntity, PlayerEntity); 87 | ``` 88 | 89 | You can now instantiate the world like this: 90 | 91 | ```rust 92 | let mut world = World::default(); 93 | ``` 94 | 95 | And create entities like this: 96 | 97 | ```rust 98 | let player_entity = world.create(PlayerEntity { 99 | position: Position(55.0, 165.0), 100 | velocity: Velocity(100.0, 50.0), 101 | ..Default::default() 102 | }); 103 | ``` 104 | 105 | ### Systems 106 | 107 | Systems run the logic for the application. There are two types of systems: `#[system]` and `#[system_for_each]`. 108 | 109 | #### system_for_each 110 | 111 | `#[system_for_each]` calls the system once for each successful query. This is the simplest way to write systems. 112 | 113 | ```rust 114 | #[system_for_each(World)] 115 | fn print_positions(position: &Position) { 116 | println!("x: {}, y: {}", position.0, position.1); 117 | } 118 | ``` 119 | 120 | Systems can also mutate and accept resources: 121 | 122 | ```rust 123 | struct DeltaTime(f32); 124 | 125 | #[system_for_each(World)] 126 | fn apply_velocity(position: &mut Position, velocity: &Velocity, delta_time: &DeltaTime) { 127 | position.0 += velocity.0 * delta_time.0; 128 | position.1 += velocity.1 * delta_time.0; 129 | } 130 | ``` 131 | 132 | #### system 133 | 134 | The default way of using systems. Needs to accept world, 0-many queries and optional resources. 135 | 136 | ```rust 137 | #[system(World)] 138 | fn print_enemy_positions(world: &World, query: Query<(&Position, &EnemyComponent)>) { 139 | world.with_query(query).iter().for_each(|(pos, _)| { 140 | println!("x: {}, y: {}", pos.0, pos.1); 141 | }); 142 | } 143 | ``` 144 | 145 | ### Creating entities and calling systems 146 | 147 | ```rust 148 | fn main() { 149 | let delta_time = DeltaTime(1.0); 150 | let mut world = World::default(); 151 | 152 | for i in 0..10 { 153 | world.create(EnemyEntity { 154 | position: Position(i as f32, 5.0), 155 | velocity: Velocity(0.0, 1.0), 156 | ..Default::default() 157 | }); 158 | 159 | world.create(PlayerEntity { 160 | position: Position(5.0, i as f32), 161 | velocity: Velocity(1.0, 0.0), 162 | ..Default::default() 163 | }); 164 | } 165 | 166 | world.apply_velocity(&delta_time); 167 | world.print_positions(); 168 | world.print_enemy_positions(); 169 | } 170 | ``` 171 | 172 | ## More advanced 173 | 174 | ### Destroying entities 175 | 176 | To destroy entities, query for `&Entity` to identify them. You can't destroy entities from within an iteration. 177 | 178 | ```rust 179 | #[system(World)] 180 | fn collide_enemy_and_players( 181 | world: &mut World, 182 | players: Query<(&Entity, &Position, &PlayerComponent)>, 183 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, 184 | ) { 185 | let mut entities_to_destroy: HashSet = HashSet::new(); 186 | 187 | world 188 | .with_query(players) 189 | .iter() 190 | .for_each(|(player_entity, player_position, _)| { 191 | world 192 | .with_query(enemies) 193 | .iter() 194 | .for_each(|(enemy_entity, enemy_position, _)| { 195 | if (player_position.0 - enemy_position.0).abs() < 3.0 196 | && (player_position.1 - enemy_position.1).abs() < 3.0 197 | { 198 | entities_to_destroy.insert(*player_entity); 199 | entities_to_destroy.insert(*enemy_entity); 200 | } 201 | }); 202 | }); 203 | 204 | for entity in entities_to_destroy { 205 | world.destroy(entity); 206 | } 207 | } 208 | ``` 209 | 210 | ### Get & At 211 | 212 | `get` is identical to query but takes an `Entity`. 213 | `at` is identical to query but takes an index. 214 | 215 | Let's say you wanted an entity that follows a player: 216 | 217 | ```rust 218 | struct CompanionComponent { 219 | target_entity: Option, 220 | } 221 | 222 | #[entity] 223 | struct CompanionEntity { 224 | position: Position, 225 | companion_component: CompanionComponent, 226 | } 227 | ``` 228 | 229 | We can't simply iterate through the companions and get the target position because we can only have one borrow if the borrow is mutable. The solution is to iterate using index, only borrowing what we need for a short time: 230 | 231 | ```rust 232 | #[system(World)] 233 | fn companion_follow( 234 | world: &mut World, 235 | companions: Query<(&mut Position, &CompanionComponent)>, 236 | positions: Query<&Position>, 237 | ) { 238 | for companion_idx in 0..world.with_query_mut(companions).len() { 239 | // iterate the count of companions 240 | if let Some(target_position) = world 241 | .with_query_mut(companions) 242 | .at_mut(companion_idx) // get the companion at index companion_idx 243 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 244 | .and_then(|companion_target_entity| { 245 | // then get the VALUE of target position (meaning we don't use a reference to the position) 246 | world 247 | .with_query(positions) 248 | .get(companion_target_entity) // get the position for the companion_target_entity 249 | .map(|p: &Position| (p.0, p.1)) // map to get the VALUE 250 | }) 251 | { 252 | if let Some((companion_position, _)) = 253 | world.with_query_mut(companions).at_mut(companion_idx) 254 | // Then simply get the companion position 255 | { 256 | // and update it to the target's position 257 | companion_position.0 = target_position.0; 258 | companion_position.1 = target_position.1; 259 | } 260 | } 261 | } 262 | } 263 | ``` 264 | 265 | ### Manual queries 266 | 267 | You can create queries outside systems using `make_query!`. Should rarely be used. 268 | 269 | ```rust 270 | fn print_player_positions(world: &World) { 271 | make_query!(PlayerPositionsQuery, Position, PlayerComponent); 272 | world 273 | .with_query(Query::::new()) 274 | .iter() 275 | .for_each(|(pos, _)| { 276 | println!("x: {}, y: {}", pos.0, pos.1); 277 | }); 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /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 | > **This is version `0.3.*`.** 12 | > It is almost a complete rewrite from `0.2.*`. And has breaking changes. 13 | 14 | ## Instructions 15 | 16 | Add the dependency: 17 | 18 | ``` 19 | cargo add zero_ecs 20 | ``` 21 | 22 | Your `Cargo.toml` should look something like this: 23 | 24 | ```toml 25 | [dependencies] 26 | zero_ecs = "0.3.*" 27 | ``` 28 | 29 | ## Using the ECS 30 | 31 | ### Use 32 | 33 | ```rust 34 | use zero_ecs::*; 35 | ``` 36 | 37 | ### Components 38 | 39 | Components are just regular structs. 40 | 41 | ```rust 42 | #[derive(Default)] 43 | struct Position(f32, f32); 44 | 45 | #[derive(Default)] 46 | struct Velocity(f32, f32); 47 | ``` 48 | 49 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 50 | 51 | ```rust 52 | #[derive(Default)] 53 | struct EnemyComponent; 54 | 55 | #[derive(Default)] 56 | struct PlayerComponent; 57 | ``` 58 | 59 | ### Entities & World 60 | 61 | Entities are a collection of components. Use the `#[entity]` attribute to define them. 62 | 63 | ```rust 64 | #[entity] 65 | #[derive(Default)] 66 | struct EnemyEntity { 67 | position: Position, 68 | velocity: Velocity, 69 | enemy_component: EnemyComponent, 70 | } 71 | 72 | #[entity] 73 | #[derive(Default)] 74 | struct PlayerEntity { 75 | position: Position, 76 | velocity: Velocity, 77 | player_component: PlayerComponent, 78 | } 79 | ``` 80 | 81 | Define the world using the `ecs_world!` macro. Must include all entities. 82 | 83 | World and entities must be defined in the same crate. 84 | 85 | ```rust 86 | ecs_world!(EnemyEntity, PlayerEntity); 87 | ``` 88 | 89 | You can now instantiate the world like this: 90 | 91 | ```rust 92 | let mut world = World::default(); 93 | ``` 94 | 95 | And create entities like this: 96 | 97 | ```rust 98 | let player_entity = world.create(PlayerEntity { 99 | position: Position(55.0, 165.0), 100 | velocity: Velocity(100.0, 50.0), 101 | ..Default::default() 102 | }); 103 | ``` 104 | 105 | ### Systems 106 | 107 | Systems run the logic for the application. There are two types of systems: `#[system]` and `#[system_for_each]`. 108 | 109 | #### system_for_each 110 | 111 | `#[system_for_each]` calls the system once for each successful query. This is the simplest way to write systems. 112 | 113 | ```rust 114 | #[system_for_each(World)] 115 | fn print_positions(position: &Position) { 116 | println!("x: {}, y: {}", position.0, position.1); 117 | } 118 | ``` 119 | 120 | Systems can also mutate and accept resources: 121 | 122 | ```rust 123 | struct DeltaTime(f32); 124 | 125 | #[system_for_each(World)] 126 | fn apply_velocity(position: &mut Position, velocity: &Velocity, delta_time: &DeltaTime) { 127 | position.0 += velocity.0 * delta_time.0; 128 | position.1 += velocity.1 * delta_time.0; 129 | } 130 | ``` 131 | 132 | #### system 133 | 134 | The default way of using systems. Needs to accept world, 0-many queries and optional resources. 135 | 136 | ```rust 137 | #[system(World)] 138 | fn print_enemy_positions(world: &World, query: Query<(&Position, &EnemyComponent)>) { 139 | world.with_query(query).iter().for_each(|(pos, _)| { 140 | println!("x: {}, y: {}", pos.0, pos.1); 141 | }); 142 | } 143 | ``` 144 | 145 | ### Creating entities and calling systems 146 | 147 | ```rust 148 | fn main() { 149 | let delta_time = DeltaTime(1.0); 150 | let mut world = World::default(); 151 | 152 | for i in 0..10 { 153 | world.create(EnemyEntity { 154 | position: Position(i as f32, 5.0), 155 | velocity: Velocity(0.0, 1.0), 156 | ..Default::default() 157 | }); 158 | 159 | world.create(PlayerEntity { 160 | position: Position(5.0, i as f32), 161 | velocity: Velocity(1.0, 0.0), 162 | ..Default::default() 163 | }); 164 | } 165 | 166 | world.apply_velocity(&delta_time); 167 | world.print_positions(); 168 | world.print_enemy_positions(); 169 | } 170 | ``` 171 | 172 | ## More advanced 173 | 174 | ### Destroying entities 175 | 176 | To destroy entities, query for `&Entity` to identify them. You can't destroy entities from within an iteration. 177 | 178 | ```rust 179 | #[system(World)] 180 | fn collide_enemy_and_players( 181 | world: &mut World, 182 | players: Query<(&Entity, &Position, &PlayerComponent)>, 183 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, 184 | ) { 185 | let mut entities_to_destroy: HashSet = HashSet::new(); 186 | 187 | world 188 | .with_query(players) 189 | .iter() 190 | .for_each(|(player_entity, player_position, _)| { 191 | world 192 | .with_query(enemies) 193 | .iter() 194 | .for_each(|(enemy_entity, enemy_position, _)| { 195 | if (player_position.0 - enemy_position.0).abs() < 3.0 196 | && (player_position.1 - enemy_position.1).abs() < 3.0 197 | { 198 | entities_to_destroy.insert(*player_entity); 199 | entities_to_destroy.insert(*enemy_entity); 200 | } 201 | }); 202 | }); 203 | 204 | for entity in entities_to_destroy { 205 | world.destroy(entity); 206 | } 207 | } 208 | ``` 209 | 210 | ### Get & At 211 | 212 | `get` is identical to query but takes an `Entity`. 213 | `at` is identical to query but takes an index. 214 | 215 | Let's say you wanted an entity that follows a player: 216 | 217 | ```rust 218 | struct CompanionComponent { 219 | target_entity: Option, 220 | } 221 | 222 | #[entity] 223 | struct CompanionEntity { 224 | position: Position, 225 | companion_component: CompanionComponent, 226 | } 227 | ``` 228 | 229 | We can't simply iterate through the companions and get the target position because we can only have one borrow if the borrow is mutable. The solution is to iterate using index, only borrowing what we need for a short time: 230 | 231 | ```rust 232 | #[system(World)] 233 | fn companion_follow( 234 | world: &mut World, 235 | companions: Query<(&mut Position, &CompanionComponent)>, 236 | positions: Query<&Position>, 237 | ) { 238 | for companion_idx in 0..world.with_query_mut(companions).len() { 239 | // iterate the count of companions 240 | if let Some(target_position) = world 241 | .with_query_mut(companions) 242 | .at_mut(companion_idx) // get the companion at index companion_idx 243 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 244 | .and_then(|companion_target_entity| { 245 | // then get the VALUE of target position (meaning we don't use a reference to the position) 246 | world 247 | .with_query(positions) 248 | .get(companion_target_entity) // get the position for the companion_target_entity 249 | .map(|p: &Position| (p.0, p.1)) // map to get the VALUE 250 | }) 251 | { 252 | if let Some((companion_position, _)) = 253 | world.with_query_mut(companions).at_mut(companion_idx) 254 | // Then simply get the companion position 255 | { 256 | // and update it to the target's position 257 | companion_position.0 = target_position.0; 258 | companion_position.1 = target_position.1; 259 | } 260 | } 261 | } 262 | } 263 | ``` 264 | 265 | ### Manual queries 266 | 267 | You can create queries outside systems using `make_query!`. Should rarely be used. 268 | 269 | ```rust 270 | fn print_player_positions(world: &World) { 271 | make_query!(PlayerPositionsQuery, Position, PlayerComponent); 272 | world 273 | .with_query(Query::::new()) 274 | .iter() 275 | .for_each(|(pos, _)| { 276 | println!("x: {}, y: {}", pos.0, pos.1); 277 | }); 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /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 | > **This is version `0.3.*`.** 12 | > It is almost a complete rewrite from `0.2.*`. And has breaking changes. 13 | 14 | ## Instructions 15 | 16 | Add the dependency: 17 | 18 | ``` 19 | cargo add zero_ecs 20 | ``` 21 | 22 | Your `Cargo.toml` should look something like this: 23 | 24 | ```toml 25 | [dependencies] 26 | zero_ecs = "0.3.*" 27 | ``` 28 | 29 | ## Using the ECS 30 | 31 | ### Use 32 | 33 | ```rust 34 | use zero_ecs::*; 35 | ``` 36 | 37 | ### Components 38 | 39 | Components are just regular structs. 40 | 41 | ```rust 42 | #[derive(Default)] 43 | struct Position(f32, f32); 44 | 45 | #[derive(Default)] 46 | struct Velocity(f32, f32); 47 | ``` 48 | 49 | It is normal to "tag" entities with a component in ECS to be able to single out those entities in systems. 50 | 51 | ```rust 52 | #[derive(Default)] 53 | struct EnemyComponent; 54 | 55 | #[derive(Default)] 56 | struct PlayerComponent; 57 | ``` 58 | 59 | ### Entities & World 60 | 61 | Entities are a collection of components. Use the `#[entity]` attribute to define them. 62 | 63 | ```rust 64 | #[entity] 65 | #[derive(Default)] 66 | struct EnemyEntity { 67 | position: Position, 68 | velocity: Velocity, 69 | enemy_component: EnemyComponent, 70 | } 71 | 72 | #[entity] 73 | #[derive(Default)] 74 | struct PlayerEntity { 75 | position: Position, 76 | velocity: Velocity, 77 | player_component: PlayerComponent, 78 | } 79 | ``` 80 | 81 | Define the world using the `ecs_world!` macro. Must include all entities. 82 | 83 | World and entities must be defined in the same crate. 84 | 85 | ```rust 86 | ecs_world!(EnemyEntity, PlayerEntity); 87 | ``` 88 | 89 | You can now instantiate the world like this: 90 | 91 | ```rust 92 | let mut world = World::default(); 93 | ``` 94 | 95 | And create entities like this: 96 | 97 | ```rust 98 | let player_entity = world.create(PlayerEntity { 99 | position: Position(55.0, 165.0), 100 | velocity: Velocity(100.0, 50.0), 101 | ..Default::default() 102 | }); 103 | ``` 104 | 105 | ### Systems 106 | 107 | Systems run the logic for the application. There are two types of systems: `#[system]` and `#[system_for_each]`. 108 | 109 | #### system_for_each 110 | 111 | `#[system_for_each]` calls the system once for each successful query. This is the simplest way to write systems. 112 | 113 | ```rust 114 | #[system_for_each(World)] 115 | fn print_positions(position: &Position) { 116 | println!("x: {}, y: {}", position.0, position.1); 117 | } 118 | ``` 119 | 120 | Systems can also mutate and accept resources: 121 | 122 | ```rust 123 | struct DeltaTime(f32); 124 | 125 | #[system_for_each(World)] 126 | fn apply_velocity(position: &mut Position, velocity: &Velocity, delta_time: &DeltaTime) { 127 | position.0 += velocity.0 * delta_time.0; 128 | position.1 += velocity.1 * delta_time.0; 129 | } 130 | ``` 131 | 132 | #### system 133 | 134 | The default way of using systems. Needs to accept world, 0-many queries and optional resources. 135 | 136 | ```rust 137 | #[system(World)] 138 | fn print_enemy_positions(world: &World, query: Query<(&Position, &EnemyComponent)>) { 139 | world.with_query(query).iter().for_each(|(pos, _)| { 140 | println!("x: {}, y: {}", pos.0, pos.1); 141 | }); 142 | } 143 | ``` 144 | 145 | ### Creating entities and calling systems 146 | 147 | ```rust 148 | fn main() { 149 | let delta_time = DeltaTime(1.0); 150 | let mut world = World::default(); 151 | 152 | for i in 0..10 { 153 | world.create(EnemyEntity { 154 | position: Position(i as f32, 5.0), 155 | velocity: Velocity(0.0, 1.0), 156 | ..Default::default() 157 | }); 158 | 159 | world.create(PlayerEntity { 160 | position: Position(5.0, i as f32), 161 | velocity: Velocity(1.0, 0.0), 162 | ..Default::default() 163 | }); 164 | } 165 | 166 | world.apply_velocity(&delta_time); 167 | world.print_positions(); 168 | world.print_enemy_positions(); 169 | } 170 | ``` 171 | 172 | ## More advanced 173 | 174 | ### Destroying entities 175 | 176 | To destroy entities, query for `&Entity` to identify them. You can't destroy entities from within an iteration. 177 | 178 | ```rust 179 | #[system(World)] 180 | fn collide_enemy_and_players( 181 | world: &mut World, 182 | players: Query<(&Entity, &Position, &PlayerComponent)>, 183 | enemies: Query<(&Entity, &Position, &EnemyComponent)>, 184 | ) { 185 | let mut entities_to_destroy: HashSet = HashSet::new(); 186 | 187 | world 188 | .with_query(players) 189 | .iter() 190 | .for_each(|(player_entity, player_position, _)| { 191 | world 192 | .with_query(enemies) 193 | .iter() 194 | .for_each(|(enemy_entity, enemy_position, _)| { 195 | if (player_position.0 - enemy_position.0).abs() < 3.0 196 | && (player_position.1 - enemy_position.1).abs() < 3.0 197 | { 198 | entities_to_destroy.insert(*player_entity); 199 | entities_to_destroy.insert(*enemy_entity); 200 | } 201 | }); 202 | }); 203 | 204 | for entity in entities_to_destroy { 205 | world.destroy(entity); 206 | } 207 | } 208 | ``` 209 | 210 | ### Get & At 211 | 212 | `get` is identical to query but takes an `Entity`. 213 | `at` is identical to query but takes an index. 214 | 215 | Let's say you wanted an entity that follows a player: 216 | 217 | ```rust 218 | struct CompanionComponent { 219 | target_entity: Option, 220 | } 221 | 222 | #[entity] 223 | struct CompanionEntity { 224 | position: Position, 225 | companion_component: CompanionComponent, 226 | } 227 | ``` 228 | 229 | We can't simply iterate through the companions and get the target position because we can only have one borrow if the borrow is mutable. The solution is to iterate using index, only borrowing what we need for a short time: 230 | 231 | ```rust 232 | #[system(World)] 233 | fn companion_follow( 234 | world: &mut World, 235 | companions: Query<(&mut Position, &CompanionComponent)>, 236 | positions: Query<&Position>, 237 | ) { 238 | for companion_idx in 0..world.with_query_mut(companions).len() { 239 | // iterate the count of companions 240 | if let Some(target_position) = world 241 | .with_query_mut(companions) 242 | .at_mut(companion_idx) // get the companion at index companion_idx 243 | .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none 244 | .and_then(|companion_target_entity| { 245 | // then get the VALUE of target position (meaning we don't use a reference to the position) 246 | world 247 | .with_query(positions) 248 | .get(companion_target_entity) // get the position for the companion_target_entity 249 | .map(|p: &Position| (p.0, p.1)) // map to get the VALUE 250 | }) 251 | { 252 | if let Some((companion_position, _)) = 253 | world.with_query_mut(companions).at_mut(companion_idx) 254 | // Then simply get the companion position 255 | { 256 | // and update it to the target's position 257 | companion_position.0 = target_position.0; 258 | companion_position.1 = target_position.1; 259 | } 260 | } 261 | } 262 | } 263 | ``` 264 | 265 | ### Manual queries 266 | 267 | You can create queries outside systems using `make_query!`. Should rarely be used. 268 | 269 | ```rust 270 | fn print_player_positions(world: &World) { 271 | make_query!(PlayerPositionsQuery, Position, PlayerComponent); 272 | world 273 | .with_query(Query::::new()) 274 | .iter() 275 | .for_each(|(pos, _)| { 276 | println!("x: {}, y: {}", pos.0, pos.1); 277 | }); 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 10 | 11 | [[package]] 12 | name = "const-random" 13 | version = "0.1.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" 16 | dependencies = [ 17 | "const-random-macro", 18 | ] 19 | 20 | [[package]] 21 | name = "const-random-macro" 22 | version = "0.1.16" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" 25 | dependencies = [ 26 | "getrandom", 27 | "once_cell", 28 | "tiny-keccak", 29 | ] 30 | 31 | [[package]] 32 | name = "convert_case" 33 | version = "0.7.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 36 | dependencies = [ 37 | "unicode-segmentation", 38 | ] 39 | 40 | [[package]] 41 | name = "convert_case" 42 | version = "0.8.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" 45 | dependencies = [ 46 | "unicode-segmentation", 47 | ] 48 | 49 | [[package]] 50 | name = "crossbeam-deque" 51 | version = "0.8.5" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 54 | dependencies = [ 55 | "crossbeam-epoch", 56 | "crossbeam-utils", 57 | ] 58 | 59 | [[package]] 60 | name = "crossbeam-epoch" 61 | version = "0.9.18" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 64 | dependencies = [ 65 | "crossbeam-utils", 66 | ] 67 | 68 | [[package]] 69 | name = "crossbeam-utils" 70 | version = "0.8.19" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 73 | 74 | [[package]] 75 | name = "crunchy" 76 | version = "0.2.3" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 79 | 80 | [[package]] 81 | name = "derive-syn-parse" 82 | version = "0.2.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" 85 | dependencies = [ 86 | "proc-macro2", 87 | "quote", 88 | "syn", 89 | ] 90 | 91 | [[package]] 92 | name = "derive_more" 93 | version = "2.0.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 96 | dependencies = [ 97 | "derive_more-impl", 98 | ] 99 | 100 | [[package]] 101 | name = "derive_more-impl" 102 | version = "2.0.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 105 | dependencies = [ 106 | "convert_case 0.7.1", 107 | "proc-macro2", 108 | "quote", 109 | "syn", 110 | "unicode-xid", 111 | ] 112 | 113 | [[package]] 114 | name = "either" 115 | version = "1.10.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 118 | 119 | [[package]] 120 | name = "extend" 121 | version = "1.2.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "311a6d2f1f9d60bff73d2c78a0af97ed27f79672f15c238192a5bbb64db56d00" 124 | dependencies = [ 125 | "proc-macro2", 126 | "quote", 127 | "syn", 128 | ] 129 | 130 | [[package]] 131 | name = "getrandom" 132 | version = "0.2.15" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 135 | dependencies = [ 136 | "cfg-if", 137 | "libc", 138 | "wasi", 139 | ] 140 | 141 | [[package]] 142 | name = "intehan_util_dump" 143 | version = "0.1.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "8b00a7be6dd8e42683b97f24d73a7b7ba43d94c81cd5b0c21433be611c521c8f" 146 | 147 | [[package]] 148 | name = "itertools" 149 | version = "0.12.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 152 | dependencies = [ 153 | "either", 154 | ] 155 | 156 | [[package]] 157 | name = "libc" 158 | version = "0.2.169" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 161 | 162 | [[package]] 163 | name = "macro_magic" 164 | version = "0.6.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "0625ea4e11e9b0f45e650aa92a94e83719433992dcafd5f130c6435d2ea67107" 167 | dependencies = [ 168 | "macro_magic_core", 169 | "macro_magic_macros", 170 | "quote", 171 | "syn", 172 | ] 173 | 174 | [[package]] 175 | name = "macro_magic_core" 176 | version = "0.6.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "10a0d941b46232df6f549002d493fc30b720b617b5d7bfb58febda386c2c5abe" 179 | dependencies = [ 180 | "const-random", 181 | "derive-syn-parse", 182 | "macro_magic_core_macros", 183 | "proc-macro2", 184 | "quote", 185 | "syn", 186 | ] 187 | 188 | [[package]] 189 | name = "macro_magic_core_macros" 190 | version = "0.6.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "31e7b9b365f39f573850b21c1e241234e29426ee8b0d6ee13637f714fad7390f" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "syn", 197 | ] 198 | 199 | [[package]] 200 | name = "macro_magic_macros" 201 | version = "0.6.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "54256681b01f4e5b038a619b24896f8c76d61995075909226d4e6bcf60bad525" 204 | dependencies = [ 205 | "macro_magic_core", 206 | "quote", 207 | "syn", 208 | ] 209 | 210 | [[package]] 211 | name = "once_cell" 212 | version = "1.20.3" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 215 | 216 | [[package]] 217 | name = "proc-macro2" 218 | version = "1.0.101" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 221 | dependencies = [ 222 | "unicode-ident", 223 | ] 224 | 225 | [[package]] 226 | name = "quote" 227 | version = "1.0.40" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 230 | dependencies = [ 231 | "proc-macro2", 232 | ] 233 | 234 | [[package]] 235 | name = "rayon" 236 | version = "1.9.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" 239 | dependencies = [ 240 | "either", 241 | "rayon-core", 242 | ] 243 | 244 | [[package]] 245 | name = "rayon-core" 246 | version = "1.12.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 249 | dependencies = [ 250 | "crossbeam-deque", 251 | "crossbeam-utils", 252 | ] 253 | 254 | [[package]] 255 | name = "syn" 256 | version = "2.0.106" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "unicode-ident", 263 | ] 264 | 265 | [[package]] 266 | name = "tiny-keccak" 267 | version = "2.0.2" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" 270 | dependencies = [ 271 | "crunchy", 272 | ] 273 | 274 | [[package]] 275 | name = "unicode-ident" 276 | version = "1.0.12" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 279 | 280 | [[package]] 281 | name = "unicode-segmentation" 282 | version = "1.12.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 285 | 286 | [[package]] 287 | name = "unicode-xid" 288 | version = "0.2.6" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 291 | 292 | [[package]] 293 | name = "wasi" 294 | version = "0.11.0+wasi-snapshot-preview1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 297 | 298 | [[package]] 299 | name = "zero_ecs" 300 | version = "0.3.4" 301 | dependencies = [ 302 | "derive_more", 303 | "extend", 304 | "itertools", 305 | "macro_magic", 306 | "rayon", 307 | "zero_ecs_macros", 308 | ] 309 | 310 | [[package]] 311 | name = "zero_ecs_build" 312 | version = "0.3.4" 313 | dependencies = [ 314 | "zero_ecs_macros", 315 | ] 316 | 317 | [[package]] 318 | name = "zero_ecs_macros" 319 | version = "0.3.4" 320 | dependencies = [ 321 | "convert_case 0.8.0", 322 | "intehan_util_dump", 323 | "macro_magic", 324 | "proc-macro2", 325 | "quote", 326 | "syn", 327 | ] 328 | 329 | [[package]] 330 | name = "zero_ecs_testbed" 331 | version = "0.3.4" 332 | dependencies = [ 333 | "zero_ecs", 334 | ] 335 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/system_impl.rs: -------------------------------------------------------------------------------- 1 | use intehan_util_dump::dump; 2 | use proc_macro::TokenStream; 3 | use quote::{format_ident, quote}; 4 | use syn::{ 5 | parse_macro_input, FnArg, GenericArgument, Ident, ItemFn, Pat, PatIdent, PatType, 6 | PathArguments, Type, 7 | }; 8 | 9 | pub fn system(_attr: TokenStream, item: TokenStream) -> TokenStream { 10 | let input_fn = parse_macro_input!(item as ItemFn); 11 | 12 | let fn_vis = &input_fn.vis; 13 | let fn_sig = &input_fn.sig; 14 | let fn_name = &fn_sig.ident; 15 | let fn_block = &input_fn.block; 16 | 17 | let system_args: Vec = collect_fn_sig(&fn_sig); 18 | assert_eq!( 19 | system_args 20 | .iter() 21 | .filter(|a| matches!(a, SystemArg::World(_))) 22 | .count(), 23 | 1, 24 | "#[system] must have exactly ONE &World or &mut World argument" 25 | ); 26 | 27 | let query_args: Vec<_> = system_args 28 | .iter() 29 | .filter_map(|arg| { 30 | if let SystemArg::Query(query) = arg { 31 | Some(query) 32 | } else { 33 | None 34 | } 35 | }) 36 | .collect(); 37 | 38 | let query_codes: Vec<_> = query_args 39 | .iter() 40 | .map(|arg| { 41 | let struct_name = format_ident!("Query__{}", arg.name); 42 | let arg_name = &arg.name_ident; 43 | 44 | let components: Vec<_> = arg 45 | .fields 46 | .iter() 47 | .map(|field| { 48 | let ty_ident = format_ident!("{}", field.ty); 49 | 50 | if field.mutable { 51 | quote! { 52 | &'a mut #ty_ident 53 | } 54 | } else { 55 | quote! { 56 | &'a #ty_ident 57 | } 58 | } 59 | }) 60 | .collect(); 61 | 62 | let code = quote! { 63 | #[query(World)] 64 | struct #struct_name<'a>(#(#components),*); 65 | 66 | let #arg_name = Query::<#struct_name>::new(); 67 | }; 68 | 69 | code 70 | }) 71 | .collect(); 72 | 73 | let out_fn_args: Vec<_> = system_args 74 | .iter() 75 | .filter_map(|arg| arg.get_arg_code()) 76 | .collect(); 77 | let resource_fn_args: Vec<_> = system_args 78 | .iter() 79 | .filter(|arg| matches!(arg, SystemArg::Resource(_))) 80 | .filter_map(|arg| arg.get_arg_code()) 81 | .collect(); 82 | let call_fn_code: Vec<_> = system_args 83 | .iter() 84 | .filter_map(|arg| arg.get_call_code()) 85 | .collect(); 86 | 87 | let ext_name = format_ident!("__ext_{}", fn_name); 88 | let expanded = quote! { 89 | #fn_vis fn #fn_name(#(#out_fn_args),*) { 90 | #(#query_codes)* 91 | 92 | #fn_block 93 | } 94 | 95 | 96 | #[ext(name = #ext_name)] 97 | pub impl World { 98 | fn #fn_name(&mut self, #(#resource_fn_args),*) { 99 | #fn_name(#(#call_fn_code),*); 100 | } 101 | } 102 | }; 103 | 104 | expanded.into() 105 | } 106 | 107 | #[derive(Debug)] 108 | struct ArgQueryField { 109 | mutable: bool, 110 | ty: String, 111 | } 112 | 113 | #[derive(Debug)] 114 | struct ArgQuery { 115 | name: String, 116 | name_ident: Ident, 117 | fields: Vec, 118 | } 119 | 120 | #[derive(Debug)] 121 | struct ArgResource { 122 | name_ident: Ident, 123 | mutable: bool, 124 | ty: String, 125 | by_ref: bool, 126 | } 127 | 128 | #[derive(Debug)] 129 | struct ArgWorld { 130 | name_ident: Ident, 131 | mutable: bool, 132 | } 133 | 134 | #[derive(Debug)] 135 | enum SystemArg { 136 | World(ArgWorld), 137 | Query(ArgQuery), 138 | Resource(ArgResource), 139 | } 140 | 141 | impl SystemArg { 142 | fn get_arg_code(&self) -> Option { 143 | match self { 144 | SystemArg::World(arg_world) => { 145 | let name = &arg_world.name_ident; 146 | Some(if arg_world.mutable { 147 | quote! { #name: &mut World } 148 | } else { 149 | quote! { #name: &World } 150 | }) 151 | } 152 | SystemArg::Resource(arg_resource) => { 153 | let name = &arg_resource.name_ident; 154 | let ty = format_ident!("{}", arg_resource.ty); 155 | if arg_resource.by_ref { 156 | Some(if arg_resource.mutable { 157 | quote! { #name: &mut #ty } 158 | } else { 159 | quote! { #name: &#ty } 160 | }) 161 | } else { 162 | Some(if arg_resource.mutable { 163 | quote! { #name: mut #ty } 164 | } else { 165 | quote! { #name: #ty } 166 | }) 167 | } 168 | } 169 | _ => None, 170 | } 171 | } 172 | 173 | // for when calling fn (self, foo, bar) 174 | fn get_call_code(&self) -> Option { 175 | match self { 176 | SystemArg::World(_) => Some(quote! { self }), 177 | SystemArg::Resource(arg_resource) => { 178 | let name = &arg_resource.name_ident; 179 | Some(quote! { #name }) 180 | } 181 | _ => None, 182 | } 183 | } 184 | } 185 | 186 | fn collect_fn_sig(fn_sig: &&syn::Signature) -> Vec { 187 | let mut system_args = vec![]; 188 | 189 | for arg in &fn_sig.inputs { 190 | match arg { 191 | FnArg::Receiver(_) => { 192 | panic!("#[system] functions cannot take self") 193 | } 194 | FnArg::Typed(PatType { pat, ty, .. }) => { 195 | // get the name (pat) 196 | let arg_ident = if let Pat::Ident(PatIdent { ident, .. }) = &**pat { 197 | ident.clone() 198 | } else { 199 | panic!("Unsupported argument pattern in #[system_for_each]"); 200 | }; 201 | 202 | // &** i don't understand but do what the compiler tells me. 203 | match &**ty { 204 | Type::Reference(ty) => { 205 | let is_mutable = ty.mutability.is_some(); 206 | let Type::Path(type_path) = &*ty.elem else { 207 | panic!("Reference: failed to get elem path #[system]"); 208 | }; 209 | let is_world = type_path 210 | .path 211 | .segments 212 | .last() 213 | .map(|s| s.ident == "World") 214 | .expect("path should have one last segment"); 215 | let Some(type_path) = type_path.path.get_ident() else { 216 | panic!("Reference: failed to get type path #[system]") 217 | }; 218 | 219 | if is_world { 220 | let arg = ArgWorld { 221 | mutable: is_mutable, 222 | name_ident: arg_ident, 223 | }; 224 | system_args.push(SystemArg::World(arg)); 225 | } else { 226 | let arg = ArgResource { 227 | mutable: is_mutable, 228 | ty: type_path.to_string(), 229 | by_ref: true, 230 | name_ident: arg_ident, 231 | }; 232 | system_args.push(SystemArg::Resource(arg)); 233 | } 234 | } 235 | Type::Path(ty) => { 236 | let segment = &ty 237 | .path 238 | .segments 239 | .first() 240 | .expect("#[system] no first segment"); 241 | let outer_ident = &segment.ident; 242 | 243 | if outer_ident == "Query" { 244 | let PathArguments::AngleBracketed(args) = &segment.arguments else { 245 | panic!("#[system] Expected angle bracketed arguments for Query, but none were found"); 246 | }; 247 | 248 | let arg = &args 249 | .args 250 | .first() 251 | .expect("#[system] args args should not be empty"); 252 | 253 | match arg { 254 | GenericArgument::Type(Type::Tuple(tuple)) => { 255 | let mut arg_query_fields = vec![]; 256 | 257 | for elem in &tuple.elems { 258 | let Type::Reference(elem) = elem else { 259 | panic!("#[system] Expected a reference type inside the tuple, but found something else"); 260 | }; 261 | let is_mutable = elem.mutability.is_some(); 262 | let Type::Path(elem_path) = &*elem.elem else { 263 | panic!("#[system] Expected a path type inside the reference, but found something else"); 264 | }; 265 | let elem_ident = 266 | &elem_path.path.segments.first().unwrap().ident; 267 | 268 | arg_query_fields.push(ArgQueryField { 269 | mutable: is_mutable, 270 | ty: elem_ident.to_string(), 271 | }); 272 | } 273 | 274 | let arg_query = ArgQuery { 275 | name: arg_ident.to_string(), 276 | name_ident: arg_ident, 277 | fields: arg_query_fields, 278 | }; 279 | system_args.push(SystemArg::Query(arg_query)); 280 | } 281 | GenericArgument::Type(Type::Reference(elem)) => { 282 | let is_mutable = elem.mutability.is_some(); 283 | let Type::Path(elem_path) = &*elem.elem else { 284 | panic!("#[system] Expected a path type inside the reference, but found something else"); 285 | }; 286 | let elem_ident = 287 | &elem_path.path.segments.first().unwrap().ident; 288 | let arg_query = ArgQuery { 289 | name: arg_ident.to_string(), 290 | name_ident: arg_ident, 291 | fields: vec![ArgQueryField { 292 | mutable: is_mutable, 293 | ty: elem_ident.to_string(), 294 | }], 295 | }; 296 | system_args.push(SystemArg::Query(arg_query)) 297 | } 298 | GenericArgument::Type(Type::Paren(type_paren)) => { 299 | let Type::Reference(ref elem) = *type_paren.elem else { 300 | panic!("#[system] Expected a reference type inside the tuple, but found something else"); 301 | }; 302 | 303 | let is_mutable = elem.mutability.is_some(); 304 | let Type::Path(elem_path) = &*elem.elem else { 305 | panic!("#[system] Expected a path type inside the reference, but found something else"); 306 | }; 307 | let elem_ident = 308 | &elem_path.path.segments.first().unwrap().ident; 309 | let arg_query = ArgQuery { 310 | name: arg_ident.to_string(), 311 | name_ident: arg_ident, 312 | fields: vec![ArgQueryField { 313 | mutable: is_mutable, 314 | ty: elem_ident.to_string(), 315 | }], 316 | }; 317 | system_args.push(SystemArg::Query(arg_query)) 318 | } 319 | GenericArgument::Type(ty) => { 320 | panic!("#[system] Unsupported type in Query: {:?}", ty); 321 | } 322 | _ => { 323 | panic!( 324 | "#[system] Unsupported generic argument type: {:?}", 325 | arg 326 | ); 327 | } 328 | } 329 | } else { 330 | let Some(ty_ident) = ty.path.get_ident() else { 331 | panic!("#[system] failed tog get_ident ty.path"); 332 | }; 333 | let arg = ArgResource { 334 | mutable: false, 335 | ty: ty_ident.to_string(), 336 | by_ref: false, 337 | name_ident: arg_ident, 338 | }; 339 | system_args.push(SystemArg::Resource(arg)); 340 | } 341 | } 342 | _ => { 343 | dump!(ty); 344 | panic!("#[system] unsuppoerd type"); 345 | } 346 | } 347 | } 348 | } 349 | } 350 | system_args 351 | } 352 | -------------------------------------------------------------------------------- /zero_ecs_macros/src/query_impl.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use std::collections::HashSet; 4 | use syn::{spanned::Spanned, Error, Fields, ItemStruct, Type}; 5 | 6 | use crate::helpers::{format_collection_name, format_field_name}; 7 | 8 | #[derive(Debug)] 9 | pub struct CollectionComponentField { 10 | pub collection_name: String, 11 | pub field_name: String, 12 | pub field_type: String, 13 | } 14 | 15 | pub fn get_collection_component_fields( 16 | foreign_struct: ItemStruct, 17 | ) -> Vec { 18 | let Fields::Named(foreign_fields) = foreign_struct.fields else { 19 | panic!( 20 | "Unnamed fields are not supported: {:?}", 21 | foreign_struct.fields 22 | ); 23 | }; 24 | 25 | let collection_component_fields: Vec = foreign_fields 26 | .named 27 | .iter() 28 | .filter_map(|field| { 29 | if let Some(field_name) = &field.ident { 30 | if field_name.to_string().starts_with("__twcf__") { 31 | let field_name_str = field_name.to_string(); 32 | let field_name_str = field_name_str.replace("__twcf__", ""); 33 | let parts: Vec<_> = field_name_str.split("__").collect(); 34 | if parts.len() == 3 { 35 | let collection_name = parts[0]; 36 | let field_name = parts[1]; 37 | let field_type = parts[2]; 38 | Some(( 39 | collection_name.to_string(), 40 | field_name.to_string(), 41 | field_type.to_string(), 42 | )) 43 | } else { 44 | // print error to build 45 | eprintln!("Error: invalid field name: {}", field_name_str); 46 | None 47 | } 48 | } else { 49 | None 50 | } 51 | } else { 52 | None 53 | } 54 | }) 55 | .map( 56 | |(collection_name, field_name, field_type)| CollectionComponentField { 57 | collection_name, 58 | field_name, 59 | field_type, 60 | }, 61 | ) 62 | .collect(); 63 | 64 | collection_component_fields 65 | } 66 | 67 | pub fn query(attr: TokenStream, item: TokenStream) -> TokenStream { 68 | let foreign_struct = syn::parse_macro_input!(attr as ItemStruct); 69 | let collection_component_fields = get_collection_component_fields(foreign_struct); 70 | let local_struct = syn::parse_macro_input!(item as ItemStruct); 71 | let Fields::Unnamed(local_fields) = local_struct.fields else { 72 | return Error::new(local_struct.fields.span(), "named fields are not supported") 73 | .to_compile_error() 74 | .into(); 75 | }; 76 | 77 | let local_struct_name = local_struct.ident; 78 | 79 | let local_fields: Vec<_> = local_fields.unnamed.iter().collect(); 80 | let mut any_mutable_local_fields = false; 81 | let types_to_query: Vec = local_fields 82 | .iter() 83 | .filter_map(|field| { 84 | // only the type name, ignore all ' and < and stuff 85 | // start by casting to a reference 86 | if let Type::Reference(ty) = &field.ty { 87 | if ty.mutability.is_some() { 88 | any_mutable_local_fields = true; 89 | } 90 | 91 | // then get the type 92 | if let Type::Path(path) = &*ty.elem { 93 | // then get the first segment 94 | if let Some(segment) = path.path.segments.first() { 95 | // then get the ident 96 | let ident = &segment.ident; 97 | Some(ident.to_string()) 98 | } else { 99 | None 100 | } 101 | } else { 102 | None 103 | } 104 | } else { 105 | None 106 | } 107 | }) 108 | .collect(); 109 | 110 | let all_collections: HashSet<_> = collection_component_fields 111 | .iter() 112 | .map(|field| &field.collection_name) 113 | .collect(); 114 | 115 | // get all collections that have all of the local_field types 116 | let matching_collections: Vec<_> = all_collections 117 | .iter() 118 | .filter_map(|collection_name| { 119 | let collection_types = collection_component_fields 120 | .iter() 121 | .filter(|field| field.collection_name == **collection_name) 122 | .map(|field| &field.field_type) 123 | .collect::>(); 124 | 125 | if types_to_query 126 | .iter() 127 | // entity is special case 128 | .filter(|field_name| *field_name != "Entity") 129 | .all(|field_name| collection_types.contains(field_name)) 130 | { 131 | Some(*collection_name) 132 | } else { 133 | None 134 | } 135 | }) 136 | .collect(); 137 | 138 | let query_codes: Vec<_> = matching_collections 139 | .iter() 140 | .map(|&collection_name| { 141 | let collection_type_name = format_collection_name(collection_name); 142 | 143 | let collection_field_names: Vec<_> = types_to_query.iter() 144 | .map(|field_type| { 145 | // find from collection_component_fields the one that matches field_type and the collection name. Should only be one 146 | let field_name = if field_type != "Entity" { collection_component_fields 147 | .iter() 148 | .find(|field| &field.collection_name == collection_name && field.field_type == *field_type) 149 | .unwrap_or_else(|| panic!("expect_collection_component_fields field_type: {}", field_type)) 150 | .field_name 151 | .clone() 152 | } else { 153 | "entity".into() 154 | }; 155 | quote::format_ident!("{}", field_name) 156 | }).collect(); 157 | 158 | let query_code = quote! { 159 | impl<'a> QueryFrom<'a, #local_struct_name<'a>> for #collection_type_name { 160 | fn query_from(&'a self) -> impl Iterator> { 161 | izip!(#(self.#collection_field_names.iter()),*) 162 | .map(|(#(#collection_field_names),*)| #local_struct_name(#(#collection_field_names),*)) 163 | } 164 | 165 | fn par_query_from(&'a self) -> impl ParallelIterator> { 166 | izip_par!(#(self.#collection_field_names.par_iter()),*) 167 | .map(|(#(#collection_field_names),*)| #local_struct_name(#(#collection_field_names),*)) 168 | } 169 | 170 | fn get_from(&'a self, entity: Entity) -> Option<#local_struct_name<'a>> { 171 | if let Some(&Some(index)) = self.index_lookup.get(entity.id) { 172 | Some(#local_struct_name( 173 | #(self.#collection_field_names.get(index)?),* 174 | )) 175 | } else { 176 | None 177 | } 178 | } 179 | 180 | fn at(&'a self, index: usize) -> Option<#local_struct_name<'a>> { 181 | Some(#local_struct_name( 182 | #(self.#collection_field_names.get(index)?),* 183 | )) 184 | } 185 | } 186 | }; 187 | 188 | let query_mut_code = quote! { 189 | impl<'a> QueryMutFrom<'a, #local_struct_name<'a>> for #collection_type_name { 190 | fn query_mut_from(&'a mut self) -> impl Iterator> { 191 | izip!(#(self.#collection_field_names.iter_mut()),*) 192 | .map(|(#(#collection_field_names),*)| #local_struct_name(#(#collection_field_names),*)) 193 | } 194 | 195 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator> { 196 | izip_par!(#(self.#collection_field_names.par_iter_mut()),*) 197 | .map(|(#(#collection_field_names),*)| #local_struct_name(#(#collection_field_names),*)) 198 | } 199 | 200 | fn get_mut_from(&'a mut self, entity: Entity) -> Option<#local_struct_name<'a>> { 201 | if let Some(&Some(index)) = self.index_lookup.get(entity.id) { 202 | Some(#local_struct_name( 203 | #(self.#collection_field_names.get_mut(index)?),* 204 | )) 205 | } else { 206 | None 207 | } 208 | } 209 | 210 | fn at_mut(&'a mut self, index: usize) -> Option<#local_struct_name<'a>> { 211 | Some(#local_struct_name( 212 | #(self.#collection_field_names.get_mut(index)?),* 213 | )) 214 | } 215 | } 216 | }; 217 | 218 | let len_from_code = quote! { 219 | impl<'a> LenFrom<'a, #local_struct_name<'a>> for #collection_type_name { 220 | fn len(&'a self) -> usize { 221 | self.entity.len() 222 | } 223 | } 224 | }; 225 | 226 | if any_mutable_local_fields { 227 | quote! { 228 | #query_mut_code 229 | #len_from_code 230 | } 231 | } else { 232 | quote! { 233 | #query_code 234 | #query_mut_code 235 | #len_from_code 236 | } 237 | } 238 | }) 239 | .collect(); 240 | 241 | let world_query_code = { 242 | let world_fields: Vec<_> = matching_collections.iter().map(format_field_name).collect(); 243 | 244 | let query_from_body_parts: Vec<_> = matching_collections 245 | .iter() 246 | .map(|name| { 247 | 248 | let field_name = format_field_name(name); 249 | let collection_name = format_collection_name(name); 250 | 251 | quote! { 252 | <#collection_name as QueryFrom<'a, #local_struct_name<'a>>>::query_from(& self.#field_name) 253 | } 254 | }) 255 | .collect(); 256 | 257 | let query_mut_from_body_parts: Vec<_> = matching_collections 258 | .iter() 259 | .map(|name| { 260 | let field_name = format_field_name(name); 261 | let collection_name = format_collection_name(name); 262 | quote! { 263 | <#collection_name as QueryMutFrom<'a, #local_struct_name<'a>>>::query_mut_from(&mut self.#field_name) 264 | } 265 | }) 266 | .collect(); 267 | 268 | let par_query_from_body_parts: Vec<_> = matching_collections 269 | .iter() 270 | .map(|name| { 271 | let field_name = format_field_name(name); 272 | let collection_name = format_collection_name(name); 273 | quote! { 274 | <#collection_name as QueryFrom<'a, #local_struct_name<'a>>>::par_query_from(&self.#field_name) 275 | } 276 | }) 277 | .collect(); 278 | let par_query_mut_from_body_parts: Vec<_> = matching_collections 279 | .iter() 280 | .map(|name| { 281 | let field_name = format_field_name(name); 282 | let collection_name = format_collection_name(name); 283 | quote! { 284 | <#collection_name as QueryMutFrom<'a, #local_struct_name<'a>>>::par_query_mut_from(&mut self.#field_name) 285 | } 286 | }) 287 | .collect(); 288 | 289 | let get_from_body_parts: Vec<_> = matching_collections 290 | .iter() 291 | .map(|name| { 292 | let field_name = format_field_name(name); 293 | let enum_name = quote::format_ident!("{}", name); 294 | 295 | quote! { 296 | EntityType::#enum_name => self.#field_name.get(entity) 297 | } 298 | }) 299 | .collect(); 300 | 301 | let get_mut_from_body_parts: Vec<_> = matching_collections 302 | .iter() 303 | .map(|name| { 304 | let field_name = format_field_name(name); 305 | let enum_name = quote::format_ident!("{}", name); 306 | 307 | quote! { 308 | EntityType::#enum_name => self.#field_name.get_mut(entity) 309 | } 310 | }) 311 | .collect(); 312 | 313 | let at_parts: Vec<_> = world_fields 314 | .iter() 315 | .map(|name| { 316 | quote! { 317 | { 318 | let len = self.#name.len(); 319 | if index < len { 320 | return self.#name.at(index); 321 | } 322 | index -= len; 323 | } 324 | } 325 | }) 326 | .collect(); 327 | 328 | let at_mut_parts: Vec<_> = world_fields 329 | .iter() 330 | .map(|name| { 331 | quote! { 332 | { 333 | let len = self.#name.len(); 334 | if index < len { 335 | return self.#name.at_mut(index); 336 | } 337 | index -= len; 338 | } 339 | } 340 | }) 341 | .collect(); 342 | 343 | let len_parts: Vec<_> = world_fields 344 | .iter() 345 | .map(|name| { 346 | quote! { 347 | self.#name.len() 348 | } 349 | }) 350 | .collect(); 351 | 352 | let query_code = quote! { 353 | 354 | impl<'a> QueryFrom<'a, #local_struct_name<'a>> for World { 355 | fn query_from(&'a self) -> impl Iterator> { 356 | chain!( 357 | #(#query_from_body_parts),* 358 | ) 359 | } 360 | 361 | fn par_query_from(&'a self) -> impl ParallelIterator> { 362 | chain_par!( 363 | #(#par_query_from_body_parts),* 364 | ) 365 | } 366 | 367 | fn get_from(&'a self, entity: Entity) -> Option<#local_struct_name<'a>> { 368 | match entity.entity_type { 369 | #(#get_from_body_parts,)* 370 | _ => None, 371 | } 372 | } 373 | 374 | fn at(&'a self, index: usize) -> Option<#local_struct_name<'a>> { 375 | let mut index = index; 376 | #(#at_parts)* 377 | None 378 | } 379 | } 380 | }; 381 | 382 | let query_mut_code = quote! { 383 | impl<'a> QueryMutFrom<'a, #local_struct_name<'a>> for World { 384 | fn query_mut_from(&'a mut self) -> impl Iterator> { 385 | chain!( 386 | #(#query_mut_from_body_parts),* 387 | ) 388 | } 389 | 390 | fn par_query_mut_from(&'a mut self) -> impl ParallelIterator> { 391 | chain_par!( 392 | #(#par_query_mut_from_body_parts),* 393 | ) 394 | } 395 | 396 | fn get_mut_from(&'a mut self, entity: Entity) -> Option<#local_struct_name<'a>> { 397 | match entity.entity_type { 398 | #(#get_mut_from_body_parts,)* 399 | _ => None, 400 | } 401 | } 402 | 403 | fn at_mut(&'a mut self, index: usize) -> Option<#local_struct_name<'a>> { 404 | let mut index = index; 405 | #(#at_mut_parts)* 406 | None 407 | } 408 | } 409 | }; 410 | 411 | let sum = if len_parts.is_empty() { 412 | quote! { 413 | 0 414 | } 415 | } else { 416 | quote! { 417 | sum!( 418 | #(#len_parts),* 419 | ) 420 | } 421 | }; 422 | 423 | let len_from_code = quote! { 424 | impl<'a> LenFrom<'a, #local_struct_name<'a>> for World { 425 | fn len(&'a self) -> usize { 426 | #sum 427 | } 428 | } 429 | }; 430 | 431 | if any_mutable_local_fields { 432 | quote! { 433 | #query_mut_code 434 | #len_from_code 435 | } 436 | } else { 437 | quote! { 438 | #query_code 439 | #query_mut_code 440 | #len_from_code 441 | } 442 | } 443 | }; 444 | 445 | quote! { 446 | 447 | #[derive(From, Into)] 448 | struct #local_struct_name<'a> (#(#local_fields),*); 449 | 450 | #(#query_codes)* 451 | 452 | #world_query_code 453 | } 454 | .into() 455 | } 456 | -------------------------------------------------------------------------------- /zero_ecs_testbed/src/integration_tests/test_systems_comprehensive.rs: -------------------------------------------------------------------------------- 1 | //! Comprehensive integration tests for the zero_ecs system. 2 | //! 3 | //! This test suite covers: 4 | //! - `#[system(World)]` with various Query types (single, two, three components) 5 | //! - `#[system_for_each(World)]` with various component combinations 6 | //! - Mutable and immutable component queries 7 | //! - Mixed mutability queries (some mutable, some immutable) 8 | //! - Resources: reference resources, value resources, mutable resources 9 | //! - Combinations of queries and resources 10 | //! - Selective system execution (only affecting entities with specific components) 11 | //! - Entity deletion and verification of correct entity removal 12 | //! - Multiple entity deletion scenarios 13 | //! - System call frequency verification (ensuring systems only affect intended entities) 14 | 15 | #![allow(dead_code)] 16 | 17 | use zero_ecs::*; 18 | 19 | // Components for testing 20 | #[derive(Debug, PartialEq)] 21 | pub struct Counter(usize); 22 | 23 | #[derive(Debug, PartialEq)] 24 | pub struct Health(i32); 25 | 26 | #[derive(Debug, PartialEq)] 27 | pub struct Speed(f32); 28 | 29 | #[derive(Debug, PartialEq)] 30 | pub struct Name(String); 31 | 32 | #[derive(Default)] 33 | pub struct TagA; 34 | 35 | #[derive(Default)] 36 | pub struct TagB; 37 | 38 | // Resources for testing 39 | #[derive(Debug)] 40 | struct DeltaTime(f32); 41 | 42 | #[derive(Debug)] 43 | struct TickCount(usize); 44 | 45 | #[derive(Debug)] 46 | struct GameState { 47 | paused: bool, 48 | score: i32, 49 | } 50 | 51 | // Entities 52 | #[entity] 53 | pub struct EntityA { 54 | counter: Counter, 55 | health: Health, 56 | tag_a: TagA, 57 | } 58 | 59 | #[entity] 60 | pub struct EntityB { 61 | counter: Counter, 62 | speed: Speed, 63 | tag_b: TagB, 64 | } 65 | 66 | #[entity] 67 | pub struct EntityAB { 68 | counter: Counter, 69 | health: Health, 70 | speed: Speed, 71 | name: Name, 72 | } 73 | 74 | ecs_world!(EntityA, EntityB, EntityAB); 75 | 76 | // ============================================================================ 77 | // Tests for #[system(World)] with various Query types 78 | // ============================================================================ 79 | 80 | #[system(World)] 81 | fn system_single_component_immutable(world: &World, query: Query<&Counter>) { 82 | let q = world.with_query(query); 83 | // Just verify we can access 84 | let count = q.len(); 85 | for i in 0..count { 86 | let _counter: &Counter = q.at(i).unwrap(); 87 | } 88 | } 89 | 90 | #[system(World)] 91 | fn system_single_component_mutable(world: &mut World, query: Query<(&mut Counter, &Health)>) { 92 | world 93 | .with_query_mut(query) 94 | .iter_mut() 95 | .for_each(|(counter, _)| { 96 | counter.0 += 1; 97 | }); 98 | } 99 | 100 | #[system(World)] 101 | fn system_two_components_immutable(world: &World, query: Query<(&Counter, &Health)>) { 102 | let q = world.with_query(query); 103 | for (counter, health) in q.iter() { 104 | let _ = (counter.0, health.0); 105 | } 106 | } 107 | 108 | #[system(World)] 109 | fn system_two_components_mutable(world: &mut World, query: Query<(&mut Counter, &mut Health)>) { 110 | world 111 | .with_query_mut(query) 112 | .iter_mut() 113 | .for_each(|(counter, health)| { 114 | counter.0 += 1; 115 | health.0 += 5; 116 | }); 117 | } 118 | 119 | #[system(World)] 120 | fn system_two_components_mixed(world: &mut World, query: Query<(&mut Counter, &Health)>) { 121 | world 122 | .with_query_mut(query) 123 | .iter_mut() 124 | .for_each(|(counter, health)| { 125 | counter.0 += health.0 as usize; 126 | }); 127 | } 128 | 129 | #[system(World)] 130 | fn system_three_components_immutable(world: &World, query: Query<(&Counter, &Health, &Speed)>) { 131 | let q = world.with_query(query); 132 | for (counter, health, speed) in q.iter() { 133 | let _ = (counter.0, health.0, speed.0); 134 | } 135 | } 136 | 137 | #[system(World)] 138 | fn system_three_components_mutable( 139 | world: &mut World, 140 | query: Query<(&mut Counter, &mut Health, &mut Speed)>, 141 | ) { 142 | world 143 | .with_query_mut(query) 144 | .iter_mut() 145 | .for_each(|(counter, health, speed)| { 146 | counter.0 += 1; 147 | health.0 += 1; 148 | speed.0 += 1.0; 149 | }); 150 | } 151 | 152 | // ============================================================================ 153 | // Tests for #[system_for_each(World)] 154 | // ============================================================================ 155 | 156 | #[system_for_each(World)] 157 | fn for_each_single_mutable(counter: &mut Counter) { 158 | counter.0 += 1; 159 | } 160 | 161 | #[system_for_each(World)] 162 | fn for_each_single_immutable(counter: &Counter) { 163 | let _ = counter.0; 164 | } 165 | 166 | #[system_for_each(World)] 167 | fn for_each_two_components_mutable(counter: &mut Counter, health: &mut Health) { 168 | counter.0 += 1; 169 | health.0 += 2; 170 | } 171 | 172 | #[system_for_each(World)] 173 | fn for_each_two_components_mixed(counter: &mut Counter, health: &Health) { 174 | counter.0 += health.0 as usize; 175 | } 176 | 177 | #[system_for_each(World)] 178 | fn for_each_three_components(counter: &mut Counter, health: &Health, speed: &Speed) { 179 | counter.0 += (health.0 as f32 * speed.0) as usize; 180 | } 181 | 182 | // ============================================================================ 183 | // Tests with resources (reference resources only for system_for_each) 184 | // ============================================================================ 185 | 186 | #[system(World)] 187 | fn system_with_ref_resource( 188 | world: &mut World, 189 | query: Query<(&mut Counter, &Health)>, 190 | tick: &TickCount, 191 | ) { 192 | world 193 | .with_query_mut(query) 194 | .iter_mut() 195 | .for_each(|(counter, _)| { 196 | counter.0 += tick.0; 197 | }); 198 | } 199 | 200 | #[system(World)] 201 | fn system_with_value_resource( 202 | world: &mut World, 203 | query: Query<(&mut Counter, &Health)>, 204 | increment: usize, 205 | ) { 206 | world 207 | .with_query_mut(query) 208 | .iter_mut() 209 | .for_each(|(counter, _)| { 210 | counter.0 += increment; 211 | }); 212 | } 213 | 214 | #[system(World)] 215 | fn system_with_multiple_resources( 216 | world: &mut World, 217 | query: Query<(&mut Counter, &mut Health)>, 218 | tick: &TickCount, 219 | dt: &DeltaTime, 220 | multiplier: i32, 221 | ) { 222 | world 223 | .with_query_mut(query) 224 | .iter_mut() 225 | .for_each(|(counter, health)| { 226 | counter.0 += tick.0; 227 | health.0 += (dt.0 * multiplier as f32) as i32; 228 | }); 229 | } 230 | 231 | #[system(World)] 232 | fn system_with_mutable_resource(world: &World, query: Query<&Counter>, state: &mut GameState) { 233 | let q = world.with_query(query); 234 | let count = q.len(); 235 | for i in 0..count { 236 | let counter: &Counter = q.at(i).unwrap(); 237 | state.score += counter.0 as i32; 238 | } 239 | } 240 | 241 | #[system_for_each(World)] 242 | fn for_each_with_ref_resource(counter: &mut Counter, tick: &TickCount) { 243 | counter.0 += tick.0; 244 | } 245 | 246 | #[system_for_each(World)] 247 | fn for_each_with_value_resource(counter: &mut Counter, tick: &TickCount) { 248 | counter.0 += tick.0; 249 | } 250 | 251 | #[system_for_each(World)] 252 | fn for_each_with_multiple_ref_resources( 253 | counter: &mut Counter, 254 | health: &mut Health, 255 | tick: &TickCount, 256 | dt: &DeltaTime, 257 | ) { 258 | counter.0 += tick.0; 259 | health.0 += dt.0 as i32; 260 | } 261 | 262 | // ============================================================================ 263 | // Selective update systems 264 | // ============================================================================ 265 | 266 | #[system(World)] 267 | fn increment_only_tag_a(world: &mut World, query: Query<(&mut Counter, &TagA)>) { 268 | world 269 | .with_query_mut(query) 270 | .iter_mut() 271 | .for_each(|(counter, _)| { 272 | counter.0 += 100; 273 | }); 274 | } 275 | 276 | #[system(World)] 277 | fn increment_only_tag_b(world: &mut World, query: Query<(&mut Counter, &TagB)>) { 278 | world 279 | .with_query_mut(query) 280 | .iter_mut() 281 | .for_each(|(counter, _)| { 282 | counter.0 += 200; 283 | }); 284 | } 285 | 286 | #[system(World)] 287 | fn increment_with_health(world: &mut World, query: Query<(&mut Counter, &Health)>) { 288 | world 289 | .with_query_mut(query) 290 | .iter_mut() 291 | .for_each(|(counter, _)| { 292 | counter.0 += 1; 293 | }); 294 | } 295 | 296 | // Helper query for verification 297 | make_query!(QueryCounter, Counter); 298 | 299 | make_query!(QueryCounterHealth, Counter, Health); 300 | 301 | make_query!(QueryCounterHealthSpeed, Counter, Health, Speed); 302 | 303 | make_query!(QueryEntityCounter, Entity, Counter); 304 | 305 | make_query!(QueryEntityCounterTagA, Entity, Counter, TagA); 306 | 307 | make_query!(QueryEntityCounterTagB, Entity, Counter, TagB); 308 | 309 | make_query!(QueryEntityCounterName, Entity, Counter, Name); 310 | 311 | make_query!(QueryEntityCounterHealth, Entity, Counter, Health); 312 | 313 | // ============================================================================ 314 | // Integration Tests 315 | // ============================================================================ 316 | 317 | #[test] 318 | fn test_system_single_component_queries() { 319 | let mut world = World::default(); 320 | 321 | world.create(EntityA { 322 | counter: Counter(0), 323 | health: Health(100), 324 | tag_a: TagA, 325 | }); 326 | 327 | // Test immutable access 328 | world.system_single_component_immutable(); 329 | 330 | // Test mutable access 331 | world.system_single_component_mutable(); 332 | 333 | // Verify the counter was incremented 334 | let query = world.with_query(Query::::new()); 335 | let QueryCounter(counter) = query.at(0).unwrap(); 336 | assert_eq!(counter.0, 1); 337 | } 338 | 339 | #[test] 340 | fn test_system_two_component_queries() { 341 | let mut world = World::default(); 342 | 343 | world.create(EntityA { 344 | counter: Counter(10), 345 | health: Health(50), 346 | tag_a: TagA, 347 | }); 348 | 349 | world.system_two_components_immutable(); 350 | world.system_two_components_mutable(); 351 | 352 | let query = world.with_query(Query::::new()); 353 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 354 | assert_eq!(counter.0, 11); 355 | assert_eq!(health.0, 55); 356 | } 357 | 358 | #[test] 359 | fn test_system_two_component_mixed_mutability() { 360 | let mut world = World::default(); 361 | 362 | world.create(EntityA { 363 | counter: Counter(0), 364 | health: Health(7), 365 | tag_a: TagA, 366 | }); 367 | 368 | world.system_two_components_mixed(); 369 | 370 | let query = world.with_query(Query::::new()); 371 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 372 | assert_eq!(counter.0, 7); 373 | assert_eq!(health.0, 7); 374 | } 375 | 376 | #[test] 377 | fn test_system_three_component_queries() { 378 | let mut world = World::default(); 379 | 380 | world.create(EntityAB { 381 | counter: Counter(1), 382 | health: Health(10), 383 | speed: Speed(2.5), 384 | name: Name("test".to_string()), 385 | }); 386 | 387 | world.system_three_components_immutable(); 388 | world.system_three_components_mutable(); 389 | 390 | let query = world.with_query(Query::::new()); 391 | let QueryCounterHealthSpeed(counter, health, speed) = query.at(0).unwrap(); 392 | assert_eq!(counter.0, 2); 393 | assert_eq!(health.0, 11); 394 | assert_eq!(speed.0, 3.5); 395 | } 396 | 397 | #[test] 398 | fn test_for_each_single_component() { 399 | let mut world = World::default(); 400 | 401 | world.create(EntityA { 402 | counter: Counter(0), 403 | health: Health(100), 404 | tag_a: TagA, 405 | }); 406 | 407 | world.for_each_single_immutable(); 408 | world.for_each_single_mutable(); 409 | world.for_each_single_mutable(); 410 | world.for_each_single_mutable(); 411 | 412 | let query = world.with_query(Query::::new()); 413 | let QueryCounter(counter) = query.at(0).unwrap(); 414 | assert_eq!(counter.0, 3); 415 | } 416 | 417 | #[test] 418 | fn test_for_each_two_components() { 419 | let mut world = World::default(); 420 | 421 | world.create(EntityA { 422 | counter: Counter(5), 423 | health: Health(20), 424 | tag_a: TagA, 425 | }); 426 | 427 | world.for_each_two_components_mutable(); 428 | world.for_each_two_components_mutable(); 429 | 430 | let query = world.with_query(Query::::new()); 431 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 432 | assert_eq!(counter.0, 7); 433 | assert_eq!(health.0, 24); 434 | } 435 | 436 | #[test] 437 | fn test_for_each_with_mixed_mutability() { 438 | let mut world = World::default(); 439 | 440 | world.create(EntityA { 441 | counter: Counter(0), 442 | health: Health(5), 443 | tag_a: TagA, 444 | }); 445 | 446 | world.for_each_two_components_mixed(); 447 | 448 | let query = world.with_query(Query::::new()); 449 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 450 | assert_eq!(counter.0, 5); 451 | assert_eq!(health.0, 5); 452 | } 453 | 454 | #[test] 455 | fn test_for_each_three_components() { 456 | let mut world = World::default(); 457 | 458 | world.create(EntityAB { 459 | counter: Counter(0), 460 | health: Health(10), 461 | speed: Speed(2.0), 462 | name: Name("entity".to_string()), 463 | }); 464 | 465 | world.for_each_three_components(); 466 | 467 | let query = world.with_query(Query::::new()); 468 | let QueryCounter(counter) = query.at(0).unwrap(); 469 | assert_eq!(counter.0, 20); // 0 + (10 * 2.0) 470 | } 471 | 472 | #[test] 473 | fn test_system_with_ref_resource() { 474 | let mut world = World::default(); 475 | 476 | world.create(EntityA { 477 | counter: Counter(0), 478 | health: Health(100), 479 | tag_a: TagA, 480 | }); 481 | 482 | let tick = TickCount(3); 483 | world.system_with_ref_resource(&tick); 484 | world.system_with_ref_resource(&tick); 485 | 486 | let query = world.with_query(Query::::new()); 487 | let QueryCounter(counter) = query.at(0).unwrap(); 488 | assert_eq!(counter.0, 6); 489 | } 490 | 491 | #[test] 492 | fn test_system_with_value_resource() { 493 | let mut world = World::default(); 494 | 495 | world.create(EntityA { 496 | counter: Counter(10), 497 | health: Health(100), 498 | tag_a: TagA, 499 | }); 500 | 501 | world.system_with_value_resource(5); 502 | world.system_with_value_resource(7); 503 | 504 | let query = world.with_query(Query::::new()); 505 | let QueryCounter(counter) = query.at(0).unwrap(); 506 | assert_eq!(counter.0, 22); 507 | } 508 | 509 | #[test] 510 | fn test_system_with_multiple_resources() { 511 | let mut world = World::default(); 512 | 513 | world.create(EntityA { 514 | counter: Counter(0), 515 | health: Health(100), 516 | tag_a: TagA, 517 | }); 518 | 519 | let tick = TickCount(2); 520 | let dt = DeltaTime(0.5); 521 | world.system_with_multiple_resources(&tick, &dt, 10); 522 | 523 | let query = world.with_query(Query::::new()); 524 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 525 | assert_eq!(counter.0, 2); 526 | assert_eq!(health.0, 105); // 100 + (0.5 * 10) 527 | } 528 | 529 | #[test] 530 | fn test_system_with_mutable_resource() { 531 | let mut world = World::default(); 532 | 533 | world.create(EntityA { 534 | counter: Counter(10), 535 | health: Health(100), 536 | tag_a: TagA, 537 | }); 538 | world.create(EntityA { 539 | counter: Counter(20), 540 | health: Health(100), 541 | tag_a: TagA, 542 | }); 543 | 544 | let mut state = GameState { 545 | paused: false, 546 | score: 0, 547 | }; 548 | 549 | world.system_with_mutable_resource(&mut state); 550 | 551 | assert_eq!(state.score, 30); 552 | } 553 | 554 | #[test] 555 | fn test_for_each_with_ref_resource() { 556 | let mut world = World::default(); 557 | 558 | world.create(EntityA { 559 | counter: Counter(0), 560 | health: Health(100), 561 | tag_a: TagA, 562 | }); 563 | 564 | let tick = TickCount(5); 565 | world.for_each_with_ref_resource(&tick); 566 | 567 | let query = world.with_query(Query::::new()); 568 | let QueryCounter(counter) = query.at(0).unwrap(); 569 | assert_eq!(counter.0, 5); 570 | } 571 | 572 | #[test] 573 | fn test_for_each_with_multiple_ref_resources() { 574 | let mut world = World::default(); 575 | 576 | world.create(EntityA { 577 | counter: Counter(0), 578 | health: Health(100), 579 | tag_a: TagA, 580 | }); 581 | 582 | let tick = TickCount(3); 583 | let dt = DeltaTime(7.0); 584 | world.for_each_with_multiple_ref_resources(&tick, &dt); 585 | 586 | let query = world.with_query(Query::::new()); 587 | let QueryCounterHealth(counter, health) = query.at(0).unwrap(); 588 | assert_eq!(counter.0, 3); 589 | assert_eq!(health.0, 107); 590 | } 591 | 592 | #[test] 593 | fn test_selective_system_calls() { 594 | let mut world = World::default(); 595 | 596 | let entity_a = world.create(EntityA { 597 | counter: Counter(0), 598 | health: Health(100), 599 | tag_a: TagA, 600 | }); 601 | 602 | let entity_b = world.create(EntityB { 603 | counter: Counter(0), 604 | speed: Speed(1.0), 605 | tag_b: TagB, 606 | }); 607 | 608 | let entity_ab = world.create(EntityAB { 609 | counter: Counter(0), 610 | health: Health(100), 611 | speed: Speed(1.0), 612 | name: Name("both".to_string()), 613 | }); 614 | 615 | // Increment TagA entities 5 times 616 | for _ in 0..5 { 617 | world.increment_only_tag_a(); 618 | } 619 | 620 | // Increment TagB entities 3 times 621 | for _ in 0..3 { 622 | world.increment_only_tag_b(); 623 | } 624 | 625 | // Check entity A (has TagA) 626 | let query_a = world.with_query(Query::::new()); 627 | for QueryEntityCounterTagA(entity, counter, _) in query_a.iter() { 628 | if *entity == entity_a { 629 | assert_eq!(counter.0, 500); // 5 * 100 630 | } 631 | } 632 | 633 | // Check entity B (has TagB) 634 | let query_b = world.with_query(Query::::new()); 635 | for QueryEntityCounterTagB(entity, counter, _) in query_b.iter() { 636 | if *entity == entity_b { 637 | assert_eq!(counter.0, 600); // 3 * 200 638 | } 639 | } 640 | 641 | // EntityAB has neither TagA nor TagB, so should remain 0 642 | let query_ab = world.with_query(Query::::new()); 643 | for QueryEntityCounterName(entity, counter, _) in query_ab.iter() { 644 | if *entity == entity_ab { 645 | assert_eq!(counter.0, 0); 646 | } 647 | } 648 | } 649 | 650 | #[test] 651 | fn test_system_called_multiple_times_affects_only_target() { 652 | let mut world = World::default(); 653 | 654 | world.create(EntityA { 655 | counter: Counter(0), 656 | health: Health(100), 657 | tag_a: TagA, 658 | }); 659 | 660 | world.create(EntityB { 661 | counter: Counter(0), 662 | speed: Speed(1.0), 663 | tag_b: TagB, 664 | }); 665 | 666 | // Call system that only affects entities with Health component 667 | for _ in 0..5 { 668 | world.increment_with_health(); 669 | } 670 | 671 | // Check EntityA (has Health) - should be affected 672 | let query_a = world.with_query(Query::::new()); 673 | let QueryEntityCounterTagA(_, counter, _) = query_a.at(0).unwrap(); 674 | assert_eq!(counter.0, 5); 675 | 676 | // Check EntityB (no Health) - should not be affected 677 | let query_b = world.with_query(Query::::new()); 678 | let QueryEntityCounterTagB(_, counter, _) = query_b.at(0).unwrap(); 679 | assert_eq!(counter.0, 0); 680 | } 681 | 682 | #[test] 683 | fn test_entity_deletion_and_verification() { 684 | let mut world = World::default(); 685 | 686 | let entity_keep = world.create(EntityA { 687 | counter: Counter(100), 688 | health: Health(100), 689 | tag_a: TagA, 690 | }); 691 | 692 | let entity_delete = world.create(EntityA { 693 | counter: Counter(200), 694 | health: Health(50), 695 | tag_a: TagA, 696 | }); 697 | 698 | let entity_keep2 = world.create(EntityB { 699 | counter: Counter(300), 700 | speed: Speed(1.0), 701 | tag_b: TagB, 702 | }); 703 | 704 | // Verify we have 3 entities 705 | let query_all = world.with_query(Query::::new()); 706 | assert_eq!(query_all.len(), 3); 707 | 708 | // Delete the middle entity 709 | world.destroy(entity_delete); 710 | 711 | // Verify we now have 2 entities 712 | let query_all = world.with_query(Query::::new()); 713 | assert_eq!(query_all.len(), 2); 714 | 715 | // Verify the correct entities remain with correct values 716 | let query_a = world.with_query(Query::::new()); 717 | assert_eq!(query_a.len(), 1); 718 | let QueryEntityCounterTagA(entity, counter, _) = query_a.at(0).unwrap(); 719 | assert_eq!(*entity, entity_keep); 720 | assert_eq!(counter.0, 100); 721 | 722 | let query_b = world.with_query(Query::::new()); 723 | assert_eq!(query_b.len(), 1); 724 | let QueryEntityCounterTagB(entity, counter, _) = query_b.at(0).unwrap(); 725 | assert_eq!(*entity, entity_keep2); 726 | assert_eq!(counter.0, 300); 727 | } 728 | 729 | #[test] 730 | fn test_delete_multiple_entities_and_verify_correct_ones() { 731 | let mut world = World::default(); 732 | 733 | let e1 = world.create(EntityA { 734 | counter: Counter(1), 735 | health: Health(10), 736 | tag_a: TagA, 737 | }); 738 | 739 | let e2 = world.create(EntityA { 740 | counter: Counter(2), 741 | health: Health(20), 742 | tag_a: TagA, 743 | }); 744 | 745 | let e3 = world.create(EntityA { 746 | counter: Counter(3), 747 | health: Health(30), 748 | tag_a: TagA, 749 | }); 750 | 751 | let e4 = world.create(EntityA { 752 | counter: Counter(4), 753 | health: Health(40), 754 | tag_a: TagA, 755 | }); 756 | 757 | let e5 = world.create(EntityA { 758 | counter: Counter(5), 759 | health: Health(50), 760 | tag_a: TagA, 761 | }); 762 | 763 | // Delete e2 and e4 764 | world.destroy(e2); 765 | world.destroy(e4); 766 | 767 | // Verify correct count 768 | let query = world.with_query(Query::::new()); 769 | assert_eq!(query.len(), 3); 770 | 771 | // Verify correct entities remain 772 | let mut found = std::collections::HashSet::new(); 773 | for QueryEntityCounterHealth(entity, counter, health) in query.iter() { 774 | found.insert(*entity); 775 | 776 | match counter.0 { 777 | 1 => { 778 | assert_eq!(*entity, e1); 779 | assert_eq!(health.0, 10); 780 | } 781 | 3 => { 782 | assert_eq!(*entity, e3); 783 | assert_eq!(health.0, 30); 784 | } 785 | 5 => { 786 | assert_eq!(*entity, e5); 787 | assert_eq!(health.0, 50); 788 | } 789 | _ => panic!("Unexpected counter value: {}", counter.0), 790 | } 791 | } 792 | 793 | assert!(found.contains(&e1)); 794 | assert!(!found.contains(&e2)); 795 | assert!(found.contains(&e3)); 796 | assert!(!found.contains(&e4)); 797 | assert!(found.contains(&e5)); 798 | } 799 | 800 | #[test] 801 | fn test_combination_queries_resources_and_deletion() { 802 | let mut world = World::default(); 803 | 804 | // Create entities 805 | let e1 = world.create(EntityA { 806 | counter: Counter(0), 807 | health: Health(100), 808 | tag_a: TagA, 809 | }); 810 | 811 | let e2 = world.create(EntityB { 812 | counter: Counter(0), 813 | speed: Speed(2.0), 814 | tag_b: TagB, 815 | }); 816 | 817 | let e3 = world.create(EntityAB { 818 | counter: Counter(0), 819 | health: Health(50), 820 | speed: Speed(1.5), 821 | name: Name("hybrid".to_string()), 822 | }); 823 | 824 | // Apply systems with resources 825 | let tick = TickCount(2); 826 | world.system_with_ref_resource(&tick); 827 | 828 | // Apply selective system 829 | world.increment_only_tag_a(); 830 | 831 | // Check values before deletion 832 | let query = world.with_query(Query::::new()); 833 | for QueryEntityCounter(entity, counter) in query.iter() { 834 | if *entity == e1 { 835 | assert_eq!(counter.0, 102); // 0 + 2 (from system_with_ref_resource) + 100 (from increment_only_tag_a) 836 | } else if *entity == e2 { 837 | assert_eq!(counter.0, 0); // 0 + 0 (no Health, so not affected by system_with_ref_resource; no TagA) 838 | } else if *entity == e3 { 839 | assert_eq!(counter.0, 2); // 0 + 2 (from system_with_ref_resource; no TagA or TagB) 840 | } 841 | } 842 | 843 | // Delete e2 844 | world.destroy(e2); 845 | 846 | // Apply more updates 847 | world.system_with_value_resource(10); 848 | 849 | // Verify final state 850 | let query = world.with_query(Query::::new()); 851 | assert_eq!(query.len(), 2); 852 | 853 | for QueryEntityCounter(entity, counter) in query.iter() { 854 | if *entity == e1 { 855 | assert_eq!(counter.0, 112); // 102 + 10 (from system_with_value_resource) 856 | } else if *entity == e3 { 857 | assert_eq!(counter.0, 12); // 2 + 10 (from system_with_value_resource) 858 | } else { 859 | panic!("Unexpected entity found"); 860 | } 861 | } 862 | } 863 | 864 | #[test] 865 | fn test_system_and_for_each_equivalence() { 866 | let mut world1 = World::default(); 867 | let mut world2 = World::default(); 868 | 869 | // Create identical entities in both worlds 870 | for i in 0..5 { 871 | world1.create(EntityA { 872 | counter: Counter(i), 873 | health: Health(100), 874 | tag_a: TagA, 875 | }); 876 | world2.create(EntityA { 877 | counter: Counter(i), 878 | health: Health(100), 879 | tag_a: TagA, 880 | }); 881 | } 882 | 883 | // Apply system in world1 884 | world1.system_single_component_mutable(); 885 | 886 | // Apply for_each in world2 887 | world2.for_each_single_mutable(); 888 | 889 | // Verify results are the same 890 | let query1 = world1.with_query(Query::::new()); 891 | let query2 = world2.with_query(Query::::new()); 892 | 893 | assert_eq!(query1.len(), query2.len()); 894 | 895 | for i in 0..query1.len() { 896 | let QueryCounter(counter1) = query1.at(i).unwrap(); 897 | let QueryCounter(counter2) = query2.at(i).unwrap(); 898 | assert_eq!(counter1.0, counter2.0); 899 | assert_eq!(counter1.0, i + 1); 900 | } 901 | } 902 | --------------------------------------------------------------------------------