Architecture ECS
Par Dimitri • Lecture 3 min. •
ECS
L'ECS est une architecture logicielle permettant de découpler la donnée des objets qui la portent.
E : Entity
C : Component
S : System
Entity
Une entité est similaire à une entité du Domain Driven Design. C'est quelque chose qui possède une identité unique et un cycle de vie. A l'inverse d'une valeur discrète comme une monnaie ou une distance. Dans cette architecture, une entité est très souvent un ID unique.
Example
let entity = commands.spawn_empty().id();
Comme dit plus tôt, une entité est juste un ID, ce qui définie sont identité. Mais il faut plus que des lists d'ID pour construire une application. C'est pourquoi à cet ID, on peut attacher des Composants.
Component
Les composants sont des conteneurs de données. Des valeurs qui definissent l'état actuel d'une entité. Par exemple, une entité pourrait contenir :
- Position
- Vitesse
- Accélération
Avec ces trois composants, si nous sommes dans une simulation 2D, la position pourrait être un vecteur 2. Ou dans le cas d'un système de géolocalisation on pourrait envisager une position (latitude, longitude) ou encore des coordonnées polaires.
Example
let entity = commands.spawn((
Name::new("Je suis une entité avec un composant Nom"),
Position(Vec2::new(0.0, 0.0)),
Vitesse(Vec2::new(0.0, 0.0)),
Acceleration(Vec2::new(0.0, 0.0))
)).id();
Cette entité contient les composants nécessaires au traitement souhaité lors de simulations physique basiques.
La donnée, loin d'être inutile, sert à apporter du comportement aux entités qui les détiennent via des Systèmes.
System
Les systèmes sont le coeur du fonctionnenment de cette architecture, sans eux, pas de changements. Ils ne sont ni plus ni moins que des fonctions qui prennent des listes de composants et effectuent des opérations dessus.
Par exemple, avec les composants plus haut on peut envisager un système qui prend la liste de tous les tuples (position, vitesse) et ajoute la vitesse à la position en fonction du temps pour faire se déplacer l'entité qui porte ces composants. Un autre système pourrait intégrer l'accélération à la vitesse avant qu'elle ne soit ajoutée. De cette manière, chaque système est spécialisé sur une interaction entre composants.
Exemple
pub fn integrate_speed(mut query: Query<(&mut Position, &Vitesse)>) {
for (mut position, vitesse) in query.iter_mut() {
position.0 += vitesse.0;
}
}
Ici integrate_speed() est une fonction qui prend en paramètre une Query (un itérateur), qui liste TOUTES les entités qui possèdent les composants Position et Vistesse.
Ici le système souhaite modifier le composant Lifetime, pour le faire avancer du tick qui sépare deux frames, donc on a besoin de l'indiquer comme mutable (mais c'est une spécificité de Rust, pas de l'ECS).
Attention
Le code ici est à prendre avec des pincettes, lors d'une intégration de vitesse dans une position, dans le cadre d'une simulation en temps réel, il faut aussi prendre en compte le temps écoulé entre 2 passages du système et éventuellement avoir un système qui va stabiliser dans le temps le nombre de passages du système, c'est ce qu'on appelle une Fixed Timestep
Conclusion
Note
Cette architecture peut être pensée comme une base de donnée colonnaire en temps réel. Les entités ont autant de composants que l'on souhaite, et les systèmes traitent composants par composant sans s'occuper directement des entités qui les portes.
Cette architecture est notamment utilisée dans le traitement en temps réel et par extension dans les jeux vidéos qui sont des systèmes en temps réel.
L'alternative à l'ECS dans les moteurs modernes c'est souvent d'avoir des objets qui possèdent tous une fonction update() (cf godot et unity) appelée chaque frame pour mettre à jour l'objet qui la possède.