## Aery
A plugin that adds a subset of Entity Relationship features to Bevy.
[![Crates.io](https://img.shields.io/crates/v/aery)](https://crates.io/crates/aery)
[![Docs.rs](https://img.shields.io/docsrs/aery)](https://docs.rs/aery/latest/aery/)
### Currently supported:
- ZST edge types only (simply means edges can't hold data)
- Fragmenting on edge types
- Cleanup policies
- Declarative APIs for:
- Joining
- Traversing
- Spawning
# API tour:
Non exhaustive. Covers most common parts. It's modeling RPG mechanics resembling tears of the
kingdom (please nintendo leave me alone I beg).
Boilerplate
```rust
use bevy::prelude::*;
use aery::prelude::*;
#[derive(Clone, Copy, Component)]
struct Pos(Vec3);
#[derive(Component)]
struct Character;
#[derive(Component)]
struct Weapon {
uses: u32,
strength: u32,
}
#[derive(Component)]
struct Stick;
#[derive(Clone, Copy)]
enum Climate {
Freezing,
Cold,
Neutral,
Hot,
Blazing,
}
#[derive(Resource)]
struct ClimateMap {
// ..
}
impl ClimateMap {
fn climate_at(&self, pos: Pos) -> Climate {
todo!()
}
}
#[derive(Component)]
enum Food {
Raw { freshness: f32 },
Cooked,
Spoiled,
}
impl Food {
fn tick(&mut self, climate: Climate) {
let Food::Raw { freshness } = self else { return };
if *freshness < 0. {
*self = Food::Spoiled;
return
}
match climate {
Climate::Neutral => *freshness -= 1., // spoils over time
Climate::Cold => *freshness -= 0.1, // spoils slowly
Climate::Freezing => *freshness -= 0.01, // spoils very slowly
Climate::Hot => *freshness -= 5., // spoils quickly
Climate::Blazing => *self = Food::Cooked, // Cooks food (should add a timer)
}
}
}
#[derive(Component)]
struct Apple;
```
Modeling a player inventory (Making relations)
```rust
#[derive(Relation)]
struct Inventory;
fn setup(mut cmds: Commands) {
// Spawn character with some starting items.
cmds.spawn((Character, Pos(Vec3::default())))
.scope::(|invt| {
// Give them a starting weapon & 3 food items
invt.add((Weapon { uses: 32, strength: 4 }, Stick))
.add((Food::Raw { freshness: 128. }, Apple))
.add((Food::Raw { freshness: 128. }, Apple))
.add((Food::Raw { freshness: 128. }, Apple));
});
// Alternatively construct relatiosn manually.
// This might be more appropriate for changing an inventory or making more complex graphs.
let char = cmds.spawn((Character, Pos(Vec3::default()))).id();
cmds.spawn((Weapon { uses: 32, strength: 4, }, Stick)).set::(char);
cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::(char);
cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::(char);
cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::(char);
}
```
Making items respond to enviornment (Join operations)
```rust
fn tick_food(
mut characters: Query<((&Character, &Pos), Relations)>,
mut inventory_food: Query<&mut Food, Without>,
mut food: Query<(&mut Food, &Pos)>,
climate_map: Res,
) {
// Tick foods that are just in the world somewhere
for (mut food, pos) in food.iter_mut() {
food.tick(climate_map.climate_at(*pos));
}
// Tick foods that are in a character's inventory based on the character's position
for ((_, pos), edges) in characters.iter() {
let climate = climate_map.climate_at(*pos);
edges.join::(&mut inventory_food).for_each(|mut food| {
food.tick(climate);
});
}
}
```
Dropping inventory items into the world (Responding to relation changes)
```rust
fn drop_item_from_inventory(
trigger: Trigger>,
mut commands: Commands,
characters: Query<&Pos, With>,
food: Query>,
) {
// Set an items position to the position of the character that last had the item
// in their inventory when they drop it.
let Ok(pos) = characters.get(trigger.event().target) else { return };
commands.entity(trigger.entity()).insert(*pos);
}
```
Powering connected devices (Traversing relations & relation properties)
```rust
// This relation has a custom property. Properties can be overriden by supplying arguments to
// the attribute macro. See the `Relation` trait & `CleanupPolicy` enum for more details.
// - Symmetric: Makes relations symmetric. Setting A -R-> B also sets B -R-> A.
// - Poly: Allows holding multiple relations of that type to different entities.
//
// There are also cleanup properties. Only one of these can be supplied to the attribute macro.
// - Counted: Edge counted cleanup (eg. despawn a parent if all its children are despawned)
// - Recursive: Recursively cleans up (eg. despawn all children of a parent with the parent)
// - Total: Does both counted & recursive cleanup
#[derive(Relation)]
#[aery(Symmetric, Poly)]
struct FuseJoint;
#[derive(Component)]
struct Fan {
orientation: Quat
}
#[derive(Component)]
struct Powered;
fn tick_devices(
mut devices: Query<((Entity, &mut Pos), Relations)>,
mut fans: Query<(Entity, &Fan, &mut Pos), With>,
) {
for (entity, fan, pos) in fans.iter_mut() {
// Move the fan based on its orientation
pos = todo!();
// Track visited nodes because this is a symmetric relationship
let mut updated = vec![entity];
devices.traverse_mut::([entity]).for_each(|(entity, ref mut pos), _| {
if updated.contains(&entity) {
TCF::Close
} else {
// Move connected device based on fan direction
pos = todo!();
updated.push(*entity);
TCF::Continue
}
});
}
}
```
Reflecting relations
```rust
App::new()
// We just need to register the types for relfection.
.register_relation::()
.register_relation::()
// ..
.run();
```
### Version table
| Bevy version | Aery verison |
|--------------|--------------|
| 0.15 | 0.8 |
| 0.14 | 0.7 |
| 0.13 | 0.6 |
| 0.12 | 0.5 |
| 0.11 | 0.3 - 0.4 |
| 0.10 | 0.1 - 0.2 |
### Credits
- [Sander Mertens](https://github.com/SanderMertens):
Responsible for pioneering Entity Relationships in ECS and the author of Flecs which Aery has taken
a lot of inspiration from.