Entrons désormais dans le vif du sujet! Dans cette vidéo nous mettrons en pratique toutes les connaissances acquises des vidéos précédentes pour créer les bases du jeu Pong.
Nous définirons des variables et des constantes, des conditions et booléens et les fonctions de la boucle de jeu du starter kit.
Installation du starter kit vierge
Tout d'abord, il nous faut à nouveau installer le starter kit, exactement de la même manière que la première partie du cours précédent:
- Rendez vous sur la page https://github.com/GameDev-Ninja/starter-kit
- Téléchargez l'archive ZIP en cliquant sur le boutn
Code
puisDownload ZIP
- Extrayez l'archive
- Ouvrez le dossier avec votre éditeur de code, et le fichier
index.html
avec votre navigateur
Une fois le kit installé, nous supprimons dans le fichier script.js
tous les éléments inutiles: la définition de l'objet logo et son chargement, ainsi que son dessin dans la fonction DrawGame
.
Le code obtenu doit être le suivant:
/**
* Données initiales du jeu
*/
/**
* Exécutée une seule fois, au chargement
*/
function LoadGame(canvas, context) {
}
/**
* Exécutée perpétuellement pour mettre à jour les données
*/
function UpdateGame(deltaTime) {
}
/**
* Exécutée perpétuellement pour dessiner la frame actuelle
*/
function DrawGame(context) {
}
C'est ainsi que vous démarrerez tous vos projets à partir du starter kit, la boucle de jeu est fonctionnelle, mais ne crée et n'affiche aucun élément.
Notre premier élément
Désormais commençons par créer notre premier élément: une raquette. Nous définissons au début du fichier sous le commentaire Données initiales du jeu
une donnée playerLeftPad
de type objet:
const playerLeftPad = {
x: 0, // Position horizontale de la raquette à l'écran
y: 0, // Position verticale de la raquette à l'écran
width: 25, // Largeur de la raquette
height: 100 // Hauteur de la raquette
}
Une fois notre élément créé, nous pouvons d'ores et déjà l'afficher à l'écran, dans la fonction DrawGame
à l'aide de la fonction context.fillRect
qui nécessite justement les 4 paramètres que nous avons définis dans notre objet: la position horizotale du rectangle, sa position verticale, sa largeur, et sa hauteur:
function DrawGame(context) {
context.fillRect(playerLeftPad.x, playerLeftPad.y, playerLeftPad.width, playerLeftPad.height)
}
Vous pouvez rafraîchir votre écran de jeu dans le navigateur pour voir apparaître un rectangle blanc, sur le bord haut/gauche.
Il nous faut encore le positionner correctement: le décaler légèrement vers la droite pour créer une marge, et le centrer à l'écran.
La création des données comme nous l'avons fait précédemment nécessite d'être réalisée au début du fichier, afin que la donnée soit accessible à nos trois fonction de la boucle de jeu. En revanche pour définir la position verticale parfaitement au centre de l'écran, nous devrons le faire dans la fonction LoadGame
.
La fonction LoadGame
reçois deux paramètres, celui qui nous intéresse est le premier: canvas
qui est un élément Canvas affiché dans le navigateur utilisé pour dessiner notre écran de jeu. Cet élément a les propriétés width
et height
qui nous permettrons de positionner correctement nos raquettes.
Dans un soucis de simplicité, pour pouvoir être réutilisé dans les autres fonctions, nous créons sous notre donnée playerLeftPad
, une seconde donnée screen
dans laquelle nous stockerons la largeur et la hauteur de notre écran de jeu:
const screen = {
width: 0,
height: 0
}
Enfin dans la fonction LoadGame
nous récupérons la largeur et la hauteur de l'écran de jeu, et calculons par rapport à celui-ci:
function LoadGame(canvas, context) {
// On récupères et stock
// la largeur et la hauteur de l'écran de jeu
screen.width = canvas.width
screen.height = canvas.height
// On décale la raquette de 10 pixels vers la droite
playerLeftPad.x = 10
// On la positionne à la moitié de la hauteur de l'écran de jeu
playerLeftPad.y = screen.height / 2
}
En rafraichissant notre écran de jeu, vous pouvez constater que notre raquettes est mieux positionnée, mais pas parfaitement au centre. Cela est dû au fait que les coordoonées de positionnement qui sont fournies à la fonction fillRect
sont utilisées pour dessiner le rectangle depuis son coin haut/gauche.
Nous devons donc modifier le calcul de la position verticale de la raquette pour lui soustraire la moitié de sa hauteur:
playerLeftPad.y = screen.height / 2 - playerLeftPad.height / 2
Désormais notre première raquette est parfaitement positionnée. Vous pouvez le visualiser dans la démo suivante.
Seconde raquette et balle
Maintenant que nous avons vu comment créer un élément et le positionner à l'écran selon les dimensions de l'écran de jeu, créons les deux autres éléments fondamentaux du Pong: la seconde raquette ainsi que la balle.
Nous ajoutons tout d'abord deux nouvelles données en haut de notre fichier, près de nos données déjà existantes playerLeftPad
et screen
:
// Raquette de droite
const playerRightPad = {
x: 0,
y: 0,
width: 25,
height: 100
}
// Balle
const ball = {
x: 0,
y: 0,
width: 16,
height: 16
}
Nous dessinons ces deux nouveaux éléments à l'écran à l'aide de la même fonction fillRect
utilisée précédemment, dans la fonction DrawGame
function DrawGame(context) {
// Dessine la raquette de gauche
context.fillRect(playerLeftPad.x, playerLeftPad.y, playerLeftPad.width, playerLeftPad.height)
// Dessine la raquette de droite
context.fillRect(playerRightPad.x, playerRightPad.y, playerRightPad.width, playerRightPad.height)
// Dessine la balle
context.fillRect(ball.x, ball.y, ball.width, ball.height)
}
Et enfin, à nouveau dans la fonction LoadGame
, nous calculons les positions initiales de ces éléments, en fonction de la taille de notre écran de jeu.
function LoadGame(canvas, context) {
// Récupération de la taille de l'écran
screen.width = canvas.width
screen.height = canvas.height
// Positionnement de la raquette de gauche
playerLeftPad.x = 10
playerLeftPad.y = screen.height / 2 - playerLeftPad.height / 2
// Positionnement de la raquette de droite
playerRightPad.x = screen.width - 10 - playerRightPad.width
playerRightPad.y = screen.height / 2 - playerRightPad.height / 2
// Positionnement de la balle
ball.x = screen.width / 2 - ball.width / 2
ball.y = screen.height / 2 - ball.height / 2
}
Parmis les choses importantes à bien comprendre dans ce code:
- la soustraction de la moitié de la largeur et hauteur de la balle dans son positionnement, nécessaire pour la centrer parfaitement à l'écran, la fonction
fillRect
dessinant pour rappel les rectangle selon leur bord haut/gauche. - la soustraction de la largeur de la raquette de droite dans son positionnement, pour exactement la même raison que le positionnement de la balle.
Désormais comme vous pouvez le visualiser dans la démo ci-dessous, nous avons tous nos éléments de notre jeu Pong.
Gestion des événements claviers
Maintenant que nous avons tous nos éléments, il est temps de prendre le contrôles de nos deux raquettes, nous utiliserons les touche Flèche Haute et Flèche Basse pour contrôler la raquette de droite, et Z et S pour contrôler la raquette de gauche.
Le starter kit fournis une fonction isKeyDown
, cette fonction permet de vérifier si une touche est enfoncée ou non à un instant T.
Elle prends en paramètre le nom de la touche dont nous voulons connaître l'état, et retourne un booléen: true
si la touche demandée est enfoncée, false
si ce n'est pas le cas.
Dans un navigateur, chaque touche du clavier est identifié par un nom, basé sur les standards américain QWERTY, vous pouvez vous servir de cet utilitaire pour retrouver le nom de la touche à passer en paramètre, il s'agît du e.code
de la touche sur la page de l'utilitaire.
Dans notre cas nous aurons dont besoin de vérifier l'état de ces quatres touches:
ArrowUp
pour Flèche hauteArrowDown
pour Flèche basseKeyW
pour ZKeyS
pour S
Pour déplacer une raquette, il nous suffis simplement d'en modifier les coordonnées, dans la fonction UpdateGame
:
function UpdateGame(deltaTime) {
// Si la touche Flèche haute est enfoncée
if (isKeyDown('ArrowUp')) {
// On retire 2 pixels au positionnement en hauteur
// De la raquette de droite
playerRightPad.y = playerRightPad.y - 2
}
// Si la touche Flèche basse est enfoncée
if (isKeyDown('ArrowDown')) {
// On ajoute 2 pixels au positionnement en hauteur
// De la raquette de droite
playerRightPad.y = playerRightPad.y + 2
}
// Si la touche Z est enfoncée
if (isKeyDown('KeyW')) {
// On retire 2 pixels au positionnement en hauteur
// De la raquette de gauche
playerLeftPad.y = playerLeftPad.y - 2
}
// Si la touche S est enfoncée
if (isKeyDown('KeyS')) {
// On ajoute 2 pixels au positionnement en hauteur
// De la raquette de gauche
playerLeftPad.y = playerLeftPad.y + 2
}
}
La fonction UpdateGame
étant appelée perpétuellement avant chaque appel de la fonction DrawGame
, toute modification des positions des raquettes est directement reportée à l'écran, comme vous pouvez le constater dans la démo suivante.
Vélocité, mouvement automatique de la balle
La balle elle n'est contrôlée par aucun des deux joueurs, nous utiliserons donc un système de vélocité pour la déplacer automatiquement.
Le système de vélocité consiste à définir une donnée de "vitesse" à un élément du jeu et à mettre à jour perpétuellement la position de l'élément par rapport à cette vitesse. Si la vitesse est de 0, alors l'élément ne bouges pas, sinon il se déplace.
Dans un environnement en deux dimensions, nous aurons besoin de deux vélocités pour notre balle: une vélocité horizontale, et une vélocité verticale. Ajoutons donc ces deux propriétés à notre objet ball
, toutes deux initialisées à 0
:
const ball = {
x: 0,
y: 0,
// Vélocité horizontale
vx: 0,
// Vélocité verticale
vy: 0,
width: 16,
height: 16
}
Ensuite dans la fonction UpdateGame
ajoutons le déplacement par sa vélocité à notre balle. Ces instructions doivent être exécutées perpétuellement, aucune condition n'est donc requise:
// Déplace la balle sur l'axe horizontale
// selon sa vélocité horizontale actuelle
ball.x = ball.x + ball.vx
// Idem sur l'axe verticale
ball.y = ball.y + ball.vy
Pour l'instant, les vélocités de la balle sont à 0
, la balle est donc immobile. Dans la fonction LoadGame
vous pouvez essayer de modifier ces vélocités pour comprendre cette mécanique.
Ainsi si vous mettez à la balle une vélocité horizontale positive, elle se déplacera vers la droite, si elle est négative, elle se déplacera vers la gauche. De même pour la vélocité verticale: si elle est positive, la balle se déplacera vers le bas, et si elle est négative, vers le haut.
Si les deux vélocités sont différentes de 0
, alors la balle prendra une direction oblique.
Dans notre cas, nous ferons prendre à la balle une direction en diagonale bas droite en indiquant une vélocité horizontale et verticale égales à 2
:
ball.vx = 2
ball.vy = 2
Désormais si vous rafraichissez votre navigateur, votre balle entre en mouvement. Mais un problème se pose: notre balle sort de l'écran!
Ce comportement est tout à fait normal puisque nous n'avons jusqu'à présent.
Gardez à l'esprit durant tout développement de jeu, que le jeu ne fera que ce que vous lui demandez de faire, dans notre cas, nous avons indiqué à la balle de se déplacer perpétuellement sur une diagonale bas/droite.
Pour empêcher la balle de sortir de notre écran, il nous faut donc coder un inversement de la vélocité verticale de la balle lorsque celle-ci sort de l'écran.
Dans notre fonction UpdateGame
, nous intégrons donc cette condition:
// Si la position verticale de la balle
// Est supérieure à la hauteur de l'écran
if (ball.y >= screen.height) {
// On inverse la vélocité verticale
ball.vy = -ball.vy
}
Notez ici que nous assignons directement ici une nouvelle valeur à la donnée vy
, qui n'est autre que son contraire mathématique:
- Si la valeur de
vy
est2
, alors elle sera désormais de-2
- Si la valeur de
vy
est-2
, alors elle sera désormais de--2
, c'est à dire2
Notre condition est imparfaite, puisque vous constatez que notre balle sort de l'écran avant d'effectuer son rebond. Une fois de plus, il s'agît simplement du principe selon lequel fillRect
dessine la balle depuis sa coordonnées y
correspondant à son bord haut.
De même si vous modifiez dans LoadGame
la vélocité verticale initiale de la balle en négatif pour la faire aller vers le haut, vous constaterez qu'il nous manque la détection du rebond sur le bord haut de l'écran.
Modifions donc notre condition pour y ajouter la collision haute et la hauteur de la balle sur la collision basse:
// Si la position verticale de la balle + sa hauteur
// Est supéreieur à la hauteur de l'écran
// OU si la position verticale de la balle
// est inférieure à 0 (bord haut de l'écran)
if (ball.y + ball.height >= screen.height || ball.y <= 0) {
ball.vy = -ball.vy
}
Désormais notre balle rebondis sur les bord haut et bas de l'écran de jeu, la preuve en démo.
Collision AABB entre la balle et les raquettes
Dans le cas la collision entre la balle et les raquettes, la détection de la collision est légèrement différentes, puisque nous ne devont pas détecter la collision sur un seule axe, mais sur les 4 côtés de la balle.
Vous retrouverez le concept de la collision AABB que nous utiliserons ici au travers de la fonction aabb
fournie dans le starter-kit.