15 KiB
Construire un jeu spatial Partie 3 : Ajouter du mouvement
Quiz avant le cours
Les jeux ne sont pas très amusants tant que vous n'avez pas des aliens 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 accomplir un mouvement à l'écran :
- Définir une nouvelle position pour un objet ; cela est nécessaire pour donner l'impression que l'objet s'est déplacé.
- Effacer l'écran, l'écran doit être nettoyé entre chaque dessin. On peut le faire en dessinant un rectangle rempli d'une couleur de fond.
- Redessiner l'objet à sa nouvelle position. En faisant cela, on parvient finalement à déplacer l'objet d'un endroit à un autre.
Voici à quoi cela peut ressembler en code :
//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.
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 lié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 d'entrée. 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 :
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 voir quelle touche a été pressée :
key
, c'est une représentation sous forme de chaîne de la touche pressée, par exempleArrowUp
.keyCode
, c'est une représentation sous forme de nombre, par exemple37
, qui correspond àArrowLeft
.
✅ La manipulation des événements clavier est utile en dehors du développement de jeux. À quels autres usages pouvez-vous penser pour cette technique ?
Touches spéciales : une mise en garde
Il existe certaines touches spéciales qui 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 de code comme celui-ci :
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 garantira que les touches fléchées et la touche 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 :
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 est essentiellement 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 doivent plus faire partie du jeu. Par exemple, si un objet est un ennemi qui a été 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 typiquement ressembler, exprimée en code :
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 avez la possibilité de 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 terminé lors de la partie I, soit vous utilisez le code de Partie II - starter.
- 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
Localisez les fichiers qui ont été créés pour vous dans le sous-dossier your-work
. Il devrait contenir les éléments suivants :
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Vous démarrez votre projet dans le dossier your_work
en tapant :
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
-
Ajouter des objets dédiés pour
hero
,enemy
etgame object
, ils devraient avoir des propriétésx
ety
. (Rappelez-vous la partie sur Héritage ou composition).ASTUCE
game object
devrait être celui avecx
ety
et la capacité de se dessiner sur un canvas.astuce : commencez par ajouter une nouvelle classe GameObject avec son constructeur défini comme ci-dessous, puis dessinez-la sur le canvas :
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 ce GameObject pour créer le Hero et Enemy.
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
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) } }
-
Ajouter des gestionnaires d'événements clavier pour gérer la navigation (déplacer le héros vers le haut/bas gauche/droite).
RAPPELEZ-VOUS c'est un système cartésien, en haut à gauche est
0,0
. Rappelez-vous également d'ajouter du code pour arrêter le comportement par défaut.astuce : créez votre fonction onKeyDown et attachez-la à la fenêtre :
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 qui sont enregistrées.
-
Implémenter le modèle Pub/Sub, cela gardera votre code propre pour les parties restantes.
Pour réaliser cette dernière partie, vous pouvez :
-
Ajouter un écouteur d'événements sur la fenêtre :
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); } });
-
Créer une classe EventEmitter pour publier et s'abonner à des messages :
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)); } } }
-
Ajouter des constantes et configurer l'EventEmitter :
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();
-
Initialiser le jeu
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; }); }
-
-
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 :
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) };
-
Ajouter 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 :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.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 :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 ? Dessinez un système pour organiser votre code, même s'il reste dans un seul fichier.
Quiz après le cours
Révision et étude personnelle
Bien que nous écrivions notre jeu sans utiliser de frameworks, il existe de nombreux frameworks basés sur JavaScript pour le développement de jeux sur canvas. Prenez le temps de faire des recherches à ce sujet.
Devoir
Avertissement :
Ce document a été traduit à l'aide du service de traduction automatique 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 recourir à une traduction professionnelle réalisée par un humain. Nous déclinons toute responsabilité en cas de malentendus ou d'interprétations erronées résultant de l'utilisation de cette traduction.