# Construire un jeu spatial Partie 3 : Ajouter du mouvement ## Quiz avant le cours [Quiz avant le cours](https://ff-quizzes.netlify.app/web/quiz/33) Les jeux ne sont pas très amusants tant que vous n'avez pas des extraterrestres qui se déplacent à l'écran ! Dans ce jeu, nous allons utiliser deux types de mouvements : - **Mouvement clavier/souris** : lorsque l'utilisateur interagit avec le clavier ou la souris pour déplacer un objet à l'écran. - **Mouvement induit par le jeu** : lorsque le jeu déplace un objet à un certain intervalle de temps. Alors, comment déplace-t-on des objets à l'écran ? Tout repose sur les coordonnées cartésiennes : on modifie la position (x, y) de l'objet, puis on redessine l'écran. En général, voici les étapes nécessaires pour réaliser un *mouvement* à l'écran : 1. **Définir une nouvelle position** pour un objet ; cela est nécessaire pour donner l'impression que l'objet s'est déplacé. 2. **Effacer l'écran**, l'écran doit être nettoyé entre chaque dessin. On peut le faire en dessinant un rectangle rempli avec une couleur de fond. 3. **Redessiner l'objet** à sa nouvelle position. Cela permet finalement de déplacer l'objet d'un endroit à un autre. Voici à quoi cela peut ressembler en code : ```javascript //set the hero's location hero.x += 5; // clear the rectangle that hosts the hero ctx.clearRect(0, 0, canvas.width, canvas.height); // redraw the game background and hero ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillStyle = "black"; ctx.drawImage(heroImg, hero.x, hero.y); ``` ✅ Pouvez-vous penser à une raison pour laquelle redessiner votre héros plusieurs fois par seconde pourrait entraîner des coûts de performance ? Lisez à propos des [alternatives à ce modèle](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas). ## Gérer les événements clavier Vous gérez les événements en attachant des événements spécifiques à du code. Les événements clavier sont déclenchés sur l'ensemble de la fenêtre, tandis que les événements souris comme un `click` peuvent être connectés à un élément spécifique. Nous utiliserons des événements clavier tout au long de ce projet. Pour gérer un événement, vous devez utiliser la méthode `addEventListener()` de la fenêtre et lui fournir deux paramètres. Le premier paramètre est le nom de l'événement, par exemple `keyup`. Le second paramètre est la fonction qui doit être invoquée lorsque l'événement se produit. Voici un exemple : ```javascript window.addEventListener('keyup', (evt) => { // `evt.key` = string representation of the key if (evt.key === 'ArrowUp') { // do something } }) ``` Pour les événements clavier, il existe deux propriétés sur l'événement que vous pouvez utiliser pour savoir quelle touche a été pressée : - `key`, qui est une représentation sous forme de chaîne de la touche pressée, par exemple `ArrowUp`. - `keyCode`, qui est une représentation numérique, par exemple `37`, correspondant à `ArrowLeft`. ✅ La manipulation des événements clavier est utile en dehors du développement de jeux. À quelles autres utilisations pouvez-vous penser pour cette technique ? ### Touches spéciales : une mise en garde Certaines touches *spéciales* affectent la fenêtre. Cela signifie que si vous écoutez un événement `keyup` et que vous utilisez ces touches spéciales pour déplacer votre héros, cela entraînera également un défilement horizontal. Pour cette raison, vous pourriez vouloir *désactiver* ce comportement intégré du navigateur lorsque vous développez votre jeu. Vous avez besoin d'un code comme celui-ci : ```javascript let onKeyDown = function (e) { console.log(e.keyCode); switch (e.keyCode) { case 37: case 39: case 38: case 40: // Arrow keys case 32: e.preventDefault(); break; // Space default: break; // do not block other keys } }; window.addEventListener('keydown', onKeyDown); ``` Le code ci-dessus garantit que les touches fléchées et la barre d'espace ont leur comportement *par défaut* désactivé. Le mécanisme de *désactivation* se produit lorsque nous appelons `e.preventDefault()`. ## Mouvement induit par le jeu Nous pouvons faire bouger des objets par eux-mêmes en utilisant des minuteries comme les fonctions `setTimeout()` ou `setInterval()` qui mettent à jour la position de l'objet à chaque tick, ou intervalle de temps. Voici à quoi cela peut ressembler : ```javascript let id = setInterval(() => { //move the enemy on the y axis enemy.y += 10; }) ``` ## La boucle de jeu La boucle de jeu est un concept qui consiste essentiellement en une fonction invoquée à intervalles réguliers. On l'appelle la boucle de jeu car tout ce qui doit être visible pour l'utilisateur est dessiné dans cette boucle. La boucle de jeu utilise tous les objets du jeu qui en font partie, en les dessinant tous sauf si, pour une raison quelconque, ils ne font plus partie du jeu. Par exemple, si un objet est un ennemi touché par un laser et explose, il ne fait plus partie de la boucle de jeu actuelle (vous en apprendrez davantage à ce sujet dans les leçons suivantes). Voici à quoi une boucle de jeu peut ressembler, exprimée en code : ```javascript let gameLoopId = setInterval(() => function gameLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawHero(); drawEnemies(); drawStaticObjects(); }, 200); ``` La boucle ci-dessus est invoquée toutes les `200` millisecondes pour redessiner le canvas. Vous pouvez choisir l'intervalle qui convient le mieux à votre jeu. ## Poursuivre le jeu spatial Vous allez prendre le code existant et l'étendre. Soit vous commencez avec le code que vous avez complété lors de la partie I, soit vous utilisez le code dans [Partie II - starter](../../../../6-space-game/3-moving-elements-around/your-work). - **Déplacer le héros** : vous ajouterez du code pour permettre de déplacer le héros à l'aide des touches fléchées. - **Déplacer les ennemis** : vous devrez également ajouter du code pour que les ennemis se déplacent de haut en bas à un rythme donné. ## Étapes recommandées Trouvez les fichiers qui ont été créés pour vous dans le sous-dossier `your-work`. Il devrait contenir les éléments suivants : ```bash -| assets -| enemyShip.png -| player.png -| index.html -| app.js -| package.json ``` Vous démarrez votre projet dans le dossier `your_work` en tapant : ```bash cd your-work npm start ``` Cela démarrera un serveur HTTP à l'adresse `http://localhost:5000`. Ouvrez un navigateur et entrez cette adresse, pour l'instant, cela devrait afficher le héros et tous les ennemis ; rien ne bouge - encore ! ### Ajouter du code 1. **Ajoutez des objets dédiés** pour `hero`, `enemy` et `game object`, ils devraient avoir des propriétés `x` et `y`. (Rappelez-vous la section sur [Héritage ou composition](../README.md)). *CONSEIL* `game object` devrait être celui avec les propriétés `x` et `y` et la capacité de se dessiner sur un canvas. > Conseil : commencez par ajouter une nouvelle classe GameObject avec son constructeur défini comme ci-dessous, puis dessinez-la sur le canvas : ```javascript class GameObject { constructor(x, y) { this.x = x; this.y = y; this.dead = false; this.type = ""; this.width = 0; this.height = 0; this.img = undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } } ``` Maintenant, étendez cet objet GameObject pour créer le Hero et l'Enemy. ```javascript class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } } ``` ```javascript class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } } ``` 2. **Ajoutez des gestionnaires d'événements clavier** pour gérer la navigation (déplacer le héros vers le haut/bas gauche/droite). *RAPPEL* c'est un système cartésien, le coin supérieur gauche est `0,0`. N'oubliez pas non plus d'ajouter du code pour arrêter le *comportement par défaut*. > Conseil : créez votre fonction onKeyDown et attachez-la à la fenêtre : ```javascript let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown); ``` Vérifiez la console de votre navigateur à ce stade, et observez les frappes de touches enregistrées. 3. **Implémentez** le [modèle Pub/Sub](../README.md), cela gardera votre code propre pour les parties restantes. Pour cette dernière partie, vous pouvez : 1. **Ajouter un écouteur d'événements** sur la fenêtre : ```javascript window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } }); ``` 1. **Créer une classe EventEmitter** pour publier et s'abonner à des messages : ```javascript class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach((l) => l(message, payload)); } } } ``` 1. **Ajouter des constantes** et configurer l'EventEmitter : ```javascript const Messages = { KEY_EVENT_UP: "KEY_EVENT_UP", KEY_EVENT_DOWN: "KEY_EVENT_DOWN", KEY_EVENT_LEFT: "KEY_EVENT_LEFT", KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter(); ``` 1. **Initialiser le jeu** ```javascript function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -=5 ; }) eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); } ``` 1. **Configurer la boucle de jeu** Refactorisez la fonction window.onload pour initialiser le jeu et configurer une boucle de jeu à un bon intervalle. Vous ajouterez également un rayon laser : ```javascript window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) }; ``` 5. **Ajoutez du code** pour déplacer les ennemis à un certain intervalle Refactorisez la fonction `createEnemies()` pour créer les ennemis et les ajouter à la nouvelle classe gameObjects : ```javascript function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } } ``` et ajoutez une fonction `createHero()` pour effectuer un processus similaire pour le héros. ```javascript function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); } ``` et enfin, ajoutez une fonction `drawGameObjects()` pour commencer le dessin : ```javascript function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); } ``` Vos ennemis devraient commencer à avancer vers votre vaisseau spatial héros ! --- ## 🚀 Défi Comme vous pouvez le constater, votre code peut devenir un "code spaghetti" lorsque vous commencez à ajouter des fonctions, des variables et des classes. Comment pouvez-vous mieux organiser votre code pour qu'il soit plus lisible ? Esquissez un système pour organiser votre code, même s'il reste dans un seul fichier. ## Quiz après le cours [Quiz après le cours](https://ff-quizzes.netlify.app/web/quiz/34) ## Révision et auto-apprentissage Bien que nous écrivions notre jeu sans utiliser de frameworks, il existe de nombreux frameworks JavaScript basés sur le canvas pour le développement de jeux. Prenez le temps de faire quelques [lectures à ce sujet](https://github.com/collections/javascript-game-engines). ## Devoir [Commentez votre code](assignment.md) --- **Avertissement** : Ce document a été traduit à l'aide du service de traduction automatique [Co-op Translator](https://github.com/Azure/co-op-translator). Bien que nous nous efforcions d'assurer l'exactitude, veuillez noter que les traductions automatisées peuvent contenir des erreurs ou des inexactitudes. Le document original dans sa langue d'origine doit être considéré comme la source faisant autorité. Pour des informations critiques, il est recommandé de faire appel à une traduction humaine professionnelle. Nous déclinons toute responsabilité en cas de malentendus ou d'interprétations erronées résultant de l'utilisation de cette traduction.