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

The Bundles to ease the similar-entities creation

Inserting component dynamically into our entities is fine to spawn entities that need to be spawn only once, but it becomes verbose and source of mistake if you need to create multiple Player in different parts of your code.

Plus, some similar-entities, such as NPC & Mob could have the same components a Player has. In such case, we use Bundles.

To introduce the bundles, let me recall you what Entity and Component stand for:

  • A component is a defined type of data stored into an entity.
  • An entity is made of one or many components and some methods.

-- from Archetype, what a complex word for nothing!

Between the two, comes the bundle which could be defined as a re-usable set of components to ease the creation of similar entities.

Bundle as a tuple

To illustrate the bundle, let's create what looks the most to a bundle: a simple tuple of components:

struct Life(u8);
struct Name(String);
struct Position {
    x: u16,
    y: u16
}

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

fn add_player(mut commands: Commands) {
    commands.spawn()
            // Insert a Player
            .insert(Player)

            // Insert a tuple of components as a bundle
            .insert_bundle((
                Name("JunDue".to_string()),
                Position { x: 5, y: 2 },
                Life(52)
            ));
}

In that case, this is not so useful, we could have even pass Player inside the tuple of insert_bundle, the result would have be the same.
The use case for that lives in the Query: at some point of our game, we could add many stuff at once to our Player. Let's give a Sword, a Shield and a Helmet to our player:

struct Helmet;
struct Sword;
struct Shield;

fn give_stuff_to_players(mut commands: Commands, query: Query<(Entity, &Player)>) {
    let (entity, player) = query.single().expect("Error");
    commands.entity(entity)
        .insert_bundle((
            Helmet,
            Sword,
            Shield
        ));
}

Bundle-it!

Because we may use Life, Name and Position often together, let's define a CharacterBundle, like so:

struct Life(u8);
struct Name(String);
struct Position {
    x: u16,
    y: u16
}

#[derive(Bundle)]
struct CharacterBundle {
    name: Name,
    position: Position,
    life: Life
}

⚠️ #[derive(Bundle)] is the most important part of this code. Without that, our struct would be a basic component (like Life, Name and Position are).

Now to use our bundle, we could use insert_bundle() method to add it dynamically to any Entity we want:

struct Player;
struct NPC;
struct Mob;

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

fn add_player(mut commands: Commands) {
    commands.spawn()
            // Insert a Player
            .insert(Player)
            // Insert the CharacterBundle
            .insert_bundle( CharacterBundle {
                name: Name("JunDue".to_string()),
                position: Position { x: 5, y: 2 },
                life: Life(52)
            });
}

We did not create a PlayerBundle directly as we will re-use the CharacterBundle to ease the Mob and NPC creation later.

Nested bundles

We can nest bundle into another to inject the CharacterBundle directly into our Player, Mob and NPC struct.
To demonstrate a possible use case, let's add some difference between each of them:

// Using `#[derive(Bundle)]` on `Player`, `NPC` and `Mob`
// we are telling to Bevy that they are now bundles.

#[derive(Bundle)]
struct Player {
    race: Race,

    // The `#[bundle]` property here is also mandatory
    // to mention that CharacterBundle is a nested bundle of Player bundle

    #[bundle]
    character: CharacterBundle
}

#[derive(Bundle)]
struct NPC {
    can_talk: Bool,

    #[bundle]
    character: CharacterBundle
}

#[derive(Bundle)]
struct Mob {
    agressiveness: u8

    #[bundle]
    character: CharacterBundle
}

Now our Player is a bundle. It shares the CharacterBundle components with NPC and Mob, but has a custom component Race.
We can now use insert_bundle method directly on a void entity or use the spawn_bundle method:

fn main() {
    App::build()
        .add_startup_system(add_player.system())
        .add_system(display_player.system())
        .run();
}

fn add_player(mut commands: Commands) {

    // Insert bundle into a void entity
    commands.spawn()
            .insert_bundle(Player {
                race: Race::ORC,
                character: CharacterBundle {
                    name: Name("JunDue".to_string()),
                    position: Position { x: 5, y: 2 },
                    life: Life(52)
                }
            });

    // Directly spawn from the bundle
    commands.spawn_bundle(Player {
                race: Race::ORC,
                character: CharacterBundle {
                    name: Name("JunDue".to_string()),
                    position: Position { x: 5, y: 2 },
                    life: Life(52)
                }
            });
}

The importance of the Bundle trait

If we forget the Bundle trait, we won't be able to use insert_bundle or spawn_bundle methods. We could add it as a component using insert, but we won't be able to query directly for the Name or Race of the player since they would just be kind of "nested-component" of the character component:

// This is now just a component
struct CharacterComponent {
    name: Name,
    position: Position,
    life: Life
}

fn add_player(mut commands: Commands) {
    commands
            .spawn()
            .insert(Player)
            // We now try to add the character **component** using insert()
            .insert(CharacterComponent {
                name: Name("JunDue".to_string()),
                race: Race::ORC,
                position: { x: 6, y: 12 }
            });
}

fn display_player(query: Query<(&Name, &Race),With<Player>>) {
     // This will fail because no Player
     // will be found with a Name and Race
     // Our Player only holds CharacterComponent

    let (name, race) = query.single().expect("Error");
    println!("{} is an {:?}", name.0, race);
}

Even if it's not what we want because we can't use Query directly on Race and Name, I'll show you how to access them, jut to understand what happend.
Because the character is now a basic component, you could have access to the name and race using this syntax as they are just "nested-component" of character:

fn display_player(query: Query<&CharacterComponent,With<Player>>) {
    let character = query.single().expect("Error");
    println!("{} is an {:?}", character.nickname.0, character.race);
}

⚠️ At the opposite, because both bundles and components are based on struct, the insert() method can't know if you are trying to add a component or a bundle.
Be careful to use insert_bundle() to insert a bundle, and insert() to insert a component.

Batched bundles

spawn_batch can be used on a bundle to create many entities at once, the following code will create 10 clones of JunDue:

fn create_player_clones(mut commands: Commands) {
    commands.spawn_batch(
        (0..10).map(|_| Player {
            character: CharacterBundle {
                name: Name("JunDue".to_string()),
                position: Position { x: 5, y: 2 },
                life: Life(52)
            }
        }));
}

Bundle as children

Using with_children method, you can pass a closure to spawn one or multiple children bundle to an entity:

struct Item {
    name: String,
    quantity: u8
}

fn add_player_with_items(mut commands: Commands) {
    commands.spawn()
        .insert_bundle(Player {
            race: Race::ORC,
            character: CharacterBundle {
                name: Name("JunDue".to_string()),
                position: Position { x: 5, y: 2 },
                life: Life(52)
            }
        }).with_children(|parent| {
            parent
                .spawn_bundle(Item {
                    name: "Helmet".to_string(),
                    quantity: 1
                });

            parent.spawn_bundle(Item {
                name: "Banana".to_string(),
                quantity: 7
            });
        });
}

Outro

Bundles are useful in many cases, all the methods that use bundle are not covered here because some are covered in the previous article covering entities spawn methods, most of them can be used with bundle the same way you could have use them with basic components.

If I missed something you would like to read here, feel free to comment this post!

Also during my research of informations for this post, I searched the most convenient way to spawn many children bundle at once, the same way we could use spawn_batch but as children, only passing a range of bundle.
I did not found any convenient way to produce something like this:

fn add_player_with_tree_children(mut commands: Commands) {
    commands.spawn()
        .insert_bundle(Player {
            race: Race::ORC,
            character: CharacterBundle {
                name: Name("JunDue".to_string()),
                position: Position { x: 5, y: 2 },
                life: Life(52)
            }
        })

        //!\\ Do not try to compile this code,
        //!\\ this methods doesn't exist out of my head
        .spawn_batch_children((0..3).map(|_| Player {
            character: CharacterBundle {
                name: Name("JunDue's child".to_string()),
                position: Position { x: 5, y: 1 },
                life: Life(12)
            }
        }));
}

Comments

Be the first to post a comment!

Add a comment

Preview