Licence CC BY-NC-SA

Un jeu de casse-briques en Lua avec Love2D

Découverte du célèbre moteur de jeux 2D par la pratique

Dernière mise à jour :
Auteur :
Catégorie :

Vous voulez apprendre à créer des jeux et vous avez entendu parler de Love2D ? Cela tombe bien, car à travers ce tutoriel, nous allons découvrir ce moteur de jeux 2D, multi-plateforme et totalement gratuit, tout en réalisant une implémentation du célèbre casse-briques ! Vous verrez ainsi qu'il est plutôt simple à prendre en main et puissant.

Comme son nom l'indique, le jeu de casse-briques consiste à détruire des briques. Pour cela, le joueur est muni d'une raquette lui permettant de frapper une balle. Lorsque celui-ci rate la balle, il perd une vie. Lorsqu'il n'a plus de vies, il perd la partie. Lorsque la balle entre en collision avec une brique, celle-ci est détruite. Le joueur gagne quand toutes les briques sont détruites.

Notez que ce tutoriel n'est qu'une introduction à ce moteur et ne le présente donc pas en profondeur. Ensuite, celui-ci s'utilisant avec le langage Lua, vous aurez sans doute besoin d'avoir des bases en celui-ci pour suivre sans problème.

Pour installer Love2D si ce n'est pas déjà fait, téléchargez le nécessaire sur cette page. La version actuelle, celle que j'utiliserai, est la 0.10.1. Par ailleurs, je me servirai de l'Environnement de Développement Intégré (EDI) ZeroBrane Studio pour développer. Si vous choisissez de l'utiliser, pensez à changer l'interpréteur pour l'exécution de votre projet (Project > Lua Interpreter > LÖVE).

Enfin, tout au long de votre lecture, n'hésitez pas à tester et à vous reporter à la documentation.

Prérequis
Bases en programmation
Connaissances en Lua (si besoin, référez-vous à ce tutoriel ou encore à celui-ci)

Objectifs
Faire découvrir Love2D
Réaliser un jeu de casse-briques

Première fenêtre

Commençons par créer notre première fenêtre.

Callbacks et squelette de base

Tout d'abord, Love2D contient ce que l'on appelle des fonctions de rappel (callback). Celles-ci sont appelées automatiquement et dans un ordre bien précis lors de l'exécution du jeu, si elles sont définies.

Ainsi, il y a par exemple, la fonction love.load qui est appelée au début et qui a pour but de charger les ressources (images, sons, etc.) ou encore d'initialiser des variables. De même, il existe une fonction love.draw destinée à dessiner à l'écran.

Aussi, certaines d'entre elles peuvent prendre des paramètres, comme la fonction love.update par exemple. Celle-ci prend le temps écoulé depuis le dernier appel à elle : le deltatime. De cette manière, il est possible de mettre à jour un jeu (la position d'un personnage par exemple) indépendamment de la puissance de l'ordinateur (l'affichage ne dépend pas de l'exécution d'une boucle).

Avec celles-ci ainsi que la fonction love.keypressed qui permet de gérer l'appui sur une touche du clavier, nous arrivons à ce squelette de base :

Fichier main.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- pour écrire dans la console au fur et à mesure, facilitant ainsi le débogage
io.stdout:setvbuf('no') 

function love.load()
  -- Fonction pour initialiser le jeu (appelée au début de celui-ci)
end

function love.update(dt)
  -- Fonction pour mettre à jour (appelée à chaque frame)
end

function love.draw()
  -- Fonction pour dessiner (appelée à chaque frame)
end

function love.keypressed(key)
  -- Fonction pour gérer l'appui sur les touches (appelée pour chaque touche pressée)  
end

En exécutant ce code, vous devriez voir votre première fenêtre s'ouvrir ! Certes, il n'y a pas grand chose d'intéressant pour le moment, mais patience !

Si vous cherchez à en savoir un peu plus sur ces fonctions et voir dans quel ordre elles s'organisent, je vous renvoie respectivement à ce lien et à celui-ci.

Configuration

Avant d'aller plus loin, nous allons d'abord configurer un peu notre fenêtre. Nous allons donc lui ajouter un titre ainsi qu'une icône et choisir ses dimensions.

Pour l'icône, enregistrez l'image ci-dessous dans un répertoire « images » sous le nom « icon.png », dans le répertoire de votre projet.

L'icône à télécharger.

Avec des fonctions spécifiques

Pour paramétrer tout ça, nous pouvons utiliser des fonctions du module love.window que vous pouvez explorer ici. Faisons donc appel à celles-ci à l'intérieur de love.load, donc au début de l'exécution de notre jeu :

Fichier main.lua

1
2
3
4
5
6
7
8
function love.load()

  love.window.setTitle("Casse-briques") -- Change le titre de la fenêtre
  local imgIcon = love.graphics.newImage("images/icon.png") -- Chargement de l'image
  love.window.setIcon(imgIcon:getData()) -- Change l'icone de la fenêtre
  love.window.setMode(480, 640) -- Change les dimensions de la fenêtre

end

Comme vous pouvez le constater, nous changeons d'abord le titre avec love.window.setTitle. Ensuite, nous chargeons l'image de notre icône que nous passons au bon format à love.window.setIcon. Enfin, nous terminons en changeant les dimensions de la fenêtre en passant la largeur puis la hauteur de celle-ci à love.window.setMode.

Avec un fichier conf.lua

Alternativement, nous pouvons aussi passer par un fichier conf.lua qui contiendra une fonction love.conf. Comme vous l'avez compris, celle-ci est effectivement une fonction de callback. Elle est appelée au lancement du jeu avant même le love.load et sert à configurer celui-ci à l'aide d'une variable reçue en paramètre.

Ainsi, en l'utilisant, cela donne :

Fichier conf.lua

1
2
3
4
5
6
7
8
function love.conf(t)

  t.window.title = "Casse-briques" -- Change le titre de la fenêtre
  t.window.icon = "images/icon.png" -- Change l'icone de la fenêtre
  t.window.width = 480 -- Change la largeur de la fenêtre
  t.window.height = 640 -- Change la hauteur de la fenêtre

end

Pour en savoir plus sur cette fonction, vous pouvez jeter un coup d’œil ici.

Voilà, à vous de choisir entre ces deux approches. Pour ma part, je préfère la seconde, parce qu'elle est effectuée en première et parce que ça libère de la place dans le love.load.

Constantes

Pour finir, afin d'avoir un code plus modulable, ajoutons un fichier constants.lua qui contiendra les constantes de notre jeu. Voici ce que ça donne :

Fichier constants.lua

1
2
3
4
TITLE = "Casse-briques" -- Titre
PATH_ICON = "images/icon.png" -- Chemin image icône
WIN_WIDTH = 480 -- Largeur fenêtre
WIN_HEIGHT = 640 -- Hauteur fenêtre

Il ne vous reste plus qu'à remplacer les valeurs dans love.load ou love.conf par ces variables après les avoir importées :

1
require('constants')

De la sorte, si nous souhaitons modifier une valeur, nous n'aurons qu'à jeter un coup d’œil au fichier des constantes et non à chercher dans tout le code. Cela est très utile quand la valeur en question apparaît à de nombreuses reprises par exemple.

Au terme de cette section, vous devriez avoir une fenêtre qui ressemble à celle présentée ci-dessous. Pour ma part, je n'ai pas l'icône d'affichée (sans doute parce que je suis sous Ubuntu).

Notre première fenêtre.

Nous allons maintenant ajouter les premiers éléments au jeu.

La raquette et les briques

Oui, il est temps de passer aux choses sérieuses ! Dans cette section, nous allons mettre en place la raquette et les briques. Nous allons donc les afficher et faire bouger la première.

Petit rappel si vous n'êtes pas familier avec les bibliothèques de jeux 2D, sachez que très souvent l'origine du repère des coordonnées se trouve dans le coin haut gauche. De plus, l'axe des abscisses est orienté vers la droite tandis que l'axe des ordonnées est orienté vers le bas. Ainsi, les coordonnées du coin haut gauche sont (0, 0) tandis que celles du coin bas droit sont (largeur, hauteur). Par ailleurs, les éléments sont situés avec la position de leur coin haut gauche. Par exemple, si notre raquette a une position en y élevée, elle sera donc proche du bas de la fenêtre. De même, si sa position en x vaut 0, alors elle touchera le bord gauche de la fenêtre.

Voici un exemple d'illustration d'un rectangle en (posX, posY) pour bien comprendre :

Repère de coordonnées.

La raquette

Démarrons avec la raquette. Celle-ci sera placée vers le bas de la fenêtre. De plus, elle se déplacera latéralement en fonction des touches pressées (vers la gauche avec Q ou <- et vers la droite avec D ou ->).

Déclaration et initialisation

Pour mettre à jour le jeu, il nous faut conserver certaines caractéristiques de la raquette. En effet, nous voulons pouvoir connaître ses dimensions, savoir où elle se trouve à un moment précis ou encore connaître sa vitesse. De même, nous voulons pouvoir modifier aisément ces valeurs si besoin, pour modifier la position de la raquette par exemple. Pour cela, nous allons utiliser une table et l'initialiser avec une fonction de notre cru appelée dans love.load :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
local racket -- Déclaration variable pour la raquette

function initializeRacket()

  racket = {} -- Initialisation variable pour la raquette

  -- Initialisation de paires (clef, valeur) de la table racket
  racket.speedX = 215 -- Vitesse horizontale
  racket.width = WIN_WIDTH / 4 -- Largeur
  racket.height = WIN_HEIGHT / 37 -- Hauteur
  racket.x = (WIN_WIDTH-racket.width) / 2 -- Position en abscisse
  racket.y = WIN_HEIGHT - 64 -- Position en ordonnée

end

function love.load()
  initializeRacket()
end

Comme vous pouvez le voir, nous définissons la taille et la position de notre raquette en fonction des dimensions de la fenêtre. De cette manière, si nous changeons ces dernières dans les constantes, le jeu sera affiché de la même façon. Nous procéderons donc ainsi tout au long de ce tutoriel.

Affichage

La raquette étant créée, nous pouvons la dessiner dans love.draw. Ainsi, nous choisissons d'abord la couleur blanche puis nous dessinons le rectangle correspondant, respectivement avec les fonctions love.graphics.setColor et love.graphics.rectangle. Pour la couleur, nous la passons sous la forme RGB (Red, Green, Blue). Pour le rectangle, nous indiquons en premier paramètre que nous le voulons rempli et ensuite nous passons les coordonnées et les dimensions.

1
2
3
4
function love.draw()
  love.graphics.setColor(255, 255, 255) -- Couleur blanche
  love.graphics.rectangle('fill', racket.x, racket.y, racket.width, racket.height) -- Rectangle
end

Comme vous pourrez le constatez si vous regardez la documentation, le module love.graphics contient de nombreuses fonctions pour dessiner à l'écran.

Eh voilà ! Si vous exécutez le code, vous verrez bien une raquette apparaître à l'écran. Désormais, il ne nous reste plus qu'à la faire bouger.

La raquette.

Déplacement

Pour faire mouvoir notre raquette, nous allons devoir récupérer des événements en provenance du clavier. Là, si vous avez bien suivi, vous penserez sans doute que nous allons utiliser la fonction love.keypressed. Eh bien, que nenni ! En effet, si nous utilisions cette dernière, nous devrions appuyer sans cesse sur les touches pour nous déplacer (par défaut, il n'y pas de fluidité en restant appuyé) et il nous manquerait le fameux deltatime. Pour résoudre cela, nous allons donc compléter notre love.update, car vu que cette fonction est appelée de nombreuses fois par seconde, l'appui sur une touche donnera une impression de fluidité dans le déplacement.

Ainsi, nous testons si les touches que nous voulons sont actuellement pressées avec love.keyboard.isDown du module love.keyboard, et nous modifions la position de la raquette de sorte qu'elle ne sorte pas de l'écran :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function love.update(dt)

  -- Mouvement vers la gauche
  if love.keyboard.isDown('left', 'q') and racket.x > 0 then
      racket.x = racket.x - (racket.speedX*dt)
  -- Mouvement vers la droite
  elseif love.keyboard.isDown('right', 'd') and racket.x + racket.width < WIN_WIDTH then
      racket.x = racket.x + (racket.speedX*dt)
  end

end

Ça y est, notre raquette est opérationnelle.

Les briques

Au tour des briques maintenant. Nous les placerons en haut de notre fenêtre.

Déclaration et initialisation

Comme pour la raquette, les briques ont certaines caractéristiques que nous devons stocker et comme pour la raquette nous allons utiliser une table ainsi qu'une fonction pour initialiser celle-ci.

Ajoutons donc la ligne suivante en dessous de la déclaration de racket :

1
local bricks -- Déclaration variable pour les briques

Créons aussi deux constantes dans le fichier constants.lua pour qu'il soit facile de faire varier le nombre de briques :

1
2
BRICKS_PER_LINE = 7 -- Nombre de briques par ligne
BRICKS_PER_COLUMN = 6 -- Nombre de briques par colonne

Puis initialisons les briques :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createBrick(line, column)

  -- Fonction pour créer une brique et l'initialiser en fonction de sa position dans le mur
  local brick = {}
  brick.isNotBroken = true -- Brique pas encore cassée
  brick.width = WIN_WIDTH / BRICKS_PER_LINE - 5 -- Largeur
  brick.height = WIN_HEIGHT / 35 -- Hauteur
  brick.x = 2.5 + (column-1) * (5+brick.width) -- Position en abscisse
  brick.y = line * (WIN_HEIGHT/35+2.5) -- Position en ordonnée
  return brick

end

function initializeBricks()

    bricks = {} -- Initialisation variable pour les briques
    for line=1, BRICKS_PER_COLUMN do
      table.insert(bricks, {}) -- Ajout d'une ligne
      for column=1, BRICKS_PER_LINE do
        local brick = createBrick(line, column)
        table.insert(bricks[line], brick) -- Ajout d'une brique par colonne de la ligne
      end
    end

end

Comme vous pouvez le voir, nous avons une fonction permettant de créer une brique qui sera appelée lors de l'initialisation pour chaque emplacement du mur de briques.

N'oubliez pas de faire appel à initializeBricks dans love.load sans quoi la table des briques ne sera pas créée.

Affichage

Enfin, il ne nous reste plus qu'à dessiner nos briques dans love.draw avec des rectangles comme nous l'avons fait pour la raquette. Pour cela, nous parcourons notre tableau à deux dimensions et nous faisons appel à love.graphics.rectangle pour chaque brique non cassée.

1
2
3
4
5
6
7
8
for line=1, #bricks do -- Ligne
  for column=1, #bricks[line] do -- Colonne
    local brick = bricks[line][column]
    if brick.isNotBroken then -- Si la brique n'est pas cassée
      love.graphics.rectangle('fill', brick.x, brick.y, brick.width, brick.height) -- Rectangle
    end
  end
end

Comme convenu, nos briques s'affichent à l'exécution :

Les briques sont là !

Le jeu commence à prendre forme, n'est-ce pas ? Toutefois, ne nous arrêtons pas en si bon chemin !

Les vies et la balle

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.

Déclaration et initialisation

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

1
local lives -- Déclaration variable pour les vies

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

1
2
3
4
5
6
7
8
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 :

1
2
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 :

1
2
3
4
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.

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 :

1
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 :

1
initializeBall(racket.height, racket.y)

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

1
2
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) :

1
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 :

1
love.graphics.rectangle('fill', ball.x, ball.y, ball.width, ball.height) -- Rectangle

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). Pour vous approprier le fonctionnement du code, vous pouvez l'essayer ici.

1
2
3
4
5
6
7
8
9
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 :

1
2
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 :

1
2
3
4
5
6
7
8
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 :

1
2
3
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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.

1
local nbBricks = BRICKS_PER_COLUMN * BRICKS_PER_LINE -- Nombre de briques

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

1
2
3
4
5
6
7
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) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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 ?

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.

Les sons

Le module love.audio va nous permettre de mettre en place les sons. Avant cela, je vous laisse télécharger ce son que nous jouerons lorsque la balle touchera une brique ainsi que celui que nous jouerons lorsque la raquette frappera la balle. Placez les dans un répertoire « sounds » à l'intérieur de votre projet.

Pour information, ceux-ci ont été réalisés avec Bfxr, un outil de génération de sons bien pratique.

Charger les sons

La fonction love.audio.newSource permet de charger un son en lui spécifiant son chemin. Par ailleurs, en précisant "static" en second paramètre, nous pouvons indiquer que nous voulons charger le son directement en mémoire. Dans le cas contraire, le fichier sera chargé au fur et à mesure de la lecture. Pour un fichier plutôt lourd telle qu'une musique, il est conseillée d'opter pour cette seconde option, de sorte à ne pas encombrer la RAM.

Commençons par déclarer deux variables locales au début du code :

1
2
local soundBrick -- Déclaration variable son brique
local soundRacket -- Déclaration variable son raquette

Puis chargeons nos sons dans love.load :

1
2
soundBrick = love.audio.newSource(PATH_SOUND_BRICK, "static") -- Chargement son brique
soundRacket = love.audio.newSource(PATH_SOUND_RACKET, "static") -- Chargement son raquette

Bien sûr, il nous faut créer les constantes dans le fichier constants.lua pour que cela fonctionne :

1
2
PATH_SOUND_BRICK = "sounds/collision_brick.wav" -- Chemin son brique
PATH_SOUND_RACKET = "sounds/collision_racket.wav" -- Chemin son raquette
Jouer les sons

Pour jouer un son, il suffit de s'en servir en faisant appel à play. Ainsi, pour jouer le son de la collision entre la balle et la raquette dans collisionBallWithRacket et pour jouer le son de la collision entre la balle et une brique dans collisionBallWithBrick, nous mettrons respectivement au début de ces fonctions :

1
soundRacket:play() -- Joue le son raquette
1
soundBrick:play() -- Joue le son brique

Alors, c'est tout de suite plus vivant avec un peu de son, non ?

Le menu

À travers cette section, nous allons ajouter un menu et enfin achever le jeu !

Pour commencer, nous allons organiser ce dernier en trois pages :

  • une page de début s'affichant avant de lancer la partie ;
  • une page de partie ;
  • une page de fin s'affichant une fois la partie terminée.

Rajoutons donc trois constantes dans constants.lua :

1
2
3
PAGE_BEGINNING = 1 -- Page de début
PAGE_ROUND = 2 -- Page de partie
PAGE_END = 3 -- Page de fin

Et créons une variable locale dans main.lua pour garder la page courante :

1
currentPage = PAGE_BEGINNING -- Page courante

Enfin, il nous faut réorganiser notre love.draw et notre love.update pour n'afficher et ne mettre à jour que la page courante (il ne faudrait pas que notre partie s'affiche ou se déroule alors que nous sommes sur la page d'accueil par exemple). Cela se fait bêtement de la sorte :

1
2
3
4
5
6
7
if currentPage == PAGE_BEGINNING then
  -- Traitement page début
elseif currentPage == PAGE_ROUND then
  -- Traitement page partie : placer le code déjà présent ici.
elseif currentPage == PAGE_END then
  -- Traitement page fin
end

Au passage, créons une fonction drawRound ainsi qu'une fonction updateRound qui contiendront respectivement le code pour afficher et mettre à jour la partie. Nous allons appeler celles-ci respectivement dans love.draw et love.update quand la page courante vaut PAGE_ROUND. De cette manière, nous factorisons le code et, surtout, gagnons en visibilité.

N'oubliez pas de passer le deltatime en paramètre à updateRound, sans quoi la partie ne pourra pas se mettre à jour.

Première page

Notre première page affichera le titre et indiquera comment lancer la partie. Pour cela, nous allons créer une police d'écriture et écrire dans la fenêtre. Tout d'abord, déclarons une variable locale au début :

1
local font -- Déclaration variable pour la police d'écriture

Puis initialisons la dans love.load en appelant love.graphics.newFont avec une taille en paramètre :

1
font = love.graphics.newFont(32) -- Initialise font avec une police de taille 32

De plus, pour se servir d'une de nos polices, nous devons l'indiquer avec love.graphics.setFont. Vu que nous n'utiliserons que celle-ci, nous pouvons donc ajouter la ligne suivante à la suite :

1
love.graphics.setFont(font) -- Définit font comme la police utilisée

Maintenant il ne reste plus qu'à écrire dans love.draw en utilisant la fonction love.graphics.printf. Celle-ci prend en paramètre le texte à écrire, la position en abscisse puis la position en ordonnée de celui-ci ainsi que la largeur de la boîte dans laquelle sera écrit le texte. Si cette dernière est plus petite que la longueur nécessaire pour écrire le texte, alors il sera écrit sur plusieurs lignes. Par ailleurs, nous pouvons indiquer un alignement dans cette boîte en dernier paramètre. Ainsi, nous allons centrer nos textes en largeur de la sorte :

1
2
love.graphics.printf("Casse-briques", 0, 0.25*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture
love.graphics.printf("Appuyez sur 'R' pour commencer", 0, 0.45*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture

Cela donne :

La page du début.

Vous remarquerez que le second texte, prenant plus de taille en largeur que possible, est écrit automatiquement sur plusieurs lignes, comme convenu.

De plus, celui-ci indique qu'il faudra presser R pour lancer une partie. Par ailleurs, cette même touche permettra de redémarrer une partie. Ainsi, si nous sommes sur la page du début, nous avons juste à passer sur la page de jeu, sinon nous réinitialisons les variables. Ainsi, voici ce que nous allons ajouter à love.keypressed :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if key == "r" then
  if currentPage ~= PAGE_BEGINNING then

    resetRacket() -- Réinitialisation de la raquette

    -- Réinitialisation des briques
    for line=1, #bricks do
      for column=1, #bricks[line] do
        bricks[line][column].isNotBroken = true
      end
    end

    lives.count = NB_LIVES -- Réinitialisation des vies
    nbBricks = BRICKS_PER_COLUMN * BRICKS_PER_LINE -- Réinitialisation du nombre de briques
    resetBall(racket.y) -- Réinitialisation de la balle

  end
  currentPage = PAGE_ROUND -- Page jeu
end

Comme vous pouvez le voir, nous réajustons juste les valeurs de nos tables au lieu d'en créer de nouvelles. Même s'il n'y a pas de soucis de performance à avoir au vu de la puissance des machines et des outils, je trouve plus propre de procéder ainsi.

Pour que ça fonctionne, il ne faut pas oublier de définir resetRacket qui permet de repositionner la raquette :

1
2
3
4
function resetRacket()
  racket.x = (WIN_WIDTH-racket.width) / 2 -- Position en abscisse
  racket.y = WIN_HEIGHT - 64 -- Position en ordonnée
end

Comme pour resetBall et initializeBall, nous avons ici un code dupliqué. Il est donc préférable de faire appel à resetRacket dans initializeRacket.

Quitter

Puisque jusqu'à présent nous sommes obligés de cliquer sur la croix rouge pour quitter le jeu, nous allons permettre de le faire en appuyant sur Échap. Comme vous vous en douter, nous allons donc ajouter quelques lignes à love.keypressed :

1
2
3
if key == "escape" then
  love.event.quit() -- Pour quitter le jeu
end

Page de fin

Pour le moment, notre jeu est fonctionnel, mais il ne se termine jamais. Nous allons donc faire en sorte qu'à la fin d'une partie, celui-ci s'arrête et qu'une page de fin s'affiche. Ainsi, nous allons tester l'état de la partie à la fin de notre updateRound et changer de page si besoin de cette manière :

1
2
3
if lives.count == 0 or nbBricks == 0 then
  currentPage = PAGE_END -- Page de fin
end

Maintenant nous n'avons plus qu'à afficher le contenu de la page de fin en fonction de si c'est une victoire ou une défaite dans love.draw :

1
2
3
4
5
6
local message = "Victoire !"
if lives.count == 0 then
  message = "Défaite !"
end
love.graphics.printf(message, 0, 0.25*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture
love.graphics.printf("Appuyez sur 'R' pour recommencer", 0, 0.45*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture

Le résultat :

La page de fin.

Voilà, notre jeu est désormais doté d'un joli menu !

Version finale et distribution

Notre casse-briques étant terminé, faisons un récapitulatif de l'organisation du projet, des sources ainsi que des améliorations possibles tout en voyant comment le distribuer :

Organisation et sources

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
casse_briques
    | images
        | --- icon.png
        | --- life.png
    | sounds
        | --- collision_brick.wav
        | --- collision_racket.wav
    | --- conf.lua
    | --- constants.lua
    | --- main.lua

Fichier constants.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
TITLE = "Casse-briques" -- Titre
PATH_ICON = "images/icon.png" -- Chemin image icône
WIN_WIDTH = 480 -- Largeur fenêtre
WIN_HEIGHT = 640 -- Hauteur fenêtre

BRICKS_PER_LINE = 7 -- Nombre de briques par ligne
BRICKS_PER_COLUMN = 6 -- Nombre de briques par colonne

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

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

PATH_SOUND_BRICK = "sounds/collision_brick.wav" -- Chemin son brique
PATH_SOUND_RACKET = "sounds/collision_racket.wav" -- Chemin son raquette

PAGE_BEGINNING = 1 -- Page de début
PAGE_ROUND = 2 -- Page de partie
PAGE_END = 3 -- Page de fin

-- Fonction pour tester la collision entre deux rectangles
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

Fichier conf.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
require('constants')

function love.conf(t)

  t.window.title = TITLE -- Change le titre de la fenêtre
  t.window.icon = PATH_ICON -- Change l'icone de la fenêtre
  t.window.width = WIN_WIDTH -- Change la largeur de la fenêtre
  t.window.height = WIN_HEIGHT -- Change la hauteur de la fenêtre

end

Fichier main.lua

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
require('constants')

-- pour écrire dans la console au fur et à mesure, facilitant ainsi le débogage
io.stdout:setvbuf('no')

-- [[ Variables locales ]]

local racket -- Déclaration variable pour la raquette
local bricks -- Déclaration variable pour les briques
local lives -- Déclaration variable pour les vies
local ball -- Déclaration variable pour la balle

local nbBricks = BRICKS_PER_COLUMN * BRICKS_PER_LINE -- Nombre de briques
local currentPage = PAGE_BEGINNING -- Page courante

local soundBrick -- Déclaration variable son brique
local soundRacket -- Déclaration variable son raquette
local font -- Déclaration variable pour la police d'écriture

--[[ Fonctions raquette ]]

function initializeRacket()

  racket = {} -- Initialisation variable pour la raquette

  -- Initialisation de paires (clef, valeur) de la table racket
  racket.speedX = 215 -- Vitesse horizontale
  racket.width = WIN_WIDTH / 4 -- Largeur
  racket.height = WIN_HEIGHT / 37 -- Hauteur
  resetRacket() -- Position

end


function resetRacket()

  racket.x = (WIN_WIDTH-racket.width) / 2 -- Position en abscisse
  racket.y = WIN_HEIGHT - 64 -- Position en ordonnée

end

-- [[ Fonctions briques ]]

function createBrick(line, column)

  -- Fonction pour créer une brique et l'initialiser en fonction de sa position dans le mur
  local brick = {}
  brick.isNotBroken = true -- Brique pas encore cassée
  brick.width = WIN_WIDTH / BRICKS_PER_LINE - 5 -- Largeur
  brick.height = WIN_HEIGHT / 35 -- Hauteur
  brick.x = 2.5 + (column-1) * (5+brick.width) -- Position en abscisse
  brick.y = line * (WIN_HEIGHT/35+2.5) -- Position en ordonnée
  return brick

end

function initializeBricks()

    bricks = {} -- Initialisation variable pour les briques
    for line=1, BRICKS_PER_COLUMN do
      table.insert(bricks, {}) -- Ajout d'une ligne
      for column=1, BRICKS_PER_LINE do
        local brick = createBrick(line, column)
        table.insert(bricks[line], brick) -- Ajout d'une brique par colonne de la ligne
      end
    end

end

-- [[ Fonction vie ]]

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

-- [[ Fonctions balle ]]

function initializeBall(racketHeight, racketY)

  ball = {} -- Initialisation variable pour la balle
  ball.width, ball.height = racketHeight * 0.75, racketHeight * 0.75  -- Taille
  resetBall(racketY)

end


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


function collisionBallWithRacket()

    soundRacket:play() -- Joue le son raquette

    -- 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


function collisionBallWithBrick(ball, brick)

  soundBrick:play() -- Joue le son brique

  -- 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

-- [[ Fonctions de la page partie ]]

function updateRound(dt)

  -- Mouvement vers la gauche
  if love.keyboard.isDown('left', 'q') and racket.x > 0 then
      racket.x = racket.x - (racket.speedX*dt)
  -- Mouvement vers la droite
  elseif love.keyboard.isDown('right', 'd') and racket.x + racket.width < WIN_WIDTH then
      racket.x = racket.x + (racket.speedX*dt)
  end

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

  -- Collision de la balle avec la fenêtre
  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

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

  -- Collision de la balle avec les briques
  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

  -- Test de l'état de la partie
  if lives.count == 0 or nbBricks == 0 then
    currentPage = PAGE_END -- Page de fin
  end
end


function drawRound()

  love.graphics.setColor(255, 255, 255) -- Couleur blanche

  -- Affichage de raquette
  love.graphics.rectangle('fill', racket.x, racket.y, racket.width, racket.height)

  -- Affichage des briques
  for line=1, #bricks do -- Ligne
    for column=1, #bricks[line] do -- Colonne
      local brick = bricks[line][column]
      if brick.isNotBroken then -- Si la brique n'est pas cassée
        love.graphics.rectangle('fill', brick.x, brick.y, brick.width, brick.height)
      end
    end
  end

  -- Affichage des vies
  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

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

end

-- [[ Fonctions Callback de Love2D ]]

function love.load()

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

  initializeRacket()
  initializeBricks()
  initializeLives()
  initializeBall(racket.height, racket.y)

  soundBrick = love.audio.newSource(PATH_SOUND_BRICK, "static") -- Chargement son brique
  soundRacket = love.audio.newSource(PATH_SOUND_RACKET, "static") -- Chargement son raquette

  font = love.graphics.newFont(32) -- Initialise font avec une police de taille 32
  love.graphics.setFont(font) -- Définit font comme la police utilisée

end


function love.update(dt)

  if currentPage == PAGE_BEGINNING then
    -- Traitement page début
  elseif currentPage == PAGE_ROUND then
    updateRound(dt)
  elseif currentPage == PAGE_END then
    -- Traitement page fin
  end

end

function love.draw()

  if currentPage == PAGE_BEGINNING then

    love.graphics.printf("Casse-briques", 0, 0.25*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture
    love.graphics.printf("Appuyez sur 'R' pour commencer", 0, 0.45*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture

  elseif currentPage == PAGE_ROUND then

    drawRound()

  elseif currentPage == PAGE_END then

    local message = "Victoire !"
    if lives.count == 0 then
      message = "Défaite !"
    end
    love.graphics.printf(message, 0, 0.25*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture
    love.graphics.printf("Appuyez sur 'R' pour recommencer", 0, 0.45*WIN_HEIGHT, WIN_WIDTH, "center") -- Écriture

  end

end


function love.keypressed(key)

  if key == "r" then
    if currentPage ~= PAGE_BEGINNING then

      resetRacket() -- Réinitialisation de la raquette

      -- Réinitialisation des briques
      for line=1, #bricks do
        for column=1, #bricks[line] do
          bricks[line][column].isNotBroken = true
        end
      end

      lives.count = NB_LIVES -- Réinitialisation des vies
      nbBricks = BRICKS_PER_COLUMN * BRICKS_PER_LINE -- Réinitialisation du nombre de briques
      resetBall(racket.y) -- Réinitialisation de la balle

    end
    currentPage = PAGE_ROUND -- Page jeu

  end

  if key == "escape" then
    love.event.quit() -- Pour quitter le jeu
  end

end

Distribution

Maintenant que nous disposons d'une première version jouable de notre jeu, il peut être sympa d'en faire profiter les autres et de savoir ce qu'ils en pensent. En effet, quoi de plus normal que de vouloir que l'on joue à notre jeu et l'améliorer ? Avec Love2D, il existe plusieurs manières de distribuer son jeu.

En créant un fichier « .love »

La plus simple consiste à créer un fichier « .love » qui pourra être exécuté sur les machines ayant une version compatible de Love2D d'installée. Pour cela, il faut d'abord se placer dans le répertoire contenant tout notre jeu (code et ressources) et s'assurer que le fichier main.lua se trouve à la racine de celui-ci, sans quoi cela ne fonctionnera pas. Comme c'est bon dans notre cas, nous pouvons sélectionner tout le contenu puis le compresser en un fichier « .zip » (clique droit puis compresser sous Ubuntu). Une fois cela fait, il ne reste plus qu'à renommer le fichier en remplaçant l'extension par « love ». L'icône change et il est alors possible d'exécuter le jeu en double-cliquant sur le fichier.

En créant un fichier selon la plateforme visée

Pour distribuer notre jeu à des utilisateurs n'ayant pas forcément Love2D d'installé, il va falloir faire en fonction de la plateforme visée. Par exemple, il est possible de viser les utilisateurs de Windows, de Mac OS X ou encore d'Android. Nous ne traiterons que de la façon de faire un exécutable pour Windows dans ce tutoriel. Pour le reste, je vous laisse vous renseigner avec la documentation.

Pour faire un fichier « .exe », nous allons avoir besoin du fichier « .love » créé précédemment ainsi que de certains fichiers de Love2D. Ceux-ci sont téléchargeables sur la page principale, en prenant la version zippée 32 ou 64 bits.

Pour choisir la version adéquate à télécharger, sachez qu'un exécutable 32 bits pourra être exécuté sur une architecture 32 et 64 bits alors qu'un exécutable 64 bits ne sera utilisable qu'avec une architecture 64 bits. Ainsi, si vous souhaitez que votre exécutable soit utilisable sur n'importe laquelle de ces deux architectures, vous pouvez soit créer un exécutable 32 bits ou soit créer un exécutable pour chaque architecture.

Après avoir téléchargé le fichier compressé, Il nous faut en extraire le contenu puis copier-coller celui-ci dans un répertoire qui contiendra notre jeu exécutable. Ajoutons-y le fichier « .love » que l'on nommera casse_briques.love pour l'occasion, ainsi que les ressources. Ensuite, il ne reste plus qu'à créer l'exécutable à partir du fichier casse_briques.love et du love.exe. Sous Linux et OS X, nous entrerons cat love.exe casse_briques.love > casse_briques.exe dans le terminal tandis que sous Windows il faudra saisir copy /b love.exe+casse_briques.love casse_briques.exe en ligne de commandes (assurez-vous d'être dans le bon répertoire sinon ces commandes ne fonctionneront pas). En double-cliquant sur l'exécutable créé, le jeu doit se lancer. Si ce n'est pas le cas, assurez-vous que votre répertoire contienne à minima les fichiers suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
casse_briques_exe
    | images
        | --- icon.png
        | --- life.png
    | sounds
        | --- collision_brick.wav
        | --- collision_racket.wav
    | --- casse_briques.exe
    | --- license.txt (il est obligatoire d'inclure la licence de Love2D)
    | --- love.dll
    | --- lua51.dll
    | --- mpg123.dll
    | --- msvcp120.dll
    | --- msvcr120.dll
    | --- OpenAL32.dll
    | --- SDL2.dll

Améliorations possibles

Bien sûr, notre jeu pourrait être amélioré de multiples façons. Je vous donne ici quelques idées pour vous entraîner si vous le souhaitez.

Déjà, vous pouvez réorganiser le code et le rendre plus modulable par exemple en rajoutant des fonctions (une fonction pour dessiner les briques par exemple ou encore une autre pour gérer le déplacement de la raquette) ou en créant de nouvelles constantes (pour la vitesse de la raquette par exemple) ou encore en séparant le contenu de main.lua en plusieurs fichiers.

Ensuite, vous pouvez aussi essayer d'ajouter des couleurs aux éléments (en ajoutant une valeur couleur générée aléatoirement par exemple). Et puis, il serait possible de rendre certaines briques plus résistantes que d'autres (en rajoutant une valeur qui diminuerait de 1 à chaque collision avec la balle par exemple et une fois à 0 la brique serait considérée comme cassée). Plus compliqué, vous pourriez faire que la balle soit ronde (il faudrait alors dessiner un cercle et non plus un rectangle, ainsi que passer par une autre fonction de collision). Par ailleurs, il serait intéressant d'ajouter des bonus qui apparaîtraient lors de la destruction d'une brique et qu'il faudrait récupérer avec la raquette.

Enfin, sachez que des casse-briques fonctionnent en donnant à la balle non pas des vitesses de déplacement horizontal et vertical, mais directement un angle. Cela fait appel à des notions de trigonométrie et ça ne peut être qu'enrichissant à mettre en œuvre.

Bref, les idées ne manquent pas ! Si ça vous intéresse, vous avez donc de quoi vous occuper un moment.

Avant de conclure, j'ai moi-même développé une version avec plus de fonctionnalités (dont le code est un peu différent), consultable ici. Si vous êtes vraiment bloqué ou curieux, ça peut vous servir.


Au terme de ce tutoriel, vous venez de découvrir Love2D et peut-être de réaliser votre premier jeu avec.

Sachez que nous avons seulement utilisé une infime partie de ce moteur. Si vous souhaitez en apprendre davantage, je vous renvoie à la documentation. Par ailleurs, si vous souhaitez pratiquer davantage, je vous recommande ce tutoriel.

À bientôt ! :)

Merci à Gabbro pour ses retours et à nohar pour la validation.

Les images du jeu, réalisées avec Paint, ainsi que les sons, générés avec Bfxr, sont sous licence CC0.

14 commentaires

Merci à toi. :)

Pour répondre à ta question, j'ai actuellement quelques tutoriels à peaufiner (celui-ci vu que je compte expliquer comment faire un .love et un .exe, le guide du contributeur à terminer, sqlite3 qui est en validation, et celui sur turtle dont je veux corriger légèrement la forme). Donc là, je suis plus dans une phase de finition. Après j'ai pas mal d'idées, mais j'ai aussi d'autres activités que je dois prendre compte (études et sport notamment).

Tu serais intéressé par un sujet en particulier ?

Merci. :)

Alors mon but n'était pas d'enseigner les bases de Lua, mais de me concentrer autour de la découverte de Love2D, c'est pourquoi j'ai passé outre cela et suggéré deux liens en pré-requis, pour ceux qui ne maîtriseraient pas suffisamment le langage. Par contre, je pense que ce serait bien d'avoir un tutoriel Lua sur ZdS (j'ai vu qu'il y avait deux ou trois initiatives jusqu'à présent, mais sans aboutissement) ; et puis ça serait dans l'optique des contenus complémentaires.

D'ailleurs, comme tu baignes à la fois dans Lua et dans Love2D avec ton jeu UnviPlanet, n'hésite pas à écrire un article ou un tutoriel autour de cela (système de tile, génération de terrain avec le perlin noise, shader, etc. [les idées ne manqueraient pas vu l'avancement de ton jeu]) si ça t'intéresse !

Merci à toi. :)

Je compte bien faire d'autres tutoriels (j'ai déjà des idées), mais pour le moment j'ai pas mal de travail pour les études et c'est prioritaire. La bonne nouvelle c'est que j'ai bientôt fini ma vague de contenus actuelle (plus que le guide du contributeur à finir), donc je pourrai m'y atteler librement.

Pour information, quels types de tutoriels t'intéresseraient ?

Édité par Smokiev

D'accord.

Dans l'immédiat, je peux te proposer les tutoriels de la documentation officiel vu que quelques uns traitent des tiles (c'est de l'anglais par contre). Aussi, pour te simplifier la vie, il est possible de combiner Lua à Tiled, un logiciel facilitant la création de cartes avec des tuiles.

Après, je ne sais pas si je pourrai en parler dans le contenu que je souhaite rédiger, qui est plus général, je vais voir. Peut-être ferais-je un billet rapide lorsque les tribunes libres seront là. :)

Salut Magic50,

Eh bien pour te dire, j'ai commencé un tutoriel général et plus détaillé sur Love2D qui couvrira pas mal de points du moteur tout en proposant des exercices pour pratiquer. J'ai par ailleurs envie d'inclure trois (ou quatre ?) jeux à coder : un tic tac toe, un shooter et un flappy bird like, qui se placeront comme des sortes de palier. Par exemple, le tic tac toe permettra de se servir des premières connaissances (configurer fenêtre, dessiner des formes, les événements de la souris).

Pour le moment, je n'ai qu'un plan, mais j'espère mettre une version avancée en bêta fin décembre. Donc pour faire large, selon ce que j'envisage actuellement et la conséquence du contenu, je pense qu'il sera pas publié avant le mois de février.

J'ai donc bien peur que tu doives patienter jusque là.

En attendant, si tu recherches des tutoriels en particulier ou si tu serais intéressé par un sujet en particulier n'hésite pas à demander ici ou sur les forums.

Je pense que Love2D/Lua est une excellente porte d'entrée dans la programmation de jeux vidéo c'est pourquoi j'ai à cœur de proposer ce tutoriel. Après, il faut aussi trouver un contenu Lua pour débutants de sorte à épauler les lecteurs, mais c'est une autre question.

Bonne soirée,

Édité par Smokiev

Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte