An in-depth analyze of entity composition in Bevy 1/2

Hi! Today I wanted to have a deep analyze about the entity composition into Bevy, we'll see that it exist many various ways to create a entity full of components.

At the beginning of this post, I wanted to write exclusively about bundle, but the my researches made me discover a lot about the entity composition that I wanted to share with you.

Let's create entity, from scratch and increase the complexity.

An entity can just contains a component marker or even be void!

To spawn an entity into Bevy, we don't need much code. This is a valid one:

fn main() {
    App::build()
        .add_startup_system(spawn_void_entity.system())
        .run();
}

fn spawn_void_entity(mut commands: Commands) {
    commands.spawn();
}

Here we just spawned an entity, this code is valid, it create an entity into the ECS.
It's not so useful as is but I told you that I would start from scratch so this is the most every minimalist code to spawn an entity.

The most minimalist real life use case would be to add a component marker to it so it become retrievable later from a Query:

struct Player;

fn main() {
    App::build()
        .add_startup_system(spawn_player.system())
        .run();
}

fn spawn_player(mut commands: Commands) {
    commands.spawn().insert(Player);
}

// ... Later during the game, use the marker to retrieve the Players
fn do_something_with_players(query: Query<(Entity, &Player)>) {
    // Asking for Entity parameter in a Query returns the id of the entity.
    // This is needed to modify a specific entity

    for entity in query.iter() {
        // Do whatever we need with the Players entities
    }
}

Dynamically compose an entity, add and remove component

The components which compose an entity can dynamically be added after the spawn method to compose our complete Player entity:

enum Race { ORC, ELF, HUMAN }
struct Pseudo(String);

struct Player;

fn main() {
    App::build()
        .add_startup_system(spawn_dynamic_player.system())
        .run();
}

fn spawn_dynamic_player(mut commands: Commands) {
    commands.spawn()
            .insert(Player)
            .insert(Pseudo("JunDue"))
            .insert(Race::ORC);
}

The dynamic method is not the most useful in that case as we'll discuss later about the Bundle trait, but it becomes really powerfull when we really need to add or remove some dynamic stuff.

Imagine that all the Players which have ORC as race can suddenly become an enemy, ELFs are neutrals and Humans are allies, and at some point of the game all become friends again 💑, we could add/remove a dynamic marker to it later using a query:

enum Race { ORC, ELF, HUMAN }
enum Friendliness { ENEMY, NEUTRAL, ALLY }

struct Pseudo(String);

struct Player;

fn main() {
    App::build()
        .add_startup_system(add_player.system())
        .add_system(apply_friendliness.system())
        .add_system_to_stage(CoreStage::PostUpdate, display_enemies.system())
        .add_system_to_stage(CoreStage::Last, all_are_friends.system())
        .run();
}

fn spawn_dynamic_player(mut commands: Commands) {
    commands.spawn()
            .insert(Player)
            .insert(Pseudo("JunDue"))
            .insert(Race::ORC);
}

fn apply_friendliness(mut commands: Commands, query: Query<(Entity, &Player)>) {
    for (entity, player) in query.iter() {
        match player.race {
            Race::ORC => commands.entity(entity).insert(Enemy),
            Race::ELF => commands.entity(entity).insert(Neutral),
            Race::HUMAN => commands.entity(entity).insert(Ally)
        };
    }
}

fn all_are_friends(mut commands: Commands, query: Query<Entity,(With<Player>, Without<Ally>)>) {
    for entity in query.iter() {
        commands.entity(entity)
                .insert(Ally)
                .remove::<Neutral>()
                .remove::<Enemy>();
    }
}

Entity hierarchy

An entity can be defined as the parent of one or many others, doing so we create an entity hierarchy, imagine our Player can hold Items

struct Item {
    name: String,
    quantity: u8
}

struct Player;

fn main() {
    App::build()
        .add_startup_system(add_player_with_items.system())
        .run();
}

fn add_player_with_items(mut commands: Commands) {
    // Spawn a helmet
    let helmet = commands.spawn().insert(Item {
        name: "Helmet".to_string(),
        quantity: 1
    }).id(); // << id() returns the Entity

    // Spawn some 🍌
    let bananas = commands.spawn().insert(Item {
        name: "Banana".to_string(),
        quantity: 7
    }).id(); // << id() returns the Entity

    commands.spawn()
        .insert(Player)
        .push_children(&[helmet,bananas]); // Link items to the Player
}

Despawn an entity, and optionally all its children

Sometimes... Actually often, we'll need to despawn some entity: when a player or a mob dies, when a tree is cut, and so on...
To do so, the most convenient way is to use the despawn method inside a Query

fn kill_players(mut commands: Commands, query: Query<(Entity,&Life),With<Player>>) {
    for (entity, life) in query.iter() {
        if life <= 0 {
            commands.entity(entity).despawn();
        }
    }
}

In many case (not all), keeping the children of an entity does not make sense, so we can despawn the entity AND all its children using despawn_recursive:

fn kill_players_and_drop_items(mut commands: Commands, query: Query<(Entity,&Life),With<Player>>) {
    for (entity, life) in query.iter() {
        if life <= 0 {
            commands.entity(entity).despawn_recursive();
        }
    }
}

We have seen here the bases of the Bevy's API about the entities composition, on the next part, we'll see our to ease the creation of entities by using re-usable bundles.

Comments

Be the first to post a comment!

Add a comment

Preview