├── examples ├── README.md ├── simple.rs ├── cleanup.rs └── in_range.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── .devcontainer └── devcontainer.json ├── src ├── operations │ ├── utils.rs │ └── mod.rs ├── relation.rs ├── scope.rs ├── lib.rs ├── tuple_traits.rs ├── for_each.rs └── edges.rs ├── README.md └── LICENSE-APACHE /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Run with: 4 | 5 | ```sh 6 | cargo run --example simple 7 | ``` 8 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aery_macros" 3 | description = "Proc macros for Aery." 4 | version = "0.3.0-dev" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | syn = "2.0" 13 | quote = "1.0" 14 | proc-macro2 = "1.0" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | *target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aery" 3 | version = "0.8.1" 4 | edition = "2021" 5 | authors = ["iiYese iiyese@outlook.com"] 6 | repository = "https://github.com/iiYese/aery" 7 | description = "Non-fragmenting ZST relations for Bevy." 8 | keywords = ["bevy", "relations", "game", "ecs"] 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | bevy_app = "0.15" 16 | bevy_derive = "0.15" 17 | bevy_ecs = "0.15" 18 | bevy_hierarchy = "0.15" 19 | bevy_reflect = "0.15" 20 | bevy_log = "0.15" 21 | bevy_utils = "0.15" 22 | smallvec = "1.11.0" 23 | aery_macros = { path = "macros", version = "0.3.0-dev" } 24 | aquamarine = "0.3.2" 25 | 26 | [dev-dependencies] 27 | bevy = "0.15" 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 iiYese 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 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use aery::prelude::*; 2 | use bevy::{app::AppExit, prelude::*}; 3 | 4 | #[derive(Relation)] 5 | struct ChildOf; 6 | 7 | #[derive(Component)] 8 | struct Name(&'static str); 9 | 10 | fn setup(mut cmds: Commands) { 11 | cmds.spawn(Name("Alice")).scope::(|chld| { 12 | chld.add(Name("Jack")) 13 | .scope::(|chld| { 14 | chld.add(Name("Dave")); 15 | }) 16 | .add(Name("Jill")) 17 | .scope::(|chld| { 18 | chld.add(Name("Stephen")).add(Name("Sam")); 19 | }); 20 | }); 21 | 22 | cmds.spawn(Name("Loyd")).scope::(|chld| { 23 | chld.add(Name("Anya")); 24 | }); 25 | } 26 | 27 | fn display_children(tree: Query<(&Name, Relations)>, roots: Query>) { 28 | tree.traverse::(roots.iter()) 29 | .track_self() 30 | .for_each(|Name(parent), _, Name(child), _| { 31 | println!("{} is the parent of {}", parent, child); 32 | }); 33 | } 34 | 35 | fn exit(mut exit: EventWriter) { 36 | exit.send(AppExit::Success); 37 | } 38 | 39 | fn main() { 40 | App::new() 41 | .add_systems(Startup, setup) 42 | .add_systems(Update, (display_children, exit).chain()) 43 | .run(); 44 | } 45 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:1", 7 | 8 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 9 | // "mounts": [ 10 | // { 11 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 12 | // "target": "/usr/local/cargo", 13 | // "type": "volume" 14 | // } 15 | // ] 16 | 17 | // Features to add to the dev container. More info: https://containers.dev/features. 18 | "features": { 19 | "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { 20 | "packages": "libasound2-dev,libudev-dev" 21 | } 22 | } 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | 27 | // Use 'postCreateCommand' to run commands after the container is created. 28 | // "postCreateCommand": "rustc --version", 29 | 30 | // Configure tool-specific properties. 31 | // "customizations": {}, 32 | 33 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 34 | // "remoteUser": "root" 35 | } 36 | -------------------------------------------------------------------------------- /examples/cleanup.rs: -------------------------------------------------------------------------------- 1 | use aery::prelude::*; 2 | use bevy::{prelude::*, window::PrimaryWindow}; 3 | 4 | const WIN_SIZE: Vec2 = Vec2::new(800., 600.); 5 | 6 | #[derive(Component)] 7 | struct Pos(Vec2); 8 | 9 | #[derive(Relation)] 10 | // By default all relations are orhpaning. 11 | // Supplying one of the other cleanup policies to the `aery` attribute will override that. 12 | // Click on any of the nodes in this example to delete them. 13 | // Change the policy and rerun the example to see the different behaviors. 14 | #[aery(Recursive)] 15 | struct ChildOf; 16 | 17 | fn setup(mut cmds: Commands) { 18 | cmds.spawn(Camera2d); 19 | 20 | cmds.spawn(Pos(Vec2::new(0.0, 150.0))) 21 | .scope::(|chld| { 22 | chld.add(Pos(Vec2::new(-150., 0.))) 23 | .scope::(|chld| { 24 | chld.add(Pos(Vec2::new(-300., -150.))) 25 | .add(Pos(Vec2::new(-100., -150.))); 26 | }) 27 | .add(Pos(Vec2::new(150., 0.))) 28 | .scope::(|chld| { 29 | chld.add(Pos(Vec2::new(100., -150.))) 30 | .add(Pos(Vec2::new(300., -150.))); 31 | }); 32 | }); 33 | } 34 | 35 | fn draw(mut gizmos: Gizmos, tree: Query<&Pos>) { 36 | for Pos(pos) in tree.iter() { 37 | gizmos.circle_2d(*pos, 40., Color::WHITE); 38 | } 39 | } 40 | 41 | fn input( 42 | mut cmds: Commands, 43 | mouse_buttons: Res>, 44 | windows: Query<&Window, With>, 45 | nodes: Query<(Entity, &Pos)>, 46 | ) { 47 | let Some(cursor_pos) = windows 48 | .get_single() 49 | .ok() 50 | .and_then(Window::cursor_position) 51 | .filter(|_| mouse_buttons.just_pressed(MouseButton::Left)) 52 | .map(|pos| pos - WIN_SIZE / 2.) 53 | .map(|pos| Vec2::new(pos.x, -pos.y)) 54 | else { 55 | return; 56 | }; 57 | 58 | if let Some((e, _)) = nodes 59 | .iter() 60 | .find(|(_, Pos(pos))| (cursor_pos - *pos).length() < 40.) 61 | { 62 | cmds.entity(e).despawn(); 63 | } 64 | } 65 | 66 | fn main() { 67 | App::new() 68 | .add_plugins(DefaultPlugins.set(WindowPlugin { 69 | primary_window: Some(Window { 70 | resolution: WIN_SIZE.into(), 71 | ..default() 72 | }), 73 | ..default() 74 | })) 75 | .add_systems(Startup, setup) 76 | .add_systems(Update, (input, draw)) 77 | .run(); 78 | } 79 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::Span; 3 | use quote::quote; 4 | use syn::{ 5 | parse_macro_input, parse_quote, punctuated::Punctuated, DeriveInput, Error, Ident, Meta, 6 | Result, Token, 7 | }; 8 | 9 | struct RelationConfig { 10 | policy: Ident, 11 | exclusive: bool, 12 | symmetric: bool, 13 | } 14 | 15 | fn parse_config(ast: &DeriveInput) -> Result { 16 | let mut policy = "Orphan"; 17 | let mut exclusive = true; 18 | let mut symmetric = false; 19 | 20 | for attr in ast.attrs.iter().filter(|attr| attr.path().is_ident("aery")) { 21 | let nested = attr.parse_args_with(Punctuated::::parse_terminated)?; 22 | for meta in nested { 23 | match meta { 24 | Meta::Path(ref path) => { 25 | if let Some(new_policy) = ["Counted", "Recursive", "Total"] 26 | .into_iter() 27 | .find(|ident| path.is_ident(ident)) 28 | { 29 | if policy != "Orphan" { 30 | return Err(Error::new_spanned( 31 | meta, 32 | "Tried to set policy multiple times", 33 | )); 34 | } 35 | policy = new_policy; 36 | } else if path.is_ident("Poly") { 37 | if !exclusive { 38 | return Err(Error::new_spanned( 39 | meta, 40 | "Tried to set exclusivity multiple times", 41 | )); 42 | } 43 | exclusive = false; 44 | } else if path.is_ident("Symmetric") { 45 | if symmetric { 46 | return Err(Error::new_spanned( 47 | meta, 48 | "Tried to set symmetry multiple times", 49 | )); 50 | } 51 | symmetric = true; 52 | } else { 53 | return Err(Error::new_spanned(meta, "Unrecognized property override")); 54 | } 55 | } 56 | _ => { 57 | return Err(Error::new_spanned(meta, "Unrecognized macro format")); 58 | } 59 | } 60 | } 61 | } 62 | 63 | Ok(RelationConfig { 64 | policy: Ident::new(policy, Span::call_site()), 65 | exclusive, 66 | symmetric, 67 | }) 68 | } 69 | 70 | #[proc_macro_derive(Relation, attributes(aery))] 71 | pub fn relation_derive(input: TokenStream) -> TokenStream { 72 | let mut ast = parse_macro_input!(input as DeriveInput); 73 | 74 | let RelationConfig { 75 | policy, 76 | exclusive, 77 | symmetric, 78 | } = match parse_config(&ast) { 79 | Ok(config) => config, 80 | Err(e) => return e.into_compile_error().into(), 81 | }; 82 | 83 | ast.generics 84 | .make_where_clause() 85 | .predicates 86 | .push(parse_quote! { Self: Sized + Send + Sync + 'static }); 87 | 88 | let struct_name = &ast.ident; 89 | let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); 90 | 91 | let output = quote! { 92 | impl #impl_generics Relation for #struct_name #type_generics #where_clause { 93 | const CLEANUP_POLICY: CleanupPolicy = CleanupPolicy::#policy; 94 | const EXCLUSIVE: bool = #exclusive; 95 | const SYMMETRIC: bool = #symmetric; 96 | } 97 | }; 98 | 99 | output.into() 100 | } 101 | -------------------------------------------------------------------------------- /src/operations/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | edges::EdgeIter, 3 | relation::{Hierarchy, Relation, RelationId}, 4 | tuple_traits::*, 5 | }; 6 | 7 | use bevy_ecs::{entity::Entity, query::QueryData}; 8 | 9 | use std::marker::PhantomData; 10 | 11 | /// Struct to track metadat for a join operation. 12 | pub struct JoinWith { 13 | pub(crate) relations: Relations, 14 | pub(crate) edges: PhantomData, 15 | pub(crate) items: JoinItems, 16 | } 17 | 18 | #[allow(missing_docs)] 19 | pub struct SelfTracking; 20 | 21 | /// Struct to track metadat for a traversal operation. 22 | pub struct TraverseAnd { 23 | pub(crate) control: Control, 24 | pub(crate) edge: PhantomData, 25 | pub(crate) starts: Starts, 26 | pub(crate) track: Tracked, 27 | pub(crate) init: Init, 28 | pub(crate) fold: Fold, 29 | } 30 | 31 | /// [`WorldQuery`] type to query for Relation types. Takes a [`RelationSet`] which is a single 32 | /// relation or tuple of relation types. *Must appear in the second position of the outer most tuple 33 | /// to use relation operations and no type may appear more than once for operations to work.* 34 | /// 35 | /// [`RelationSet`]: crate::tuple_traits::RelationSet 36 | #[derive(QueryData)] 37 | pub struct Relations { 38 | pub(crate) edges: RS::Edges, 39 | _phantom: PhantomData, 40 | } 41 | 42 | #[allow(missing_docs)] 43 | pub struct EdgeProduct<'a, const N: usize> { 44 | pub(crate) base_iterators: [EdgeIter<'a>; N], 45 | pub(crate) live_iterators: [EdgeIter<'a>; N], 46 | pub(crate) entities: [Option; N], 47 | } 48 | 49 | impl<'a, const N: usize> EdgeProduct<'a, N> { 50 | pub(crate) fn advance(&mut self, prev_matches: [bool; N]) -> Option<[Entity; N]> { 51 | let n = prev_matches 52 | .iter() 53 | .enumerate() 54 | .find_map(|(n, matches)| (!matches).then_some(n)) 55 | .unwrap_or(N); 56 | 57 | for i in (1..N).skip(n) { 58 | self.live_iterators[i] = self.base_iterators[i].clone(); 59 | self.entities[i] = self.live_iterators[i].next(); 60 | } 61 | 62 | 'next_permutation: { 63 | for i in (1..N).take(n).rev() { 64 | if let Some(entity) = self.live_iterators[i].next() { 65 | self.entities[i] = Some(entity); 66 | break 'next_permutation; 67 | } else { 68 | self.live_iterators[i] = self.base_iterators[i].clone(); 69 | self.entities[i] = self.live_iterators[i].next(); 70 | } 71 | } 72 | 73 | self.entities[0] = self.live_iterators[0].next(); 74 | } 75 | 76 | self.entities 77 | .iter() 78 | .all(Option::is_some) 79 | .then(|| self.entities.map(Option::unwrap)) 80 | } 81 | } 82 | 83 | /// Flip the direction of a [`Join`] or [`Traverse`] operation to use targets instead of hosts. 84 | /// 85 | /// [`Join`]: crate::operations::Join 86 | /// [`Traverse`]: crate::operations::Traverse 87 | pub struct Up(PhantomData); 88 | 89 | #[allow(missing_docs)] 90 | pub trait EdgeSide { 91 | fn entities<'i, 'r, RS>(relations: &'r RelationsItem<'i, RS>) -> EdgeIter<'r> 92 | where 93 | 'i: 'r, 94 | RS: RelationSet, 95 | RelationsItem<'i, RS>: RelationEntries; 96 | } 97 | 98 | impl EdgeSide for Hierarchy { 99 | fn entities<'i, 'r, RS>(relations: &'r RelationsItem<'i, RS>) -> EdgeIter<'r> 100 | where 101 | 'i: 'r, 102 | RS: RelationSet, 103 | RelationsItem<'i, RS>: RelationEntries, 104 | { 105 | relations.hosts(Hierarchy).iter().copied() 106 | } 107 | } 108 | 109 | impl EdgeSide for Up { 110 | fn entities<'i, 'r, RS>(relations: &'r RelationsItem<'i, RS>) -> EdgeIter<'r> 111 | where 112 | 'i: 'r, 113 | RS: RelationSet, 114 | RelationsItem<'i, RS>: RelationEntries, 115 | { 116 | relations.targets(Hierarchy).iter().copied() 117 | } 118 | } 119 | 120 | impl EdgeSide for R { 121 | fn entities<'i, 'r, RS>(relations: &'r RelationsItem<'i, RS>) -> EdgeIter<'r> 122 | where 123 | 'i: 'r, 124 | RS: RelationSet, 125 | RelationsItem<'i, RS>: RelationEntries, 126 | { 127 | relations.hosts(RelationId::of::()).iter().copied() 128 | } 129 | } 130 | 131 | impl EdgeSide for Up { 132 | fn entities<'i, 'r, RS>(relations: &'r RelationsItem<'i, RS>) -> EdgeIter<'r> 133 | where 134 | 'i: 'r, 135 | RS: RelationSet, 136 | RelationsItem<'i, RS>: RelationEntries, 137 | { 138 | relations.targets(RelationId::of::()).iter().copied() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /examples/in_range.rs: -------------------------------------------------------------------------------- 1 | use aery::prelude::Up; 2 | use aery::prelude::*; 3 | use bevy::log; 4 | use bevy::prelude::*; 5 | 6 | #[derive(Component)] 7 | struct MovingEntity { 8 | direction: f32, // Movement direction along the x-axis, positive or negative. 9 | } 10 | 11 | // Define a relationship where multiple entities can be linked non-exclusively. 12 | // Symmetry is not set in this example, it may arise from the implementation, 13 | // when we set Poly relation from both sides. 14 | #[derive(Relation)] 15 | #[aery(Poly)] 16 | struct InRange; 17 | const RANGE_THRESHOLD: f32 = 100.0; 18 | 19 | fn setup(mut commands: Commands) { 20 | // Spawning two moving entities with initial positions and directions. 21 | commands.spawn(( 22 | Name::new("Alice"), 23 | MovingEntity { direction: 1.0 }, 24 | Transform::from_xyz(-150.0, 0.0, 0.0), 25 | GlobalTransform::default(), 26 | )); 27 | commands.spawn(( 28 | Name::new("Bob"), 29 | MovingEntity { direction: -1.0 }, 30 | Transform::from_xyz(150.0, 0.0, 0.0), 31 | GlobalTransform::default(), 32 | )); 33 | } 34 | 35 | fn move_entities(mut query: Query<(&mut Transform, &mut MovingEntity)>, time: Res