Archetype, what a complex word for nothing!

A word that quickly comes when you are developing with Bevy is Archetype.
The first time I read this word, I was like:

Wow, that must be an high-skilled design pattern ๐Ÿง

Then again, I asked into the official Bevy's discord and made some search about this word. Special thanks to leonsver1, alice๐ŸŒนฯ‰๐ŸŒน and Franรงois for their lights on the subject.

At the end of this post, you'll discover that it's a complex term for a mostly-irrelevant concept for Bevy beginners, but it's good to know the tool we use, so let's dig in!

The base of an ECS

An ECS is a programming design pattern to store data, it's built on two major parts: the Entities and the Components (that's why we name that Entity Component System obviously ๐Ÿค”).

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

In Bevy, you build both component and entity using Rust's struct. Here you can see a Player entity, composed with Life and Position component:

struct Position {
    x: u32,
    y: u32
}

struct Life(u8);

struct Player {
    life: Life,
    position: Position
}

Our first archetype

Accordingly to the Bevy's Unofficial Cheatbook:

An archetype is the set / combination of components that a given entity has.

At this point of code, you already created your first archetype, which could be defined like so: Our Player entity have Life and Position components.

๐Ÿ™‹ Ok but why? Why do we name something that is just what it is?

The answer to that is data reading performance: behind the scene, to store a Player, Bevy won't just store it on a the same stack of various object of our game, it will be stored on some sort of data table with all other entities that have Life and Position.

________________________________
|   Archetype<Life;Position>   |  
|       | Life |   Position    |
|Player |  88  |  x: 2  y: 12  |
|Player |  75  |  x: 8  y: 20  |
|Player |  67  |  x: 52 y: 25  |
|Player |  25  |  x: 17 y: 12  |

Having what's look like a regular data-table is then easier for the engine to segmentize and cache the data accumulated during the game.

The tricky part: Player โš”๏ธ Enemy

Now what happen if two entities have the same components but represent different kind of data? Here the answer is not so obvious as its depends on what your entity is in Bevy.

In an ECS, the entities are not just the rendered objects of your application.
Throughout the game, you'll need to store many sort of data which may not be displayed graphically, such as the player's settings, the messages from the tchat, and so on...

Bevy has two different ways to store the data.
If the data you want to store is into a struct with the Bundle trait, all the entities which have the same components will be stored as the same archetype. If the data is stored into a basic struct, the data will be stored only with the others data of the same struct type.

So in our case, these two bundles will be stored as the same archetype:

struct Position {
    x: u32,
    y: u32
}

struct Life(u8);

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

#[derive(Bundle)]
struct Enemy {
    life: Life,
    position: Position
}
________________________________
|   Archetype<Life;Position>   |  
|       | Life |   Position    |
|Enemy  |  22  |  x: 22 y: 12  |
|Player |  58  |  x: 82 y: 27  |
|Enemy  |  69  |  x: 52 y: 27  |
|Player |  92  |  x: 12 y: 78  |

but these two structs will be separated:

enum InventoryItemKind {
    POTION,
    EQUIPEMENT,
    MISCELANNEOUS
}

struct InventoryItem {
    kind: InventoryItemKind,
    quantity: u16
}

struct PlayerInventory {
    items: Vec<InventoryItem>,
    capacity: u16
}

struct NPCInventory {
    items: Vec<InventoryItem>,
    capacity: u16
}
_________________________________________   ______________________________________
|       Archetype<PlayerInventory>      |   |      Archetype<NPCInventory>       |
|                 | items  |  capacity  |   |              | items  |  capacity  |
|PlayerInventory  |  ....  |     30     |   |NPCInventory  |  ....  |     10     |
|PlayerInventory  |  ....  |     50     |   |NPCInventory  |  ....  |     20     |

The way all of this is stored depend of decision that Bevy made to optimize the data storage engine.

All of this stuff are internally handled by Bevy, and you could have already build a fantastic game without have even heard of the word archetype. This is why this article is titled "a complex word for nothing".

Nothing? No, it's a bit of a stretch to say nothing, because now that you know how Bevy stores your game's data, it's more easy to optimize.

Increase the performance

Let's imagine a performance issue: you want to build the 69557th Space Invader remake ๐Ÿ‘พ.
To do that you'll probably use the Player and Enemy bundles described earlier, with Life and Position components, so they will be stored with eachother.
The player go to the level 99, there is a ton of enemies, the game handle many many informations about the bullets' position and their damage, additionally to the player's, enemies' and other miscellaneous data.

๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ // The player
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ // is surrounded
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ // by enemies
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ  ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ
๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ  ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ   ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ   ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ    ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ๐Ÿ‘พ

     .       .    .  .. .    .      .     .     .   . . .    . // <<<
.      .          .  . .    .     .     .     .   .    .    .  // Bullets come
.      .       .    .  . . .    .     .     .     .   . . .    // from everywhere

                ๐Ÿš€ // What is the life of the player?

In this situation, if you didn't make any optimization, here is an example of the data the machine need to handle:

________________________________  ______________________________________________
|   Archetype<Life;Position>   |  |    Archetype<Damage;Direction;Position>    |
|       | Life |   Position    |  |       | Damage | Direction |   Position    |
|Enemy  |  22  |  x: 22 y: 12  |  |Bullet |   54   |     โ†‘     |  x: 78 y: 54  |
|Enemy  |  58  |  x: 11 y: 54  |  |Bullet |   22   |     โ†‘     |  x: 98 y: 23  |
| ... More and more enemies... |  |     ... More and more bullets' data...     |
|Player |  92  |  x: 11 y: 3   |<<< Your Player entity, somewhere into the Enemies
|Enemy  |  58  |  x: 82 y: 56  |  _________________________________________
| ... More and more enemies... |  |       Archetype<PlayerInventory>      |
|Enemy  |  58  |  x: 82 y: 78  |  |                 | items  |  capacity  |
|Enemy  |  69  |  x: 52 y: 99  |  |PlayerInventory  |  ....  |     30     |

... And probably a tons of miscellaneous additional data

A real mess! And you need to query the machine on each frame of the game the life of the player, to know if he hit the ground!

Because your Player is stored with all the Enemys, you'll need to loop over all the Archetype<Life;Position> entities on each frame to end the game when the player die.

If you read my previous post about the components markers, you should already know how to separate the Player from the Enemy as two different archetypes, using a component marker:

struct Position {
    x: u32,
    y: u32
}

struct Life(u8);

// We create a marker that we'll add only to the Player bundle
struct PlayerShip;

#[derive(Bundle)]
struct Player {
    life: Life,
    position: Position,
    is_player: PlayerShip // Here is the marker
}

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

Now let see how Bevy will store our game data:

__________________________________________
|   Archetype<Life;Position;PlayerShip>   |
|       | Life |   Position  | PlayerShip |
|Player |  92  | x: 11 y: 3  |            | <<< Your Player entity, easily accessible
________________________________  ______________________________________________
|   Archetype<Life;Position>   |  |    Archetype<Damage;Direction;Position>    |
|       | Life |   Position    |  |       | Damage | Direction |   Position    |
|Enemy  |  22  |  x: 21 y: 12  |  |Bullet |   52   |     โ†‘     |  x: 14 y: 56  |
|Enemy  |  58  |  x: 59 y: 27  |  |Bullet |   99   |     โ†‘     |  x: 22 y: 12  |
|Enemy  |  22  |  x: 22 y: 12  |  |     ... More and more bullets' data...     |
|Enemy  |  58  |  x: 82 y: 27  |  _________________________________________
| ... More and more enemies... |  |       Archetype<PlayerInventory>      |
|Enemy  |  58  |  x: 89 y: 27  |  |                 | items  |  capacity  |
|Enemy  |  69  |  x: 52 y: 27  |  |PlayerInventory  |  ....  |     30     |

... And probably a tons of miscellaneous additional data

Now the engine is able to get your Player's life more easily and quickly. You can create many marker to create more archetype when you feel the need of.

Enjoy ๐Ÿ––

Comments

Be the first to post a comment!

Add a comment

Preview