ECS en pratique : EventBus, testabilité et pont avec l'entreprise

— 5 min de lecture

ECS en pratique : EventBus, testabilité et pont avec l'entreprise

Read in English


Dans le premier article, j’ai posé les bases de l’Entity Component System à travers un jeu de stratégie spatiale 4X que je développe dans le navigateur avec PixiJS et TypeScript.

Le constat de départ : la hiérarchie de classes OOP ne tient pas quand des centaines d’entités différentes interagissent à chaque frame. L’ECS propose une alternative radicale : des entités réduites à un simple ID, des composants qui ne portent que des données, et des systèmes qui contiennent toute la logique. On a vu comment un VelocitySystem de 30 lignes gère la physique de tout le jeu, et comment le BehaviorSystem plugge des comportements IA sans modifier l’existant.

Mais une question restait ouverte : si les systèmes sont indépendants et ne se connaissent pas, comment communiquent-ils ? Et surtout : qu’est-ce que ce pattern dit aux architectes d’entreprise habitués aux message brokers, aux architectures event-driven et aux microservices ?

C’est ce qu’on explore ici.

Dans cet article :

L’EventBus

L’ECS, en théorie, c’est trois concepts : Entity, Component, System. Point. Mais en pratique, dès que le jeu dépasse le prototype, une question s’impose : si les systèmes sont indépendants et ne se connaissent pas, comment communiquent-ils ?

Un EventBus, c’est un canal de communication interne. Un système peut émettre un événement (“ce vaisseau vient d’être détruit”) sans savoir qui écoute. D’autres systèmes s’abonnent aux événements qui les intéressent et réagissent. Personne n’importe personne. Le contrat, c’est l’événement, pas le module.

Sans ça, le couplage revient par la porte de derrière. Si le CombatSystem doit poser un composant “flag” que le FactionSystem sait lire, ils partagent une connaissance implicite. L’EventBus rend ce contrat explicite et typé.

En entreprise, c’est exactement la même réalité : on peut faire communiquer des services en appels directs, mais à un moment on a besoin d’un broker (Kafka, SNS, RabbitMQ). Sauf qu’ici, tout tourne dans un seul processus dans le navigateur, ce qui ramène les temps de traitement à l’ordre de la microseconde, là où un broker réseau est plutôt sur la milliseconde voire la dizaine de millisecondes.

Dans son état actuel, le jeu définit une centaine d’événements typés. En voici deux :

"entity:destroyed": {
  entityId: number;
  victimFaction: string;
  killerFaction: string;
  sectorId: string;
};

"trade:completed": {
  shipEntityId: number;
  stationEntityId: number;
  resourceId: string;
  qty: number;
  totalPrice: number;
  action: "buy" | "sell";
};

Le CombatSystem émet entity:destroyed. Le FactionSystem écoute cet événement pour mettre à jour les relations diplomatiques. Le RenderBridge l’écoute aussi pour afficher une explosion. Aucun de ces modules n’importe directement les autres.

graph LR
  CS["CombatSystem"] -- "emit" --> EB{{"EventBus"}}
  EB -- "entity:destroyed" --> FS["FactionSystem"]
  EB -- "entity:destroyed" --> RB["RenderBridge"]

  style EB stroke-width:2px
Les systèmes communiquent par événements typés, sans s’importer mutuellement, comme un broker Kafka, mais en microsecondes.

C’est exactement le pattern qu’on retrouve en architecture d’entreprise avec les message brokers (Kafka, SNS, RabbitMQ).

Les règles pures

Les formules de calcul (dégâts, prix, physique) sont extraites dans des fonctions pures, dans un dossier dédié :

// movement.ts
export function applyThrust(vx, vy, rotation, acceleration, dt) {
  return {
    vx: vx + Math.cos(rotation) * acceleration * dt,
    vy: vy + Math.sin(rotation) * acceleration * dt,
  };
}

Pas de dépendance, pas d’effet de bord. Input → output. Ces fonctions sont réutilisables et testables unitairement sans aucun setup.

D’ailleurs, tous les tests du domaine tournent sans navigateur. On instancie un vrai World, un vrai EventBus, un vrai système. On émet un événement, on fait un update, on vérifie l’état. Pas de mock, pas de DOM.

L’ECS au-delà de ce projet

Mon implémentation est un prototype maison en TypeScript. Mais le pattern est validé à bien plus grande échelle.

En gaming, les grands moteurs convergent vers le data-oriented design :

  • Unity DOTS — framework ECS optionnel pour la haute performance
  • Unreal Engine 5 Mass Entity — architecture data-oriented pour gérer des dizaines de milliers d’agents autonomes
  • Bevy (Rust) — moteur entièrement construit sur l’ECS, avec parallélisation automatique des systèmes

Hors gaming, le simulateur robotique Gazebo utilise un ECS pour modéliser des agents autonomes. Ses systèmes déclarent à quelle phase du cycle ils interviennent (PreUpdate, Update, PostUpdate), une discipline qu’on retrouve dans la plupart des moteurs de jeu.

Le pont avec l’architecture d’entreprise

Si vous travaillez sur des systèmes distribués, l’ECS vous parlera. Voici les parallèles que j’ai identifiés :

La composition d’entités ressemble à la composition de services. En ECS, une entité est un ID auquel on attache des composants. En microservices, un domaine métier est un ensemble de services composés autour d’une responsabilité.

L’EventBus intra-processus est un message broker local. Les systèmes communiquent par événements typés. Remplacez “intra-processus” par “inter-services” et vous avez Kafka ou SNS.

La séparation données/logique structure naturellement le flux. Les composants portent l’état, les systèmes le mutent, les queries le lisent. On retrouve une séparation lecture/écriture qui rappelle les architectures event-driven.

La testabilité vient du même principe. En ECS, chaque système est testable en isolation parce qu’il ne dépend que du World et de l’EventBus. En microservices, chaque service est testable en isolation parce qu’il ne dépend que de ses ports (API, message queue).

L’indépendance des systèmes permet le scaling sélectif, exactement comme l’indépendance des services permet le scaling horizontal : chaque partie du système évolue à son propre rythme.

Ce que ça change pour un architecte

Après quelques semaines à coder ce jeu, j’ai une conviction renforcée : la composition bat l’héritage, dans le game dev comme en entreprise.

L’ECS force une discipline de conception. On ne peut pas tricher avec un héritage rapide. Chaque fonctionnalité passe par le même schéma : un composant (les données), un système (la logique), des événements (la communication). C’est contraignant au début, libérateur ensuite.

Cette discipline produit un code lisible. Un nouveau développeur qui ouvre le projet comprend VelocitySystem en 30 secondes. Il n’a pas besoin de remonter cinq niveaux d’héritage pour comprendre d’où vient un comportement.

Et surtout, ça scale. Pas seulement en performance (le jeu gère actuellement plus de 200 000 entités réparties sur 154 secteurs sans sourciller), mais en complexité organisationnelle. Ajouter un nouveau comportement ne demande pas de comprendre l’existant — juste de savoir quels composants lire et quels événements émettre.

C’est exactement la promesse des architectures event-driven et microservices. L’ECS m’a permis de la vivre à petite échelle, en une soirée, dans un navigateur.

Pour aller plus loin

Maintenant que les fondations sont posées, la prochaine question : qu’est-ce qui se passe quand 200 000 entités commencent à échanger des ressources, sans chef d’orchestre ? Le prochain article creuse l’économie émergente — et les parallèles avec les systèmes distribués en entreprise.