Mon studio de développement d'expériences numériques.
Avec de vrais morceaux de codes, je conçoit des choses utiles - parfois et
d'autres moins inutiles - souvent.

Découvrez moi, mes articles, ou mes réalisations.

Créer un jeu vidéo - Journal de bord partie 3

Créer un jeu vidéo - Journal de bord partie 3

gamedev, javascript, threejs Publié le mar. 11 févr. 2025

Dans la deuxième partie de la suite d’articles sur la création d’un jeu vidéo, nous avons vu comment mieux structurer notre code, dans cette partie, nous allons voir comment gérer les éléments de notre jeu.

De façon assez naïve la réflexion que l’on pourrait avoir serait de créer une classe pour chaque élément de notre jeu, par exemple une classe Player, une classe PNJ et une classe Monster.

Pour gérer les points communs, comme le déplacement par exemple, on pourrait créer une classe Person qui serait la classe mère de Player, PNJ et Monster. Mais il s’avère que s’il y a du comportement commun entre ces élèments de jeux, il y a aussi des comportements qui leur sont propres. Et avec cette technique, on va se retrouver à devoir gérer des exceptions dans les classes filles pour gérer ces comportements spécifiques. Voir pire embarquer du code non utile dans les classes filles héritées de la classe mère.

ECS (Entity Component System)

Les moteurs de jeu récent ont adopté une autre approche pour répondre à ce problème d’héritage multiple, avec le modèle ECS pour Entity Component System. L’idée est la suivante :

Chaque élèment du jeu est une entité (Entity) qui possède un identifiant et un ensemble de composants. Un composant (Component) défini un comportement qui sera appliqué à l’entité, un composant possède bien souvent un paramétrage pour définir son comportement. Enfin des systèmes (System) vont appliquer des traitements sur les entités qui possèdent certains composants, ce sont ces systèmes qui possèdent la logique des comportements.

Prenons un exemple pour illustrer ce concept, on souhaite faire bouger un cube.

On va créer une classe Entity qui va représenter notre entité, cette classe va posséder un identifiant et un tableau de composants.

class Entity {
  id: string;
  components: Record<string, Component> = {}

  constructor(id: string) {
    this.id = id;
  }

  addComponent(component: Component) {
    this.components[component.name] = component
  }

  hasComponent(name: string | string[]) {
    if (Array.isArray(name)) {
      return name.every((n) => this.components[n])
    }

    return !!this.components[name]
  }
}

Ensuite, on va créer notre entité cube.

const cube = new Entity('cube');

On va créer la classe Component qui va représenter un composant.

abstract class Component {
  name: string

  protected constructor(name: string) {
    this.name = name
  }
}

Et on va créer un composant Movable qui va définir les informations de déplacement de notre cube, la direction, la vitesse, etc.

class Movable extends Component {
  direction: Vector3 // Direction de déplacement, ThreeJs fournit un objet Vector3 pour représenter un vecteur 3D
  speed: number // Vitesse de déplacement

  constructor(direction: Vector3, speed: number) {
    super('movable')
    this.direction = direction
    this.speed = speed
  }
}

On va ajouter ce composant à notre entité cube.

cube.addComponent(new Movable(new Vector3(1, 0, 0), 10)) // On fait bouger le cube vers la droite

Ensuite, on va s’occuper de créer la classe System qui va représenter un système.

export abstract class System {
  // Méthode qui va être appelée à chaque frame pour mettre à jour les entités
  abstract update(entities: Entity[], delta: number): void
}

Enfin, on va créer un système MovementSystem qui va déplacer les entités qui possèdent le composant Movable.

class MovementSystem extends System {
  update(entities: Entity[], delta: number) {
    entities.forEach((entity) => {
      // On traite que les entités qui possèdent le composant Movable
      if (entity.hasComponent('movable')) {
        const movable = entity.components.movable as Movable

        // On déplace l'entité en fonction de la direction et de la vitesse
        entity.position.add(movable.direction.clone().multiplyScalar(movable.speed * delta))
      }
    })
  }
}

Bien entendu, cet exemple est simpliste comme son implémentation, mais il permet de comprendre le concept de base de l’ECS. Pour l’intégrer dans notre code, on va créer une classe ECS qui va gérer les entités et les systèmes.

class ECS {
  game: Game = Game.getInstance()
  entities: Entity[] = []
  systems: System[] = []

  constructor() {
    this.game.time.events.on('tick', () => this.update())
  }

  addEntity(entity: Entity) {
    this.entities.push(entity)
  }

  addSystem(system: System) {
    this.systems.push(system)
  }

  update() {
    this.systems.forEach((system) => {
      system.update(this.entities, this.game.time.deltaTime)
    })
  }
}

Ensuite, on l’ajoute dans notre classe Game.

class Game {
  // ...
  ecs: ECS

  constructor() {
    // ...
    this.ecs = new ECS()
  }
}

Et voilà ! On a maintenant un système de gestion des éléments de notre jeu qui est plus flexible et plus facile à maintenir.

Dans la prochaine partie, on va voir comment gérer le déplacement de notre joueur et la gestion de la caméra.