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.
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!