Un Jeu De Casse-briques En Lua Avec Love2D - Zeste De Savoir

C’est bien sympa, mais vous admettrez qu’on s’ennuie un peu pour l’instant, non ? Qu’à cela ne tienne, nous allons rajouter un système de vie et une balle !

Les vies

Attaquons avec les vies. Pour notre jeu, le joueur en aura trois au départ et nous afficherons celles-ci en bas à gauche de la fenêtre.

Voici l’image que nous utiliserons. Enregistrez la sous le nom « life.png », dans le même répertoire que l’icône.

Image d'une vie.
Image d’une vie.

Déclaration et initialisation

Une fois n’est pas coutume, nous allons utiliser une table pour gérer les vies :

local lives -- Déclaration variable pour les vies

Et nous allons initialiser cette table avec une fonction appelée dans love.load :

function initializeLives() lives = {} -- Initialisation variable pour les vies lives.count = NB_LIVES -- Nombre de vie lives.img = love.graphics.newImage(PATH_LIFE) -- Image vie lives.width, lives.height = lives.img:getDimensions() -- Dimensions de l'image end

Comme vous pouvez le voir, nous chargeons l’image en faisant appel à love.graphics.newImage avec le chemin de celle-ci en paramètre (si vous avez fait attention, nous avions déjà utilisée cette fonction pour l’icône). De plus, nous nous servons de notre image en faisant appel à getDimensions afin de récupérer la largeur et la hauteur de celle-ci.

Par ailleurs, comme vous le devinez, il faut ajouter quelques constantes au fichier constants.lua pour que ça fonctionne :

NB_LIVES = 3 -- Nombre de vies initiales PATH_LIFE = "images/life.png" -- Chemin image vie

Affichage

Désormais, il ne reste plus qu’à dessiner chaque vie dans love.draw. Dans ce but, nous allons utiliser la fonction love.graphics.draw en lui passant l’image à dessiner ainsi que la position en abscisse et la position en ordonnée :

for i=0, lives.count-1 do -- Pour chaque vie local posX = 5 + i * 1.20 * lives.width -- Calcul de la position en abscisse love.graphics.draw(lives.img, posX, WIN_HEIGHT-lives.height) -- Affichage de l'image end

Si tout se passe bien, vous verrez les vies apparaître comme désiré :

Les vies.
Les vies.

La balle

Il ne nous reste plus que la balle pour que le jeu soit jouable. Celle-ci sera carrée, apparaîtra juste au dessus du centre de la raquette et aura une direction initiale aléatoire.

Déclaration et initialisation

Vous commencez à être rodé avec le système : nous allons créer une table spécifique et nous allons initialiser celle-ci au chargement du jeu :

local ball -- Déclaration variable pour la balle

Mais contrairement à d’habitude, nous allons passer deux paramètres à la fonction d’initialisation afin que la balle soit proportionnelle à la raquette et soit placée au dessus :

function initializeBall(racketHeight, racketY) ball = {} -- Initialisation variable pour la balle ball.width, ball.height = racketHeight * 0.75, racketHeight * 0.75 -- Taille ball.speedY = -DEFAULT_SPEED_BY -- Vitesse verticale ball.speedX = math.random(-DEFAULT_SPEED_BX, DEFAULT_SPEED_BX) -- Vitesse horizontale ball.x = WIN_WIDTH / 2 - ball.width / 2 -- Position en abscisse ball.y = racketY - 2 * ball.height - ball.height / 2 -- Position en ordonnée end

De cette manière, si nous changeons la hauteur ou la position en ordonnée de la raquette, la balle sera toujours affichée correctement. D’ailleurs, n’oubliez pas de l’initialiser dans love.load :

initializeBall(racket.height, racket.y)

Ensuite, vous avez dû vous rendre compte qu’il fallait ajouter quelques constantes :

DEFAULT_SPEED_BX = 130 -- Vitesse horizontale DEFAULT_SPEED_BY = 335 -- Vitesse verticale

Et puis, qu’est-ce que ce math.random ?

Eh bien, celui-ci nous sert à choisir aléatoirement une vitesse en abscisse pour la balle dans intervalle [-DEFAULT_SPEED_BX, DEFAULT_SPEED_BX]. Comme Lua charge automatiquement les bibliothèques standards dans l’environnement global, nous n’avons même pas besoin d’importer math avant de l’utiliser.

Enfin, si vous connaissez déjà un peu les générateurs de nombres pseudo-aléatoires, vous savez qu’il nous faut initialiser la graine aléatoire avec une valeur qui ne sera jamais identique, sans quoi la suite de résultats sera toujours la même (par exemple, la première balle partira à gauche, la seconde à droite, etc. à chaque partie) :

math.randomseed(love.timer.getTime()) -- Initialisation de la graine avec un temps en ms

Nous placerons la ligne précédente au début de love.load.

Affichage

L’affichage de la balle est maintenant très simple puisqu’il suffit de dessiner le rectangle adéquate dans love.draw, comme nous avons déjà pu le faire :

love.graphics.rectangle('fill', ball.x, ball.y, ball.width, ball.height) -- Rectangle
Apparition de la balle.
Apparition de la balle.

Déplacement et collisions

Voici sans doute la partie la plus intéressante puisqu’il va y avoir enfin de l’action impliquant des conséquences dans notre jeu. En effet, jusqu’à présent, seule notre raquette bougeait, et encore, il fallait appuyer sur des touches pour cela. Or, la balle va se déplacer toute seule. De plus, elle va entrer en collision avec son environnement.

Afin de tester la collision entre deux rectangles (sans rotation dans le plan), nous utiliserons la fonction ci-dessous tirée d'ici, où il est expliqué qu’elle vérifie s’il n’y a pas du vide autour des 4 côtés du rectangle. S’il n’y a pas que du vide, c’est donc qu’il y a une collision. Comme nous pouvons le voir, les deux premières conditions permettent de tester la collision en abscisse (par la gauche ou par la droite) tandis que les deux secondes s’occupent de la collision en ordonnée (par le haut ou par le bas).

function collideRect(rect1, rect2) if rect1.x < rect2.x + rect2.width and rect1.x + rect1.width > rect2.x and rect1.y < rect2.y + rect2.height and rect1.height + rect1.y > rect2.y then return true end return false end

Vous pouvez d’ores et déjà ajouter cette fonction au fichier constants.lua.

Déplacement

Pour déplacer notre balle, il nous suffit de modifier sa position en fonction du temps écoulé, dans love.update :

ball.x = ball.x + ball.speedX * dt -- Mise à jour position en abscisse de la balle ball.y = ball.y + ball.speedY * dt -- Mise à jour position en ordonnée de la balle

En exécutant vous verrez, ô miracle, la balle se déplacer, et … sortir de la fenêtre. :o

Collisions avec la fenêtre

En fait, à bien y réfléchir, c’est tout à fait normal puisque nous diminuons à chaque rafraîchissement la position en ordonnée de la balle. Du coup, elle finit par passer la bordure du haut. De la même façon, si le déplacement en x est non nul, la balle finira par dépasser par la gauche ou par la droite de la fenêtre.

Vous l’aurez compris, il faut que notre balle puisse rebondir sur les bordures afin de ne pas quitter l’écran. À une exception près : si la balle sort par le bas alors c’est que la raquette ne l’a pas renvoyée, donc nous devons enlever une vie au joueur et réinitialiser la balle.

Il faut donc rajouter ceci à love.update :

if ball.x + ball.width >= WIN_WIDTH then -- Bordure droite ball.speedX = -ball.speedX elseif ball.x <= 0 then -- Bordure gauche ball.speedX = -ball.speedX end if ball.y <= 0 then -- Bordure haut ball.speedY = -ball.speedY elseif ball.y + ball.height >= WIN_HEIGHT then -- Bordure bas lives.count = lives.count - 1 resetBall(racket.y) end

Et voici la fonction pour réinitialiser la balle :

function resetBall(racketY) ball.speedY = -DEFAULT_SPEED_BY -- Vitesse verticale ball.speedX = math.random(-DEFAULT_SPEED_BX, DEFAULT_SPEED_BX) -- Vitesse horizontale ball.x = WIN_WIDTH / 2 - ball.width / 2 -- Position en abscisse ball.y = racketY - 2 * ball.height - ball.height / 2 -- Position en ordonnée end

Vous remarquerez qu’elle reprend en partie initializeBall, donc nous pourrions remplacer les lignes redondantes dans cette dernière par un appel à cette fonction, afin d’éviter la duplication de code.

Collisions avec la raquette

Pour tester la collision entre la raquette et la balle, il nous suffit de tester la collision dans love.update en faisant appel à collideRect définie précédemment :

if collideRect(ball, racket) then collisionBallWithRacket() -- Collision entre la balle et la raquette end

Et de changer la direction de la balle en fonction de la collision :

function collisionBallWithRacket() -- Collision par la gauche (coin haut inclus) if ball.x < racket.x + 1/8 * racket.width and ball.speedX >= 0 then if ball.speedX <= DEFAULT_SPEED_BX/2 then -- Si vitesse trop faible ball.speedX = -math.random(0.75*DEFAULT_SPEED_BX, DEFAULT_SPEED_BX) -- Nouvelle vitesse else ball.speedX = -ball.speedX end -- Collision par la droite (coin haut inclus) elseif ball.x > racket.x + 7/8 * racket.width and ball.speedX <= 0 then if ball.speedX >= -DEFAULT_SPEED_BX/2 then -- Si vitesse trop faible ball.speedX = math.random(0.75*DEFAULT_SPEED_BX, DEFAULT_SPEED_BX) -- Nouvelle vitesse else ball.speedX = -ball.speedX end end -- Collision par le haut if ball.y < racket.y and ball.speedY > 0 then ball.speedY = -ball.speedY end end

Vous pouvez remarquez que lors de la collision avec les côtés et les coins du haut de la raquette, nous en profitons pour donner une nouvelle vitesse de déplacement en abscisse à la balle si celle-ci est trop faible. Cela peut donne une impression d’effet et permet surtout d’éviter que la balle fasse du surplace en x.

Collisions avec les briques

Pour la collision avec les briques, nous allons rajouter une variable locale en dessous des autres afin de voir facilement combien de briques il reste, ce qui nous servira par la suite pour tester la fin du jeu.

local nbBricks = BRICKS_PER_COLUMN * BRICKS_PER_LINE -- Nombre de briques

Comme pour la raquette, nous allons devoir tester la collision dans love.update :

for line=#bricks, 1, -1 do for column=#bricks[line], 1, -1 do if bricks[line][column].isNotBroken and collideRect(ball, bricks[line][column]) then collisionBallWithBrick(ball, bricks[line][column]) -- Collision entre la balle et une brique end end end

Et modifier le jeu en fonction (modification de la direction de la balle et suppression de la brique) :

function collisionBallWithBrick(ball, brick) -- Collision côté gauche brique if ball.x < brick.x and ball.speedX > 0 then ball.speedX = -ball.speedX -- Collision côté droit brique elseif ball.x > brick.x + brick.width and ball.speedX < 0 then ball.speedX = -ball.speedX end -- collision haut brique if ball.y < brick.y and ball.speedY > 0 then ball.speedY = -ball.speedY -- Collision bas brique elseif ball.y > brick.y and ball.speedY < 0 then ball.speedY = -ball.speedY end brick.isNotBroken = false -- Brique maintenant cassée nbBricks = nbBricks - 1 -- Ne pas oublier de décrémenter le nombre de briques end

Si tout se passe bien, vous devriez pouvoir vous amusez un peu :

Prêt à tout casser ?
Prêt à tout casser ?

Désormais, nous avons quasiment tout ce qu’il faut pour un jeu fonctionnel ! Il nous reste à tester la fin du jeu, mais avant cela nous allons rajouter quelques sons.

Tag » Code Casse Brique Android