#relation #bevy #ecs #game

aery

Non-fragmenting ZST relations for Bevy

9 releases (5 breaking)

0.6.0 Feb 18, 2024
0.5.2 Dec 17, 2023
0.5.1 Nov 9, 2023
0.4.0 Oct 9, 2023
0.1.0 Jun 6, 2023

#181 in Game dev

Download history 15/week @ 2023-12-22 54/week @ 2023-12-29 26/week @ 2024-01-05 6/week @ 2024-01-12 1/week @ 2024-02-09 181/week @ 2024-02-16 72/week @ 2024-02-23 43/week @ 2024-03-01 37/week @ 2024-03-08 39/week @ 2024-03-15 8/week @ 2024-03-22 31/week @ 2024-03-29 10/week @ 2024-04-05

96 downloads per month
Used in 2 crates

MIT/Apache

135KB
3K SLoC

Aery

A plugin that adds a subset of Entity Relationship features to Bevy.

Crates.io Docs.rs

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.

// Modeling RPG mechanics that resemble TOTK:
// - Items interacting with enviornment climate
// - Powering connected devices
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;

#[derive(Relation)]
struct Inventory;

fn setup(mut cmds: Commands) {
    // Spawn character with some starting items.
    cmds.spawn((Character, Pos(Vec3::default())))
        .scope::<Inventory>(|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::<Inventory>(char);
    cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::<Inventory>(char);
    cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::<Inventory>(char);
    cmds.spawn((Food::Raw { freshness: 128. }, Apple)).set::<Inventory>(char);
}

fn tick_food(
    mut characters: Query<((&Character, &Pos), Relations<Inventory>)>,
    mut inventory_food: Query<&mut Food, Without<Pos>>,
    mut food: Query<(&mut Food, &Pos)>,
    climate_map: Res<ClimateMap>,
) {
    // 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::<Inventory>(&mut inventory_food).for_each(|mut food| {
            food.tick(climate);
        });
    }
}

fn drop_item_from_inventory(
    mut commands: Commands,
    mut events: EventReader<TargetEvent>,
    characters: Query<&Pos, With<Character>>,
    food: Query<Entity, With<Food>>,
) {
    // Set an items position to the position of the character that last had the item
    // in their inventory when they drop it.
    for event in events
        .iter()
        .filter(|event| event.matches(Wc, Op::Unset, Inventory, Wc))
    {
        let Ok(pos) = characters.get(event.target) else { return };
        commands.entity(event.host).insert(*pos);
    }

}

#[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<FuseJoint>)>,
    mut fans: Query<(Entity, &Fan, &mut Pos), With<Powered>>,
) {
    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::<FuseJoint>([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
            }
        });
    }
}

Version table

Bevy version Aery verison
0.13 0.6
0.12 0.5
0.11 0.3 - 0.4
0.10 0.1 - 0.2

Credits

  • Sander Mertens: Responsible for pioneering Entity Relationships in ECS and the author of Flecs which Aery has taken a lot of inspiration from.

Dependencies

~12–17MB
~315K SLoC