La réalisation d’un jeu (web ou autre) n’est pas plus complexe que celle d’une web app : elle aussi requiert en amont une conception particulière pour la gestion de son cycle de vie.
Que le jeu soit très basique (un simple quizz) ou au contraire très complexe, il y aura très souvent un système d’écrans et d’étapes qui se suivent les unes après les autres.
Prenons un exemple d’écrans se succédant :
- Chargement / Introduction
- Tutoriel du jeu
- Le jeu
- Ecran de Fin (victoire ou défaite)
Cette liste est non exhaustive car nous pourrions en ajouter / supprimer en fonction de la demande. La particularité de ces écrans est qu’ils sont indépendants les uns des autres : ils doivent s’enchaîner, passer de l’un à l’autre sans tenir compte de l’écran qui le précède et de celui qui suivra.
Prérequis
Nous utiliserons un émetteur d’événements pour la communication entre nos objets javascript : on.js est une petite librairie qui fait très bien le travail. Le code javascript sera écrit en ES6.
Voici la structure du projet :
- index.js (point d’entré de notre code)
- GameManager.js
- GameScene.js
- /0_intro/index.js
- /1_tuto/index.js
- /2_play/index.js
- /3_end/index.js
One manager to rule them all
Pour gérer l’état de nos écrans il va nous falloir mettre en place un Game manager qui sera chargé de gérer le passage d’une étape à l’autre.
// import de nos différentes scène import Intro from './0_intro' import Tuto from './1_tuto' import Play from './2_play' import End from './3_end' export default class GameManager { constructor(type = 'default') { this.scene = Intro this._container = document.querySelector('.game-container') } startScene() { this._container.innerHTML = this._scene.html this._scene.start() } // Lorsqu’une scène enverra un événement de fin // notre game manager aura la responsabilité de // choisir quel sera la scène suivante sceneEnd(evt) { switch (evt.step) { case Intro.STEP: this.scene = Tuto break case Tuto.STEP: this.scene = Play break case Play.STEP: this.scene = End break case End.STEP: console.log('the game is over') break } } get scene() { return this._scene } set scene(scene) { this._scene = scene this._step = this._scene.step // nous verrons plus tard comment //la scène va émettre l’évènement de fin de scène this._scene.onEvent(evt => this.sceneEnd(evt)) } }
Tous en scène
Tous nos écrans de jeu vont avoir en commun différentes actions, c’est pourquoi il est intéressant de faire un objet de scène de jeu qui sera hérité par toutes nos scènes :
// import de notre bibliothèque d'émetteur d'événement import on from 'on' export default class GameScene { constructor(step = 'default') { this.step = step // on transforme notre scène en émetteur d'événement this.onEvent = on(this) } start() { console.log(`scene ${this.step} started`) } end() { // lorsque l’écran se termine //nous envoyons l'événement de fin this.onEvent._fire({ event: 'sceneEnd', step: this.step }) } // Voici le contenu par défaut d'une scène get html() { return `<div>Je suis l'étape : ${this.step}</div>` } }
Les deux actions communes à toutes les scènes sont la méthode start (début du jeu) et end (fin du jeu) qui a pour objectif d’envoyer l’événement de fin de jeu qui pourra être écouté par notre game manager. De plus, chaque scène aura un type enregistré dans la variable this.step, qui sera accessible par notre manager pour qu’il sache quelle étape vient de se finir.
Ensuite voyons comment une scène va hériter de notre objet GameScene et l’utiliser :
import GameScene from '../GameScene' const STEP_VALUE = 'intro' class Scene extends GameScene { get STEP() { return STEP_VALUE } constructor() { super(STEP_VALUE) } // Nous pouvons overrid les méthodes // de GameScene si besoin // get html() {} }
Notre scène d’intro étend GameScene pour profiter des méthodes start et end, de plus elle est typé avec le valeur de STEP qui est égale a intro pour que le manager sache a quel étape en est le déroulement des différents écrans.
Passage d’un écran à un autre
L’étape d’intro pourrait charger les assets de notre jeu, afficher un loader puis une animation / cinématique, pour se terminer et appeler la méthode end()
Pour l’exemple nous allons seulement voir l’écran intro
import GameScene from '../GameScene' const STEP_VALUE = 'intro' class Scene extends GameScene { get STEP() { return STEP_VALUE } constructor() { super(STEP_VALUE) } start() { let btn = document.querySelector('.btn') // lors du click sur notre bouton //nous envoyons l'evenement de fin de scène btn.addEventListener('click', () => this.end()) super.start() } get html() { return `<button class="btn">fin de l'intro</button>` } } export default new Scene()
Une fois le bouton cliqué notre méthode sceneEnd() du fichier GameManager.js sera appelé et il s’occupera de passer à la scène d’intro, puis ainsi de suite pour chacune des scènes.
Cette exemple est plutôt réduit mais il offre une bonne base pour un cycle de vie des différents écrans d’un jeu.
Dans notre scène d’intro, nous aurions par exemple pu overrider la méthode end(), sans oublier d’appeler super.end(), pour retirer l’écouteur d’événement clique sur notre bouton. En effet il est important qu’une scène gère sa création et sa destruction pour des raison de performances.
Vous pouvez retrouver le code complet ici sur github et n’hésitez pas à commenter cet article si vous des questions !