|
3 weeks ago | |
---|---|---|
.. | ||
README.md | 3 weeks ago | |
assignment.md | 4 weeks ago |
README.md
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 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 :
- 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 avec une couleur de fond.
- 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 :
//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 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 :
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 exempleArrowUp
.keyCode
, qui est une représentation numérique, par exemple37
, 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 :
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 :
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 :
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.
- 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 :
-| 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
-
Ajoutez des objets dédiés pour
hero
,enemy
etgame object
, ils devraient avoir des propriétésx
ety
. (Rappelez-vous la section sur Héritage ou composition).CONSEIL
game object
devrait être celui avec les propriétésx
ety
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 :
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.
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) } }
-
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 :
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.
-
Implémentez le modèle Pub/Sub, cela gardera votre code propre pour les parties restantes.
Pour 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) };
-
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 :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 ? Esquissez un système pour organiser votre code, même s'il reste dans un seul fichier.
Quiz après le cours
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.
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 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.