lundi 16 juin 2025

Ramenez les Années 80 dans Votre Code avec un Clone de Missile Command en LÖVE !


Arcade-Forge : Plongée dans l'Histoire de Missile Command et Construction de Notre Propre Défense Atomique en LÖVE !

Salut les archivistes de l'arcade et les futurs développeurs de jeux !

Aujourd'hui, chez Arcade-Forge, nous allons rendre hommage à l'un des jeux les plus emblématiques et anxiogènes de l'histoire du jeu vidéo : Missile Command. Préparez-vous à un voyage dans le temps jusqu'à l'apogée de la Guerre Froide, puis à une plongée dans le code pour recréer ce classique avec le framework LÖVE2D.

L'Histoire Dramatique de Missile Command : Un Classique Né de la Peur

Missile Command, sorti en juillet 1980, n'est pas juste un jeu d'arcade, c'est un véritable témoignage de son époque.

  • Qui l'a créé ? Le jeu a été développé par Atari, avec une conception principale attribuée à Dave Theurer. L'idée lui est venue après une nuit d'anxiété et de cauchemars récurrents sur l'holocauste nucléaire, une peur palpable durant la Guerre Froide. Il voulait créer un jeu qui transmette cette tension et cette impuissance face à une menace écrasante.
  • Quand est-il sorti ? En plein été 1980. Le contexte géopolitique de l'époque (tensions entre les États-Unis et l'Union Soviétique, course aux armements nucléaires) a indéniablement contribué à la résonance du jeu auprès du public.
  • A-t-il eu du succès tout de suite ? Oui, un succès retentissant ! Missile Command a été un hit immédiat dans les salles d'arcade. Son gameplay unique, son thème intense et son ambiance sonore et visuelle (avec le célèbre "cloc" des missiles qui montent et le "bip" des alertes) ont captivé les joueurs. Il était non seulement divertissant mais aussi profondément marquant, les laissant souvent avec un sentiment d'urgence et d'inéluctabilité. Il est devenu l'un des jeux d'arcade les plus rentables d'Atari.

Missile Command est resté gravé dans les mémoires comme un chef-d'œuvre de design qui transcende le simple divertissement pour toucher à des thèmes plus profonds.

Qu'est-ce que Missile Command ? Le But, le Gameplay, la Tension

Le But du Jeu : Votre mission est simple, mais vitale : défendre six villes (ou bases) situées en bas de l'écran contre des vagues incessantes de missiles balistiques ennemis tombant du ciel.

Comment y Jouer : Le joueur dispose de trois bases de lancement de missiles (dans le jeu original, trois sites de lancement, un central et deux latéraux).

  1. Ciblage : À l'aide d'un trackball (ou de la souris dans notre version), vous déplacez un viseur sur l'écran.
  2. Tir : En cliquant, vous tirez un missile intercepteur depuis l'une de vos bases (la plus proche du point ciblé). Ce missile s'élève et explose à l'endroit que vous avez ciblé.
  3. L'Explosion Salvatrice : Le but est de faire exploser votre missile juste avant qu'un missile ennemi n'atteigne le sol. L'explosion de votre missile crée une boule de feu qui détruit tous les missiles ennemis qui la traversent.
  4. Gestion des Munitions : Dans l'original, chaque base avait un stock limité de missiles. Une fois vides, elles ne pouvaient plus tirer. Les bases pouvaient aussi être détruites par les missiles ennemis.
  5. Perte du Jeu : Le jeu se termine lorsque toutes vos villes (et vos bases dans certaines versions ou si vous manquez de munitions) sont détruites. La musique emblématique et l'écran final montrant l'annihilation de vos villes contribuent à la gravité du propos.

Missile Command est un jeu d'anticipation et de gestion de ressources sous pression, où chaque décision compte et le sentiment d'être submergé est quasi constant.

Notre Projet : Recréer Missile Command avec LÖVE2D

Inspirés par ce monument, nous avons entrepris de recréer une version simplifiée de Missile Command en utilisant LÖVE2D. LÖVE est un framework de développement de jeux léger et puissant, parfait pour les débutants grâce à sa simplicité et au langage Lua.

L'objectif de ce projet était double : rendre hommage à un classique et surtout, montrer comment les mécaniques de base d'un jeu peuvent être construites pas à pas. Nous avons rencontré des défis typiques, comme la gestion des collisions et la logique de suppression d'objets, mais chaque obstacle fut une occasion d'apprendre !


 

Le Code Commenté : Décortiquer la Défense Atomique pour les Débutants

Maintenant, il est temps de plonger dans le cœur de notre clone de Missile Command. Nous avons commenté le code de manière très détaillée pour que même un programmeur débutant puisse comprendre chaque ligne.

-- Arcade-forge 

-- main.lua
-- C'est le fichier principal de ton jeu LÖVE. Tout le code que LÖVE va exécuter est ici.

---
-- [[
--    ==========================
--    1. Définition des Constantes
--    ==========================
--    Les constantes sont des valeurs qui ne changent pas pendant l'exécution du jeu.
--    C'est une bonne pratique de les mettre en haut pour les modifier facilement si besoin.
-- ]]
---

local SCREEN_WIDTH = 800  -- Largeur de l'écran de jeu en pixels (points sur l'écran)
local SCREEN_HEIGHT = 600 -- Hauteur de l'écran de jeu en pixels

local BASE_COUNT = 3    -- Nombre de bases (lanceurs de missiles) du joueur
local CITY_COUNT = 6    -- Nombre de villes à protéger

local MISSILE_SPEED = 200     -- Vitesse des missiles ennemis (en pixels par seconde)
local PLAYER_MISSILE_SPEED = 400 -- Vitesse des missiles que le joueur tire

local EXPLOSION_LIFETIME = 0.5 -- Durée de vie d'une explosion en secondes
local EXPLOSION_RADIUS = 50   -- Rayon initial d'une explosion (quand elle apparaît)

-- Dimensions des éléments du jeu pour les collisions.
-- On les définit ici pour être cohérent avec le dessin et la détection.
local CITY_WIDTH = 30   -- Largeur d'une ville
local CITY_HEIGHT = 15  -- Hauteur d'une ville
local BASE_WIDTH = 40   -- Largeur d'une base
local BASE_HEIGHT = 20  -- Hauteur d'une base

---
-- [[
--    ==========================
--    2. Déclaration des Tables (Listes d'Objets)
--    ==========================
--    Les "tables" en Lua sont comme des listes ou des ensembles d'éléments.
--    Ici, chaque table va contenir tous les objets d'un certain type dans le jeu.
--    Par exemple, 'enemyMissiles' contiendra tous les missiles ennemis qui sont actuellement en l'air.
-- ]]
---

local bases = {}        -- Une table pour stocker toutes les bases du joueur
local cities = {}       -- Une table pour stocker toutes les villes à protéger
local enemyMissiles = {} -- Une table pour stocker tous les missiles ennemis
local playerMissiles = {} -- Une table pour stocker tous les missiles tirés par le joueur
local explosions = {}   -- Une table pour stocker toutes les explosions actives

---
-- [[
--    ==========================
--    3. Variables de Jeu Générales
--    ==========================
--    Ces variables gardent une trace de l'état global du jeu.
-- ]]
---

local score = 0      -- Le score du joueur, commence à zéro
local gameOver = false -- Un "drapeau" (flag) qui devient vrai (true) quand le jeu est terminé

---
-- [[
--    ==========================
--    4. love.load()
--    ==========================
--    Cette fonction est la première à être exécutée quand le jeu démarre.
--    C'est ici qu'on prépare tout : on initialise les éléments, on charge les ressources.
-- ]]
---
function love.load()
    -- Définit le titre de la fenêtre du jeu
    love.window.setTitle("LÖVE Missile Command")
    -- Configure la fenêtre : largeur, hauteur, non redimensionnable, et synchronisation verticale (pour éviter le "tearing")
    love.window.setMode(SCREEN_WIDTH, SCREEN_HEIGHT, {resizable = false, vsync = true})

    -- Initialisation des bases du joueur
    -- On calcule un espacement pour qu'elles soient bien réparties sur la largeur de l'écran
    local baseSpacing = SCREEN_WIDTH / (BASE_COUNT + 1)
    for i = 1, BASE_COUNT do -- Boucle pour créer chaque base
        -- Chaque base est une petite table avec sa position (x, y) et son état (vivante ou non)
        -- Les bases sont placées vers le bas de l'écran (SCREEN_HEIGHT - 50)
        bases[i] = {x = i * baseSpacing, y = SCREEN_HEIGHT - 50, alive = true}
    end

    -- Initialisation des villes
    -- Même principe que pour les bases, mais plus près du bas de l'écran
    local citySpacing = SCREEN_WIDTH / (CITY_COUNT + 1)
    for i = 1, CITY_COUNT do -- Boucle pour créer chaque ville
        -- Les villes sont placées encore plus près du bas de l'écran (SCREEN_HEIGHT - 20)
        cities[i] = {x = i * citySpacing, y = SCREEN_HEIGHT - 20, alive = true}
    end

    -- Charge une police de caractères par défaut pour afficher le texte (score, Game Over)
    love.graphics.setFont(love.graphics.newFont(20)) -- Taille de la police 20
end

---
-- [[
--    ==========================
--    5. love.update(dt)
--    ==========================
--    Cette fonction est appelée à *chaque image* du jeu.
--    C'est ici que toute la logique du jeu est mise à jour : les mouvements, les collisions, l'apparition des ennemis, etc.
--    'dt' (delta time) est un paramètre très important : c'est le temps écoulé depuis la dernière fois que love.update a été appelée.
--    Utiliser 'dt' permet de rendre la vitesse des objets indépendante de la vitesse de ton ordinateur (du nombre d'images par seconde).
-- ]]
---
function love.update(dt)
    if gameOver then return end -- Si le jeu est terminé, on ne fait rien dans la fonction update

    -- Mise à jour des missiles ennemis
    -- On parcourt la table 'enemyMissiles' à l'envers (de la fin vers le début).
    -- C'est important de faire ça quand on supprime des éléments d'une table pendant qu'on la parcourt,
    -- pour éviter des erreurs d'index.
    for i = #enemyMissiles, 1, -1 do
        local missile = enemyMissiles[i] -- On prend le missile actuel
        missile.y = missile.y + MISSILE_SPEED * dt -- Le missile descend (sa position y augmente)

        local targetHit = false -- Un drapeau pour savoir si ce missile a touché une cible au sol (base ou ville)

        -- 1. Vérifier les collisions avec les BASES
        -- On vérifie seulement si le missile est arrivé assez bas pour potentiellement toucher une base.
        -- 'bases[1].y - BASE_HEIGHT / 2 - 15' est une estimation du haut de la zone des bases.
        if missile.y >= (bases[1].y - BASE_HEIGHT / 2 - 15) then
            -- Cette boucle parcourt toutes les 'bases'.
            -- 'ipairs(bases)' : C'est une fonction qui parcourt les éléments d'une table numérotée (comme une liste).
            --                  Elle renvoie à chaque fois deux choses : l'index (1, 2, 3...) et la valeur de l'élément.
            -- '_' (underscore) : Ici, on n'a pas besoin de l'index numérique de la base (est-ce la 1ère, la 2ème ? Peu importe).
            --                    L'underscore est une convention en Lua pour dire "j'ignore cette valeur".
            -- 'base' : Cette variable va contenir l'objet 'base' lui-même (la table {x=..., y=..., alive=...}) à chaque tour de boucle.
            --          C'est cette variable 'base' qu'on utilisera ensuite pour vérifier ses propriétés.
            for _, base in ipairs(bases) do
                local base = base -- On prend la base actuelle (oui, le nom de la variable est le même que le paramètre de la boucle, c'est OK ici)
                -- Si la base est vivante ET que le missile est dans la zone de collision de la base
                if base.alive and
                   missile.x > base.x - BASE_WIDTH / 2 and missile.x < base.x + BASE_WIDTH / 2 and
                   missile.y > base.y - BASE_HEIGHT / 2 and missile.y < base.y + BASE_HEIGHT / 2 then
                    base.alive = false -- La base est détruite (elle n'est plus vivante)
                    targetHit = true   -- On indique que ce missile a touché quelque chose
                    break              -- Cesser de vérifier les autres bases pour ce missile (il n'en détruit qu'une)
                end
            end
        end

        -- 2. Vérifier les collisions avec les VILLES
        -- On ne vérifie les villes QUE SI le missile n'a PAS déjà touché une base.
        -- Et si le missile est arrivé assez bas pour potentiellement toucher une ville.
        if not targetHit and missile.y >= (cities[1].y - CITY_HEIGHT / 2 - 15) then
            for _, city in ipairs(cities) do -- Même logique de parcours que pour les bases
                local city = city -- On prend la ville actuelle
                -- Si la ville est vivante ET que le missile est dans la zone de collision de la ville
                if city.alive and
                   missile.x > city.x - CITY_WIDTH / 2 and missile.x < city.x + CITY_WIDTH / 2 and
                   missile.y > city.y - CITY_HEIGHT / 2 and missile.y < city.y + CITY_HEIGHT / 2 then
                    city.alive = false -- La ville est détruite
                    targetHit = true   -- On indique que ce missile a touché quelque chose
                    break              -- Cesser de vérifier les autres villes pour ce missile
                end
            end
        end

        -- 3. Retirer le missile
        -- Si le missile a touché une cible OU s'il a dépassé le bas de l'écran, on le retire.
        if targetHit or missile.y >= SCREEN_HEIGHT then
            table.remove(enemyMissiles, i) -- Retire le missile de la table
        end
    end

    -- Mise à jour des missiles du joueur
    for i = #playerMissiles, 1, -1 do
        local missile = playerMissiles[i] -- Prend le missile du joueur actuel
        -- Calcule la direction vers la cible (où le joueur a cliqué/touché)
        local dx = missile.targetX - missile.x
        local dy = missile.targetY - missile.y
        local dist = math.sqrt(dx*dx + dy*dy) -- Calcule la distance restante jusqu'à la cible

        if dist > 0 then -- Si le missile n'est pas encore sur sa cible
            -- Calcule comment le missile doit se déplacer pour atteindre la cible en fonction de sa vitesse et du temps écoulé (dt)
            local speedX = (dx / dist) * PLAYER_MISSILE_SPEED * dt
            local speedY = (dy / dist) * PLAYER_MISSILE_SPEED * dt

            missile.x = missile.x + speedX -- Déplace le missile horizontalement
            missile.y = missile.y + speedY -- Déplace le missile verticalement

            -- Vérifie si le missile a atteint ou dépassé sa cible.
            -- C'est une façon robuste de le faire, même si le missile "saute" la cible à cause de sa vitesse.
            if (dx * (missile.targetX - missile.x) <= 0 and dy * (missile.targetY - missile.y) <= 0) then
                -- Si oui, crée une explosion à l'endroit de la cible
                table.insert(explosions, {x = missile.targetX, y = missile.targetY, timer = EXPLOSION_LIFETIME, radius = EXPLOSION_RADIUS})
                table.remove(playerMissiles, i) -- Retire le missile du joueur (il a explosé)
            end
        else -- Si le missile est déjà pile sur sa cible (distance est zéro)
            table.insert(explosions, {x = missile.targetX, y = missile.targetY, timer = EXPLOSION_LIFETIME, radius = EXPLOSION_RADIUS})
            table.remove(playerMissiles, i)
        end
    end

    -- Mise à jour des explosions
    for i = #explosions, 1, -1 do
        local explosion = explosions[i] -- Prend l'explosion actuelle
        explosion.timer = explosion.timer - dt -- Le compte à rebours de l'explosion diminue
        explosion.radius = explosion.radius + 100 * dt -- L'explosion grandit visuellement

        -- Vérifier les collisions entre explosions et missiles ennemis
        for j = #enemyMissiles, 1, -1 do
            local enemyMissile = enemyMissiles[j] -- Prend le missile ennemi actuel
            -- Calcule la distance entre le centre de l'explosion et le missile ennemi
            local dist = math.sqrt((explosion.x - enemyMissile.x)^2 + (explosion.y - enemyMissile.y)^2)
            if dist < explosion.radius then -- Si le missile ennemi est dans le rayon de l'explosion
                score = score + 10 -- Le joueur gagne des points
                table.remove(enemyMissiles, j) -- Le missile ennemi est détruit par l'explosion
            end
        end

        if explosion.timer <= 0 then -- Si le compte à rebours de l'explosion est terminé
            table.remove(explosions, i) -- L'explosion disparaît
        end
    end

    -- Faire apparaître de nouveaux missiles ennemis (très basique pour l'instant)
    -- math.random() génère un nombre aléatoire entre 0 et 1.
    -- Si ce nombre est très petit (0.015), un nouveau missile apparaît.
    -- Cela crée une apparition aléatoire.
    if math.random() < 0.015 then
        -- Ajoute un nouveau missile ennemi, avec une position X aléatoire en haut de l'écran (y=0)
        table.insert(enemyMissiles, {x = math.random(SCREEN_WIDTH), y = 0})
    end

    -- Vérifier la condition de "Game Over"
    -- On suppose que toutes les villes sont détruites au début de la vérification
    local allCitiesDestroyed = true
    for _, city in ipairs(cities) do -- On parcourt toutes les villes
        if city.alive then -- Si on trouve au moins une ville vivante
            allCitiesDestroyed = false -- Alors toutes les villes ne sont pas détruites
            break                      -- On peut arrêter de vérifier les villes
        end
    end

    -- On fait la même chose pour les bases
    local allBasesDestroyed = true
    for _, base in ipairs(bases) do
        if base.alive then
            allBasesDestroyed = false
            break
        end
    end

    -- Si toutes les villes ET toutes les bases sont détruites, c'est "Game Over"
    if allCitiesDestroyed and allBasesDestroyed then
        gameOver = true
    end
end

---
-- [[
--    ==========================
--    6. love.draw()
--    ==========================
--    Cette fonction est appelée à *chaque image* juste après love.update.
--    C'est ici qu'on dessine tout ce qui doit apparaître à l'écran.
--    Il est important que love.draw() ne contienne pas de logique de jeu (mouvement, collisions)
--    car elle peut être appelée plus ou moins souvent que love.update selon la performance.
-- ]]
---
function love.draw()
    -- Dessiner les bases
    love.graphics.setColor(0.2, 0.8, 0.2) -- Définit la couleur de dessin en vert foncé (RVB : 0 à 1)
    for _, base in ipairs(bases) do -- Pour chaque base
        if base.alive then -- Si la base est vivante
            -- Dessine un rectangle plein. Les coordonnées sont ajustées pour que x,y soit le centre de la base.
            love.graphics.rectangle("fill", base.x - BASE_WIDTH / 2, base.y - BASE_HEIGHT / 2, BASE_WIDTH, BASE_HEIGHT)
        end
    end

    -- Dessiner les villes
    love.graphics.setColor(0.8, 0.8, 0.2) -- Couleur jaune pour les villes
    for _, city in ipairs(cities) do
        if city.alive then
            love.graphics.rectangle("fill", city.x - CITY_WIDTH / 2, city.y - CITY_HEIGHT / 2, CITY_WIDTH, CITY_HEIGHT)
        end
    end

    -- Dessiner les missiles ennemis
    love.graphics.setColor(1, 0, 0) -- Couleur rouge
    love.graphics.setLineWidth(3)   -- Définit l'épaisseur de ligne à 3 pixels (pour les rendre plus visibles)
    for _, missile in ipairs(enemyMissiles) do
        -- Dessine une ligne pour représenter le missile. 'missile.y - 15' rend la ligne un peu plus longue.
        love.graphics.line(missile.x, missile.y, missile.x, missile.y - 15)
    end
    love.graphics.setLineWidth(1) -- Très important : Réinitialise l'épaisseur de ligne à la valeur par défaut (1)
                                  -- pour que les prochains dessins (comme les missiles du joueur) n'aient pas cette épaisseur.

    -- Dessiner les missiles du joueur
    love.graphics.setColor(0, 0, 1) -- Couleur bleue
    for _, missile in ipairs(playerMissiles) do
        love.graphics.line(missile.x, missile.y, missile.x, missile.y - 10)
    end

    -- Dessiner les explosions
    love.graphics.setColor(1, 0.5, 0, 0.5) -- Couleur orange avec une transparence (0.5)
    for _, explosion in ipairs(explosions) do
        -- Dessine un cercle plein au centre de l'explosion, avec son rayon actuel
        love.graphics.circle("fill", explosion.x, explosion.y, explosion.radius)
    end

    -- Afficher le score
    love.graphics.setColor(1, 1, 1) -- Couleur blanche pour le texte
    -- Affiche le texte "Score: " suivi de la valeur de la variable 'score'
    love.graphics.print("Score: " .. score, 10, 10) -- Affiche en haut à gauche (position 10, 10)

    -- Afficher "Game Over" si le jeu est terminé
    if gameOver then
        love.graphics.setColor(1, 0, 0) -- Couleur rouge pour le texte Game Over
        -- Affiche "GAME OVER" centré sur l'écran
        love.graphics.printf("GAME OVER", 0, SCREEN_HEIGHT / 2 - 20, SCREEN_WIDTH, "center")
    end
end

---
-- [[
--    ==========================
--    7. love.mousepressed(x, y, button, istouch)
--    ==========================
--    Cette fonction est appelée chaque fois qu'un bouton de la souris est pressé.
--    Elle est aussi appelée lors d'un toucher sur un écran tactile !
--    x, y: position du clic ou du toucher.
--    button: quel bouton a été cliqué (1 pour le gauche/principal).
--    istouch: indique si l'événement vient d'un écran tactile (vrai/true) ou d'une souris (faux/false).
-- ]]
---
function love.mousepressed(x, y, button, istouch)
    if gameOver then return end -- Si le jeu est terminé, les clics/touchers n'ont plus d'effet

    -- On vérifie si c'est un clic gauche de souris (button == 1) OU un toucher d'écran (istouch == true).
    -- Cela rend le jeu jouable avec les deux types d'entrée.
    if button == 1 or istouch then
        -- On doit trouver la base du joueur la plus proche pour qu'elle tire le missile.
        local closestBase = nil -- Variable pour stocker la base la plus proche trouvée
        local minDist = math.huge -- Initialise la distance minimale à un très grand nombre (l'infini)

        -- Cette boucle parcourt toutes les 'bases'.
        -- 'ipairs(bases)' : C'est une fonction qui parcourt les éléments d'une table numérotée (comme une liste).
        --                  Elle renvoie à chaque fois deux choses : l'index (1, 2, 3...) et la valeur de l'élément.
        -- '_' (underscore) : Ici, on n'a pas besoin de l'index numérique de la base (est-ce la 1ère, la 2ème ? Peu importe).
        --                    L'underscore est une convention en Lua pour dire "j'ignore cette valeur".
        -- 'base' : Cette variable va contenir l'objet 'base' lui-même (la table {x=..., y=..., alive=...}) à chaque tour de boucle.
        --          C'est cette variable 'base' qu'on utilisera ensuite pour vérifier ses propriétés.
        for _, base in ipairs(bases) do
            if base.alive then -- Seulement si la base est encore vivante
                -- Calcule la distance entre la position du clic/toucher (x,y) et le centre de la base
                local dist = math.sqrt((x - base.x)^2 + (y - base.y)^2)
                if dist < minDist then -- Si cette distance est plus petite que la distance minimale trouvée jusqu'à présent
                    minDist = dist      -- Met à jour la distance minimale
                    closestBase = base  -- C'est cette base qui est la plus proche
                end
            end
        end

        if closestBase then -- Si une base vivante la plus proche a été trouvée
            -- Crée un nouveau missile du joueur. Il part de la position de la base
            -- et a pour cible la position où le joueur a cliqué/touché (x, y).
            table.insert(playerMissiles, {x = closestBase.x, y = closestBase.y, targetX = x, targetY = y})
        end
    end
end 

Les Piliers de Notre Code : Concepts Fondamentaux pour la Création de Jeux

En parcourant le code, vous avez découvert ou redécouvert des notions essentielles :

  • Variables (ex: score, gameOver) : Ce sont des conteneurs pour stocker des informations (nombres, états vrai/faux).
  • Tables (ex: enemyMissiles, bases) : Des collections d'objets. Elles sont vitales pour gérer tous les éléments dynamiques de votre jeu.
  • Fonctions LÖVE clés :
    • love.load() : La phase de préparation du jeu. C'est ici que vous initialisez vos variables, créez vos bases et villes, et chargez ce dont vous avez besoin.
    • love.update(dt) : Le moteur du jeu. Cette fonction est appelée continuellement et gère la logique : mouvement des missiles, détection des collisions, destruction des éléments, apparition de nouveaux ennemis. Le dt (delta time) est crucial pour que le jeu tourne à la même vitesse sur tous les ordinateurs.
    • love.draw() : Le pinceau du jeu. Appelé après update, il se charge uniquement de dessiner tous les éléments à l'écran à leurs positions mises à jour.
  • Boucles (for ... do ... end) : Permettent de répéter des actions pour chaque élément d'une liste (par exemple, déplacer tous les missiles ennemis). La technique de boucler à l'envers (for i = #table, 1, -1 do) est fondamentale quand vous retirez des éléments d'une liste pendant que vous la parcourez.
  • Conditions (if ... then ... end) : Le cœur de la logique décisionnelle du jeu. "Si ceci se produit, alors fais cela." (Ex: si un missile touche, alors détruis-le).
  • Système de Coordonnées (X, Y) : En LÖVE, le point (0,0) est en haut à gauche de votre fenêtre. L'axe X va vers la droite, et l'axe Y va vers le bas.
  • Détection de Collision (Point dans Rectangle) : Une méthode simple pour savoir si un missile (représenté par un point) touche une base ou une ville (représentées par des rectangles).

Ce projet n'est pas seulement une recréation d'un classique, c'est aussi un excellent tremplin pour comprendre les principes fondamentaux de la création de jeux. Chaque ligne de code, chaque ajustement pour corriger un bug, est une leçon.

N'attendez plus ! Téléchargez LÖVE2D, copiez ce code, et amusez-vous à le modifier, à y ajouter des fonctionnalités (sons, graphismes améliorés, vagues d'ennemis plus complexes...). Le monde du développement de jeux vous attend !

Rendez-vous sur Arcade-Forge pour de nouvelles aventures pixelisées !

 

Et voici le jeu :

 

vendredi 13 juin 2025

Rendez Votre Flappy Bird Love2D Jouable sur Mobile


Rendez Votre Flappy Bird Love2D Jouable sur Mobile : Le Guide Complet

Créer un jeu, c'est fantastique ! Mais dans le monde actuel, si votre création n'est pas accessible sur smartphone, elle passe à côté d'une énorme audience. Aujourd'hui, nous allons voir comment transformer un clone de Flappy Bird précédement développé avec Love2D, initialement conçu pour PC, en une expérience fluide et agréable sur mobile.

Pourquoi Adapter aux Mobiles ?

La principale différence entre jouer sur un ordinateur et sur un smartphone réside dans le mode d'interaction : clavier/souris contre écran tactile. De plus, les écrans de mobiles varient énormément en taille et en orientation, et les performances sont souvent plus limitées. Il est crucial de prendre en compte ces éléments dès le début de l'adaptation.

Les Fondamentaux du Jeu Original

Notre point de départ est un jeu Love2D Flappy Bird classique : l'oiseau tombe sous l'effet de la gravité et on le fait voler en appuyant sur la barre d'espace. Des tuyaux défilent, et si l'oiseau touche un tuyau ou le sol, c'est "Game Over". Sur PC, un simple appui sur "R" permet de rejouer.

1. Des Commandes Intuitives : Le Tactile avant Tout !

Le plus grand changement pour le mobile est l'abandon du clavier. Sur un smartphone, l'action principale de "faire voler" l'oiseau doit se faire par un simple tapotement sur l'écran.

Dans Love2D, la fonction love.mousepressed(x, y, button, istouch) est notre meilleure amie. Elle gère non seulement les clics de souris, mais aussi les événements tactiles via le paramètre istouch.

Avant (love.keypressed) :

function love.keypressed(key)
    if key == "space" and GAME_STATE == "playing" then
        bird_velocity_y = BIRD_FLAP_VELOCITY
    end
    -- ... logique de démarrage du jeu
end

Après (love.mousepressed) :

Nous avons modifié love.mousepressed pour gérer le démarrage du jeu et le "flap" de l'oiseau. Sur PC, un simple clic (button == 1) fonctionnera. Sur mobile, un tapotement sera également interprété comme un button == 1, rendant l'interaction universelle.

function love.mousepressed(x, y, button, istouch)
    -- On vérifie que c'est un clic gauche ou un tapotement
    if button == 1 then
        if GAME_STATE == "start" then
            GAME_STATE = "playing"
            bird_velocity_y = BIRD_FLAP_VELOCITY
        elseif GAME_STATE == "playing" then
            bird_velocity_y = BIRD_FLAP_VELOCITY
        -- ... logique pour le bouton rejouer (voir point 2)
        end
    end
end

Désormais, un simple toucher de l'écran, que ce soit pour démarrer le jeu ou pour faire sauter l'oiseau, sera reconnu. Nous avons également retiré la détection de la touche 'Space' de love.keypressed pour ne pas avoir de comportement dupliqué et pour concentrer la logique d'interaction dans love.mousepressed.

2. Une Interface Revisitée : Les Boutons à l'Écran

Après un "Game Over", il n'y a pas de touche 'R' sur un écran tactile ! Il faut donc un bouton "REJOUER" visible et facile à tapoter.

Dans la fonction love.draw(), lorsque GAME_STATE est "gameover", nous dessinons un rectangle et y ajoutons le texte "REJOUER". Les coordonnées et la taille de ce bouton sont stockées dans des variables globales (RESTART_BUTTON_X, etc.) pour être réutilisées.

Ajout dans love.draw() (quand GAME_STATE == "gameover") :

-- Dessine le bouton "REJOUER"
RESTART_BUTTON_WIDTH = 200
RESTART_BUTTON_HEIGHT = 60
RESTART_BUTTON_X = (WINDOW_WIDTH - RESTART_BUTTON_WIDTH) / 2
RESTART_BUTTON_Y = WINDOW_HEIGHT / 2 + 100

love.graphics.setColor(0.3, 0.5, 0.8) -- Couleur du bouton
love.graphics.rectangle("fill", RESTART_BUTTON_X, RESTART_BUTTON_Y, RESTART_BUTTON_WIDTH, RESTART_BUTTON_HEIGHT)

love.graphics.setColor(COLOR_TEXT) -- Texte blanc
love.graphics.setFont(love.graphics.newFont(30))
love.graphics.printf("REJOUER", RESTART_BUTTON_X, RESTART_BUTTON_Y + (RESTART_BUTTON_HEIGHT - love.graphics.getFont():getHeight()) / 2, RESTART_BUTTON_WIDTH, "center")

Ensuite, dans love.mousepressed, nous ajoutons une vérification pour voir si le tapotement a eu lieu à l'intérieur de la zone de ce bouton.

Ajout dans love.mousepressed (quand GAME_STATE == "gameover") :

elseif GAME_STATE == "gameover" then
    if x >= RESTART_BUTTON_X and x <= RESTART_BUTTON_X + RESTART_BUTTON_WIDTH and
       y >= RESTART_BUTTON_Y and y <= RESTART_BUTTON_Y + RESTART_BUTTON_HEIGHT then
        resetGame() -- Réinitialise le jeu si le bouton est tapoté
    end
end

Cette approche assure que l'utilisateur mobile peut facilement relancer une partie sans dépendre d'un clavier.

3. Gérer l'Orientation de l'Écran

Les smartphones peuvent être tenus en mode portrait ou paysage. Pour Flappy Bird, le mode paysage est idéal, offrant plus d'espace pour que les tuyaux défilent horizontalement.

Love2D permet de spécifier l'orientation préférée via les paramètres de love.window.setMode. Cependant, ces paramètres sont spécifiques aux plateformes mobiles. Pour éviter une erreur (Invalid window setting: mobile) lors des tests sur PC, nous devons appliquer cette configuration de manière conditionnelle.

Modification dans love.load() :

function love.load()
    -- ... (titre, etc.)

    local os_name = love.system.getOS() -- Récupère le nom du système d'exploitation
    local window_settings = {
        resizable = false,
        fullscreen = false, -- Peut être true pour le déploiement final
        vsync = true
    }

    -- Si c'est un OS mobile (Android ou iOS), on force l'orientation paysage
    if os_name == "Android" or os_name == "iOS" then
        window_settings.mobile = {orientation = "landscape"}
        window_settings.fullscreen = true -- Souvent souhaitable sur mobile
        window_settings.borderless = true
    end

    love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT, window_settings)
    
    -- ... (chargement des images, etc.)
end

Grâce à love.system.getOS(), notre jeu s'adaptera intelligemment, demandant l'orientation paysage sur mobile et se comportant normalement sur PC.

4. Petites Optimisations et Prochaines Étapes

Pour un jeu simple comme Flappy Bird, la performance n'est généralement pas un problème majeur. Cependant, pour des jeux plus complexes, vous devriez toujours considérer :

  • La taille des images : Utilisez des images de résolution raisonnable pour éviter de surcharger la mémoire.

  • Les polices : Évitez un trop grand nombre de polices différentes, et assurez-vous qu'elles sont lisibles sur de petits écrans.

Une fois que votre jeu fonctionne parfaitement sur PC avec ces modifications, l'étape finale est le packaging pour mobile. Love2D fournit des guides détaillés pour compiler votre jeu en application Android (.apk) ou iOS (.ipa) en utilisant les SDKs respectifs (Android Studio pour Android, Xcode pour iOS). C'est le moment où toutes vos adaptations prendront tout leur sens sur un véritable appareil !

Adapter un jeu pour mobile demande de repenser l'interaction et l'affichage, mais avec Love2D, le processus est remarquablement fluide. Lancez-vous, et bientôt, votre Flappy Bird volera sur des milliers de smartphones !

Le code à télécharger et le jeu sur notre pages : https://arcadeforge.itch.io/flappy-bird 

mardi 10 juin 2025

Créons Flappy Bird


 

Créons Flappy Bird : Un Projet Idéal pour Débuter en Programmation de Jeux

Qui n'a jamais entendu parler de Flappy Bird ? Ce jeu mobile, en apparence simpliste, a captivé (et parfois frustré !) des millions de joueurs à travers le monde. Son concept est enfantin : faites voler un oiseau entre des tuyaux sans le faire tomber ni le faire s'écraser. Mais derrière cette simplicité se cachent des mécaniques de jeu fondamentales, parfaites pour apprendre les bases de la programmation de jeux vidéo.

L'histoire d'un phénomène inattendu

Flappy Bird a été créé en 2013 par un développeur vietnamien, Dong Nguyen, en seulement quelques jours. Le jeu a explosé en popularité début 2014, devenant un succès mondial inattendu. Son secret ? Une difficulté exigeante, un concept addictif et un style graphique rétro inspiré des jeux 8-bit. Malgré son succès fulgurant, Dong Nguyen l'a retiré des magasins d'applications, citant l'addiction qu'il générait. Cela n'a fait qu'alimenter sa légende, et le jeu est depuis devenu un classique pour les développeurs indépendants qui cherchent à recréer ses mécaniques pour l'apprentissage.

Pourquoi Flappy Bird est parfait pour apprendre à coder un jeu ?

Pour les débutants en programmation de jeux, Flappy Bird est un trésor. Il permet d'explorer des concepts clés sans se noyer dans la complexité :

  • Gestion des entrées utilisateur : Comment réagir aux pressions de touche.

  • Physique simplifiée : La gravité et les sauts (impulsions).

  • Détection de collisions : Savoir quand l'oiseau touche les tuyaux ou le sol.

  • Génération d'objets dynamiques : Faire apparaître de nouveaux tuyaux à l'infini.

  • Gestion des états de jeu : Écran de démarrage, jeu en cours, fin de partie.

  • Animation simple : Animer un sprite avec quelques images.

  • Système de score : Suivre la progression du joueur.

Nous allons recréer une version complète de Flappy Bird en utilisant Love2D, un framework de jeu fantastique pour Lua.

Le Code de Flappy Bird (pas à pas)

Voici le code complet du jeu. Nous allons le parcourir section par section pour comprendre comment chaque partie fonctionne.

1. Variables Globales et Paramètres du Jeu

Au début du fichier, nous définissons toutes les variables importantes qui contrôlent le comportement du jeu, la taille de la fenêtre, la vitesse de l'oiseau, la fréquence des tuyaux, etc. Ces valeurs peuvent être ajustées pour modifier la difficulté ou l'apparence du jeu.

-- main.lua - Un clone simple de Flappy Bird pour Love2D
-- Ce code est conçu pour expliquer les bases de la programmation de jeux aux débutants.

-- [[ Variables Globales du Jeu ]]
-- Ces variables définissent les paramètres de base de notre jeu.
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600

-- Définitions du joueur (maintenant appelé "oiseau" pour la généralité)
BIRD_RADIUS = 20                 -- Rayon de collision logique de l'oiseau.
BIRD_X = WINDOW_WIDTH / 4        -- Position X fixe de l'oiseau sur l'écran.
BIRD_Y = WINDOW_HEIGHT / 2       -- Position Y initiale de l'oiseau.
BIRD_GRAVITY = 1000              -- Force de gravité qui tire l'oiseau vers le bas (pixels/s^2).
BIRD_FLAP_VELOCITY = -400        -- Vitesse verticale du saut lorsque l'oiseau "bat des ailes" (pixels/s).
bird_velocity_y = 0              -- Vitesse verticale actuelle de l'oiseau.
bird_y = BIRD_Y                  -- Position Y actuelle de l'oiseau.

-- Définitions des tuyaux (obstacles)
PIPE_WIDTH = 80                  -- Largeur des tuyaux.
PIPE_GAP_HEIGHT = 180            -- Hauteur de l'espace par lequel l'oiseau doit passer.
PIPE_SPEED = 200                 -- Vitesse de défilement des tuyaux (pixels/s).
PIPE_SPAWN_INTERVAL = 2          -- Temps entre l'apparition de deux tuyaux (en secondes).
pipe_spawn_timer = 0             -- Compteur pour gérer l'apparition des tuyaux.
pipes = {}                       -- Table pour stocker tous les tuyaux actifs à l'écran.

-- Score du joueur
score = 0

-- États du jeu
-- Le jeu peut être dans différents états: "start" (écran de démarrage), "playing" (en jeu), "gameover" (fin de partie).
GAME_STATE = "start"

-- Couleurs utilisées dans le jeu (le fond et l'oiseau utiliseront des images, mais ces couleurs restent pour le sol et le texte).
COLOR_BIRD = {1, 1, 0.2}         -- Couleur de l'oiseau (si dessinée sans image).
COLOR_PIPE_TOP = {0.2, 0.7, 0.2} -- Couleur du haut des tuyaux (si dessinée sans image).
COLOR_PIPE_BOTTOM = {0.2, 0.6, 0.2} -- Couleur du bas des tuyaux (si dessinée sans image).
COLOR_BACKGROUND = {0.4, 0.6, 1} -- Couleur de fond par défaut (sera recouverte par l'image).
COLOR_TEXT = {1, 1, 1}           -- Couleur blanche pour le texte.
COLOR_GROUND = {0.3, 0.2, 0.1}   -- Couleur marron pour le sol.

-- Hauteur du sol, pour la détection de collision.
GROUND_HEIGHT = 50

-- Variables pour les images chargées (elles seront initialisées dans love.load()).
background_image = nil
bird_sprite_0 = nil              -- Première image du sprite de l'oiseau.
bird_sprite_1 = nil              -- Deuxième image du sprite de l'oiseau (pour l'animation).
pipe_top_image = nil    -- Image pour le tuyau du haut (était pipe2.png).
pipe_bottom_image = nil -- Image pour le tuyau du bas (était pipe.png).

-- Variables d'animation de l'oiseau
current_player_frame = 1         -- Frame d'animation actuelle (1 ou 2).
animation_timer = 0              -- Compteur pour le temps écoulé avant de changer de frame.
animation_speed = 0.08           -- Vitesse de l'animation (changer de frame toutes les 0.08 secondes).

-- Nombre de frames disponibles pour l'animation de l'oiseau.
PLAYER_FRAME_COUNT = 2

-- Couleurs arc-en-ciel pour le texte de l'écran de démarrage.
local rainbow_colors = {
    {1, 0, 0},     -- Rouge
    {1, 0.5, 0},   -- Orange
    {1, 1, 0},     -- Jaune
    {0, 1, 0},     -- Vert
    {0, 0.5, 1},   -- Bleu clair
    {0, 0, 1},     -- Bleu foncé
    {0.5, 0, 1}    -- Violet
}

-- Fonction utilitaire : getTableLength(t)
-- Cette fonction calcule la taille d'une table (nombre d'éléments).
-- Elle est utilisée pour la compatibilité avec certains environnements Lua.
local function getTableLength(t)
    local count = 0
    for _ in pairs(t) do
        count = count + 1
    end
    return count
end

-- Fonction pour dessiner du texte avec des couleurs arc-en-ciel.
-- text_str: la chaîne de caractères à dessiner.
-- x, y: la position de départ du texte.
-- font: la police à utiliser.
-- color_offset: un décalage optionnel pour varier le début de l'arc-en-ciel.
local function drawTextRainbow(text_str, x, y, font, color_offset)
    color_offset = color_offset or 0
    local current_x = x
    love.graphics.setFont(font) -- S'assurer que la bonne police est utilisée.
    for i = 1, #text_str do
        local char = text_str:sub(i, i)
        local char_width = font:getWidth(char)
        -- Calcul de l'index de couleur pour chaque caractère, créant l'effet arc-en-ciel.
        local color_index = (i - 1 + color_offset) % getTableLength(rainbow_colors) + 1
        love.graphics.setColor(rainbow_colors[color_index])
        love.graphics.print(char, current_x, y)
        current_x = current_x + char_width
    end
end

2. La Fonction love.load() : Les Préparatifs

La fonction love.load() est le point de départ de votre jeu. Love2D l'appelle une seule fois, au tout début. C'est ici que vous définissez la taille de la fenêtre, chargez toutes vos images (les sprites de l'oiseau, les tuyaux et le fond) et initialisez les variables du jeu pour la première partie.

-- [[ love.load() ]]
-- Cette fonction est appelée une seule fois au début du jeu.
-- Elle est utilisée pour charger les ressources (images, sons) et initialiser les variables de départ.
function love.load()
    love.window.setTitle("Flappy Bird Love2D") -- Définit le titre de la fenêtre.
    love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT) -- Définit la taille de la fenêtre.
    
    -- Charger les images depuis le dossier "images".
    background_image = love.graphics.newImage("images/flappy bird wallpaper.jpeg")
    bird_sprite_0 = love.graphics.newImage("images/sprite_0.png")
    bird_sprite_1 = love.graphics.newImage("images/sprite_1.png")
    
    -- Charger les images des tuyaux (attention à l'inversion demandée).
    pipe_top_image = love.graphics.newImage("images/pipe2.png")    -- Ceci est l'image pour le tuyau du haut.
    pipe_bottom_image = love.graphics.newImage("images/pipe.png") -- Ceci est l'image pour le tuyau du bas.

    love.graphics.setFont(love.graphics.newFont(30)) -- Définit la police par défaut pour le texte.
    math.randomseed(os.time()) -- Initialise le  générateur de nombres aléatoires (pour des tuyaux différents à chaque partie).
    
    -- Réinitialise toutes les variables pour commencer une nouvelle partie.
    resetGame()
end

-- Fonction : resetGame()
-- Réinitialise l'état du jeu pour une nouvelle partie.
function resetGame()
    bird_y = BIRD_Y             -- Position Y de l'oiseau remise à l'initiale.
    bird_velocity_y = 0         -- Vitesse de l'oiseau remise à zéro.
    pipes = {}                  -- Vide la table des tuyaux.
    score = 0                   -- Score remis à zéro.
    pipe_spawn_timer = PIPE_SPAWN_INTERVAL -- Le timer est réinitialisé pour faire apparaître le premier tuyau.
    GAME_STATE = "start"        -- Le jeu revient à l'écran de démarrage.
    current_player_frame = 1    -- L'animation de l'oiseau est réinitialisée.
    animation_timer = 0
end

3. La Fonction love.update(dt) : Le Cœur de la Logique

love.update(dt) est la fonction la plus importante de votre jeu. Love2D l'appelle à chaque frame (des dizaines de fois par seconde !). C'est là que toute la logique du jeu se déroule :

  • Mouvement de l'oiseau : La gravité est appliquée pour le faire tomber, et la vitesse verticale est mise à jour.

  • Animation : Le sprite de l'oiseau change de frame pour créer l'illusion de mouvement.

  • Génération des tuyaux : Un compte à rebours gère l'apparition de nouveaux tuyaux à l'écran.

  • Déplacement des tuyaux : Tous les tuyaux existants se déplacent vers la gauche.

  • Détection de collisions : On vérifie si l'oiseau touche un tuyau ou le sol.

  • Gestion du score : Le score augmente lorsque l'oiseau passe un tuyau.

  • Changement d'état du jeu : Passage de l'état "playing" à "gameover" en cas de collision.

-- [[ love.update(dt) ]]
-- Cette fonction est appelée à chaque frame du jeu.
-- Elle est utilisée pour mettre à jour la logique du jeu : mouvements, collisions, génération d'obstacles, etc.
-- 'dt' (delta time) est le temps écoulé depuis la dernière frame, utile pour des mouvements fluides quelle que soit la performance de l'ordinateur.
function love.update(dt)
    -- Met à jour le timer d'animation de l'oiseau.
    animation_timer = animation_timer + dt
    if animation_timer >= animation_speed then
        -- Change la frame de l'oiseau pour créer l'animation (passe de 1 à 2, puis de 2 à 1).
        current_player_frame = current_player_frame % PLAYER_FRAME_COUNT + 1
        animation_timer = animation_timer - animation_speed
    end

    -- La logique du jeu ne s'exécute que si l'état est "playing" ou "gameover".
    if GAME_STATE == "playing" then
        -- Applique la gravité à l'oiseau.
        bird_velocity_y = bird_velocity_y + BIRD_GRAVITY * dt
        bird_y = bird_y + bird_velocity_y * dt

        -- Empêche l'oiseau de sortir par le haut de l'écran.
        if bird_y < 0 then
            bird_y = 0
            bird_velocity_y = 0
        end

        -- Vérifie la collision avec le sol.
        if bird_y + BIRD_RADIUS >= WINDOW_HEIGHT - GROUND_HEIGHT then
            bird_y = WINDOW_HEIGHT - GROUND_HEIGHT - BIRD_RADIUS
            GAME_STATE = "gameover" -- Si l'oiseau touche le sol, c'est "gameover".
            bird_velocity_y = 0
        end

        -- Gère le timer d'apparition des tuyaux.
        pipe_spawn_timer = pipe_spawn_timer - dt
        if pipe_spawn_timer <= 0 then
            -- Génère une nouvelle paire de tuyaux (un en haut, un en bas) avec un espace aléatoire.
            local gap_y = math.random(PIPE_GAP_HEIGHT, WINDOW_HEIGHT - GROUND_HEIGHT - PIPE_GAP_HEIGHT)
            table.insert(pipes, {
                x = WINDOW_WIDTH,      -- Le tuyau apparaît à droite de l'écran.
                gap_y = gap_y,         -- Position verticale de l'espace.
                passed = false         -- Indique si l'oiseau a déjà passé ce tuyau (pour le score).
            })
            pipe_spawn_timer = PIPE_SPAWN_INTERVAL -- Réinitialise le timer pour le prochain tuyau.
        end

        -- Parcourt tous les tuyaux existants.
        for i = getTableLength(pipes), 1, -1 do -- On parcourt à l'envers pour pouvoir supprimer des tuyaux en toute sécurité.
            local pipe = pipes[i]
            pipe.x = pipe.x - PIPE_SPEED * dt -- Déplace le tuyau vers la gauche.

            -- Détection de collision (AABB - Bounding Box Alignée sur les Axes)
            -- Vérifie si l'oiseau entre en collision avec le tuyau du haut ou du bas.
            local bird_collides_with_top_pipe = 
                BIRD_X + BIRD_RADIUS > pipe.x and BIRD_X - BIRD_RADIUS < pipe.x + PIPE_WIDTH and
                bird_y - BIRD_RADIUS < pipe.gap_y - PIPE_GAP_HEIGHT / 2

            local bird_collides_with_bottom_pipe =
                BIRD_X + BIRD_RADIUS > pipe.x and BIRD_X - BIRD_RADIUS < pipe.x + PIPE_WIDTH and
                bird_y + BIRD_RADIUS > pipe.gap_y + PIPE_GAP_HEIGHT / 2

            if bird_collides_with_top_pipe or bird_collides_with_bottom_pipe then
                GAME_STATE = "gameover" -- Collision détectée, c'est "gameover".
            end

            -- Augmente le score si l'oiseau a passé le tuyau sans collision.
            if pipe.x + PIPE_WIDTH < BIRD_X and not pipe.passed then
                score = score + 1
                pipe.passed = true -- Marque le tuyau comme passé.
            end

            -- Supprime les tuyaux qui sont sortis de l'écran à gauche.
            if pipe.x + PIPE_WIDTH < 0 then
                table.remove(pipes, i)
            end
        end

    elseif GAME_STATE == "gameover" then
        -- Si le jeu est terminé, l'oiseau continue de tomber jusqu'à toucher le sol.
        bird_velocity_y = bird_velocity_y + BIRD_GRAVITY * dt
        bird_y = bird_y + bird_velocity_y * dt
        if bird_y + BIRD_RADIUS >= WINDOW_HEIGHT - GROUND_HEIGHT then
            bird_y = WINDOW_HEIGHT - GROUND_HEIGHT - BIRD_RADIUS
            bird_velocity_y = 0
        end
    end
end

4. La Fonction love.draw() : L'Affichage du Jeu

La fonction love.draw() est responsable de tout ce que vous voyez à l'écran. Elle est appelée juste après love.update() pour redessiner le jeu avec les dernières mises à jour.

  • Gestion des écrans : Elle vérifie l'état actuel du jeu (GAME_STATE) pour savoir s'il faut afficher l'écran de démarrage avec les instructions ou l'écran de jeu/fin de partie.

  • Dessin du fond : L'image de fond est dessinée en premier.

  • Dessin du sol : Le sol est dessiné par-dessus le fond.

  • Dessin des tuyaux : Chaque tuyau est dessiné à sa position actuelle.

  • Dessin de l'oiseau : L'image de l'oiseau est dessinée, en tenant compte de la frame d'animation actuelle et de sa position.

  • Affichage du score et des messages : Le score est affiché en haut à gauche, et des messages (comme "GAME OVER") apparaissent selon l'état du jeu.

  • Effet Arc-en-ciel : Le texte "ARCADE FORGE" utilise un color_offset basé sur le temps pour faire défiler les couleurs, créant un effet visuel amusant.

-- [[ love.draw() ]]
-- Cette fonction est appelée à chaque frame pour dessiner tous les éléments à l'écran.
function love.draw()
    -- Si le jeu est à l'état "start", on affiche l'écran de démarrage.
    if GAME_STATE == "start" then
        -- Dessine le fond noir de l'écran de démarrage.
        love.graphics.setColor(0, 0, 0) -- Noir
        love.graphics.rectangle("fill", 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)

        -- Dessine le texte "ARCADE FORGE" avec des couleurs arc-en-ciel qui défilent.
        local title_font = love.graphics.newFont(60)
        local title_text = "ARCADE FORGE"
        local title_width = title_font:getWidth(title_text)
        local title_x = (WINDOW_WIDTH - title_width) / 2
        local title_y = WINDOW_HEIGHT / 4 - 40 
        
        -- Le décalage des couleurs change avec le temps pour créer l'effet de défilement.
        local rainbow_offset_time = math.floor(love.timer.getTime() * 10) % getTableLength(rainbow_colors)
        drawTextRainbow(title_text, title_x, title_y, title_font, rainbow_offset_time)

        -- Dessine le titre du jeu "Flappy Bird".
        love.graphics.setColor(COLOR_TEXT) -- Blanc
        local game_title_font = love.graphics.newFont(80)
        love.graphics.setFont(game_title_font)
        local game_title_text = "Flappy Bird"
        local game_title_width = game_title_font:getWidth(game_title_text)
        local game_title_x = (WINDOW_WIDTH - game_title_width) / 2
        local game_title_y = title_y + title_font:getHeight() + 20 
        love.graphics.printf(game_title_text, game_title_x, game_title_y, game_title_width, "center")

        -- Affiche les instructions du jeu.
        love.graphics.setColor(COLOR_TEXT) -- Blanc
        local instructions_font = love.graphics.newFont(25)
        love.graphics.setFont(instructions_font)
        local instructions = {
            "Évitez les tuyaux et le sol avec votre oiseau !",
            "", -- Ligne vide pour l'espacement.
            "Appuyez sur ESPACE pour le faire voler.",
            "Appuyez sur 'R' pour rejouer après un GAME OVER."
        }
        local instructions_y = WINDOW_HEIGHT / 2 + 50 

        local current_y = instructions_y
        for _, line in ipairs(instructions) do
            love.graphics.printf(line, 0, current_y, WINDOW_WIDTH, "center")
            current_y = current_y + instructions_font:getHeight() + 5
        end

    else -- Si le jeu est en mode "playing" ou "gameover", on dessine les éléments du jeu.
        -- Dessine l'image de fond du jeu.
        love.graphics.setColor(1, 1, 1) -- Utilise la couleur blanche par défaut pour les images.
        local bg_scale_x = WINDOW_WIDTH / background_image:getWidth()
        local bg_scale_y = WINDOW_HEIGHT / background_image:getHeight()
        love.graphics.draw(background_image, 0, 0, 0, bg_scale_x, bg_scale_y)

        -- Dessine le sol (par-dessus le fond).
        love.graphics.setColor(COLOR_GROUND)
        love.graphics.rectangle("fill", 0, WINDOW_HEIGHT - GROUND_HEIGHT, WINDOW_WIDTH, GROUND_HEIGHT)

        -- Dessine les tuyaux.
        love.graphics.setColor(1, 1, 1) -- Réinitialise la couleur pour les images de tuyaux.
        for _, pipe in ipairs(pipes) do
            -- Tuyau du haut.
            local top_pipe_height = pipe.gap_y - PIPE_GAP_HEIGHT / 2
            -- Vérifie que la hauteur est positive pour éviter les erreurs de dessin.
            if top_pipe_height > 0 then
                local pipe_scale_x = PIPE_WIDTH / pipe_top_image:getWidth()
                local pipe_scale_y = top_pipe_height / pipe_top_image:getHeight()
                love.graphics.draw(pipe_top_image, pipe.x, 0, 0, pipe_scale_x, pipe_scale_y)
            end
            
            -- Tuyau du bas.
            local bottom_pipe_y = pipe.gap_y + PIPE_GAP_HEIGHT / 2
            local bottom_pipe_height = WINDOW_HEIGHT - GROUND_HEIGHT - bottom_pipe_y
            -- Vérifie que la hauteur est positive pour éviter les erreurs de dessin.
            if bottom_pipe_height > 0 then
                local pipe_scale_x = PIPE_WIDTH / pipe_bottom_image:getWidth()
                local pipe_scale_y = bottom_pipe_height / pipe_bottom_image:getHeight()
                love.graphics.draw(pipe_bottom_image, pipe.x, bottom_pipe_y, 0, pipe_scale_x, pipe_scale_y)
            end
        end

        -- Dessine l'oiseau.
        love.graphics.setColor(1, 1, 1) -- L'image contient déjà sa couleur.
        local current_bird_sprite = (current_player_frame == 1) and bird_sprite_0 or bird_sprite_1
        
        -- Calcule la taille cible de l'oiseau sur l'écran.
        local bird_target_width = BIRD_RADIUS * 2.5 
        local bird_target_height = BIRD_RADIUS * 1.5 
        
        -- Calcule les facteurs de mise à l'échelle pour l'image.
        local scale_x = bird_target_width / current_bird_sprite:getWidth()
        local scale_y = bird_target_height / current_bird_sprite:getHeight()

        -- Dessine l'image de l'oiseau, centrée à sa position logique.
        love.graphics.draw(
            current_bird_sprite,
            BIRD_X, -- Position X du centre de l'oiseau.
            bird_y, -- Position Y du centre de l'oiseau.
            0,      -- Pas de rotation pour l'instant.
            scale_x, scale_y, -- Applique la mise à l'échelle.
            current_bird_sprite:getWidth() / 2, current_bird_sprite:getHeight() / 2 -- Définit l'origine du dessin au centre de l'image.
        )

        -- Affiche le score actuel.
        love.graphics.setColor(COLOR_TEXT)
        love.graphics.print("Score: " .. score, 10, 10)

        -- Affiche le message "GAME OVER" si le jeu est terminé.
        if GAME_STATE == "gameover" then
            love.graphics.setFont(love.graphics.newFont(50))
            love.graphics.setColor(1, 0.2, 0.2) -- Rouge vif pour "GAME OVER".
            love.graphics.printf("GAME OVER", 0, WINDOW_HEIGHT / 2 - 60, WINDOW_WIDTH, "center")
            love.graphics.setColor(COLOR_TEXT)
            love.graphics.setFont(love.graphics.newFont(30))
            love.graphics.printf("Score final: " .. score, 0, WINDOW_HEIGHT / 2, WINDOW_WIDTH, "center")
            love.graphics.setFont(love.graphics.newFont(25))
            love.graphics.printf("Appuyez sur 'R' pour rejouer", 0, WINDOW_HEIGHT / 2 + 50, WINDOW_WIDTH, "center")
        end
    end
end

5. La Fonction love.keypressed(key) : Gérer les Actions du Joueur

Cette fonction est appelée chaque fois qu'une touche du clavier est enfoncée. C'est le moyen le plus simple de gérer les interactions du joueur dans Love2D.

  • Saut de l'oiseau : Lorsque la touche ESPACE est pressée, l'oiseau reçoit une impulsion vers le haut.

  • Démarrage du jeu : Si le jeu est à l'écran de démarrage et que ESPACE est pressé, le jeu passe à l'état "playing".

  • Redémarrage du jeu : Si la touche R est pressée après un "GAME OVER", la fonction resetGame() est appelée pour recommencer une partie.

-- [[ love.keypressed(key) ]]
-- Cette fonction est appelée chaque fois qu'une touche du clavier est enfoncée.
-- 'key' est le nom de la touche pressée (ex: "space", "r", "left").
function love.keypressed(key)
    -- Si la touche ESPACE est pressée.
    if key == "space" then
        -- Si le jeu est à l'état "start", on passe en mode "playing" et l'oiseau fait son premier saut.
        if GAME_STATE == "start" then
            GAME_STATE = "playing"
            bird_velocity_y = BIRD_FLAP_VELOCITY 
        -- Si le jeu est déjà en mode "playing", l'oiseau fait un saut normal.
        elseif GAME_STATE == "playing" then
            bird_velocity_y = BIRD_FLAP_VELOCITY
        end
    -- Si la touche 'R' est pressée et que le jeu est terminé.
    elseif key == "r" and GAME_STATE == "gameover" then
        resetGame() -- Réinitialise le jeu pour une nouvelle partie.
    end
end

Conclusion

Voilà ! Vous avez maintenant un jeu Flappy Bird fonctionnel, le tout écrit en Lua avec Love2D. Ce projet vous a permis de toucher à des concepts essentiels de la programmation de jeux :

  • La boucle de jeu : Comment love.update() et love.draw() travaillent ensemble.

  • La gestion des états : Comment faire évoluer le jeu à travers différentes phases.

  • La physique de base : Gravité et impulsion.

  • La détection de collision : Comment les objets interagissent.

  • L'animation de sprites : Utiliser plusieurs images pour créer un mouvement.

  • L'intégration d'assets : Charger et afficher des images.

N'hésitez pas à modifier les variables, à changer les couleurs, ou même à essayer de créer de nouveaux sprites pour personnaliser davantage votre jeu. Le monde du développement de jeux est à portée de main !

 Pour jouer au jeu cliquez ici

Pour télécharger le code source complet cliquez ici sur notre page itch

  

lundi 9 juin 2025

Créez votre premier jeu avec Love2D : Le Célèbre Pong !



 

Créez votre premier jeu avec Love2D : Le Célèbre Pong ! (Guide pas à pas)

Salut les futurs développeurs et passionnés de jeux vidéo !

Aujourd'hui, sur Arcade Forge, nous allons faire nos premiers pas passionnants dans la création de jeux. Pas besoin d'être un expert en programmation ! Nous allons utiliser Love2D, un framework open-source fantastique qui rend le développement de jeux 2D accessible, simple et incroyablement amusant. Et pour commencer, quoi de mieux que l'indémodable Pong, le jeu qui a lancé l'industrie ?

Nous allons décortiquer un programme Pong complet, ajouter quelques fonctionnalités sympas, et vous donner les clés pour comprendre comment un jeu simple prend vie.


Un Petit Coup d'Histoire : La Naissance de Pong

Avant de plonger dans le code, prenons un instant pour saluer le grand-père des jeux vidéo !

Pong n'est pas juste un jeu, c'est une véritable icône culturelle. Créé par Allan Alcorn pour Atari en 1972, il est considéré comme l'un des tout premiers jeux vidéo à succès commercial. L'anecdote veut qu'Allan Alcorn, jeune ingénieur, ait été chargé par Nolan Bushnell (le co-fondateur d'Atari) de créer un "jeu simple" comme exercice. Bushnell s'attendait à ce que le projet prenne du temps et n'ait pas de suite immédiate.

Pourtant, quand le premier prototype de Pong a été installé dans un bar local, le jeu a connu un succès fou et inattendu. Les pièces de monnaie affluaient tellement que la machine a fini par se bloquer ! C'est ce succès fulgurant qui a convaincu Atari du potentiel énorme des jeux vidéo et a marqué le début d'une industrie que nous connaissons si bien aujourd'hui. Simple dans son concept (deux raquettes, une balle, un score), Pong a prouvé que la simplicité pouvait être la clé de la révolution.


Qu'est-ce que Love2D ?

Imaginez une boîte à outils magique pour créer des jeux. Love2D est exactement cela ! Il utilise le langage de programmation Lua, connu pour sa simplicité et sa légèreté, ce qui en fait un excellent choix pour les débutants. Avec Love2D, vous écrivez votre code, et il s'occupe de la partie complexe (affichage, son, gestion des entrées) pour vous.

Le Programme Pong : Anatomie d'un Jeu Simple

Votre fichier principal s'appelle toujours main.lua. Un jeu Love2D typique est structuré autour de quelques fonctions principales que Love2D appelle automatiquement. Plongeons dans le code !


1. love.load() : Le Setup de Votre Monde

C'est la première et la seule fonction que Love2D exécute au démarrage de votre jeu. C'est ici que vous préparez tout : la taille de votre fenêtre, les positions de départ de vos personnages, et le chargement de vos ressources (images, sons).

 function love.load()
    love.window.setTitle("Simple Pong Love2D")       -- Définit le titre de la fenêtre
    love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT) -- Définit la taille de la fenêtre
    
    -- Initialisation des raquettes et de la balle avec leurs positions, tailles et scores
    player1_paddle = { /* ... */ }
    player2_paddle = { /* ... */ }
    ball = { /* ... */ }
    
    -- Chargement des sons !
    collision_sound = love.audio.newSource("sounds/paddle_hit.wav", "static")
    score_sound = love.audio.newSource("sounds/score.wav", "static")

    -- NOUVEAU : Variables pour gérer l'état du son
    sound_enabled = true
    sound_status_text = "Son: ON (appuyez sur 'S' pour couper)"
end 
 

Ce que vous apprenez ici :

  • Variables globales : WINDOW_WIDTH, PADDLE_WIDTH, etc. Elles définissent les caractéristiques de votre jeu.
  • Objets de jeu : Les raquettes (player1_paddle, player2_paddle) et la balle (ball) sont créées comme des tables Lua (similaire à des objets) pour stocker leurs propriétés (position x, y, taille, etc.).
  • Chargement des sons : love.audio.newSource() charge un fichier audio. Remarquez que les chemins sont relatifs ("sounds/paddle_hit.wav") et non absolus (/sounds/...). C'est important !
  • Contrôle du son : Nous ajoutons sound_enabled pour savoir si le son est activé ou non.

2. love.update(dt) : Le Cœur Battant du Jeu

Cette fonction est appelée à chaque "frame" (chaque image) de votre jeu. C'est ici que toute la logique se déroule : le mouvement des objets, la détection des collisions, la mise à jour des scores, et la gestion des états du jeu (début, en jeu, fin de partie).

Le dt (delta time) est crucial ! Il représente le temps écoulé depuis la dernière mise à jour. En multipliant les vitesses par dt, on assure que le jeu tourne à la même vitesse, quelle que soit la puissance de l'ordinateur.

 

function love.update(dt)
    if GAME_STATE == "play" then
        -- Mouvement des raquettes (avec love.keyboard.isDown("touche") pour les pressions continues)
        if love.keyboard.isDown("up") then
            player1_paddle.y = player1_paddle.y - PADDLE_SPEED * dt
        end
        -- ... et pour le bas, l'IA de la raquette 2
        
        -- Clamper les raquettes dans les limites de l'écran (elles ne sortent pas)
        player1_paddle.y = math.max(0, math.min(WINDOW_HEIGHT - PADDLE_HEIGHT, player1_paddle.y))

        -- Mouvement de la balle
        ball.x = ball.x + ball.dx * dt
        ball.y = ball.y + ball.dy * dt

        -- Collisions avec les bords haut/bas et les raquettes
        if ball.y <= 0 or ball.y + ball.size >= WINDOW_HEIGHT then
            ball.dy = -ball.dy -- Inverse la direction
            if sound_enabled then collision_sound:play() end -- NOUVEAU : Joue le son si activé
        end
        -- ... (logique de collision avec les raquettes, également mise à jour pour le son)

        -- Gestion des points marqués
        if ball.x < 0 then
            player2_paddle.score = player2_paddle.score + 1
            if sound_enabled then score_sound:play() end -- NOUVEAU : Joue le son si activé
            resetBall()
            -- ... (vérification de la victoire)
        end
        -- ... (idem pour le joueur 1)
    end
end 

Ce que vous apprenez ici :

  • Boucle de jeu : love.update est le moteur de votre jeu.
  • Mouvement indépendant du framerate : L'utilisation de dt est une bonne pratique essentielle.
  • Gestion des entrées : love.keyboard.isDown() vérifie si une touche est maintenue enfoncée.
  • Collisions simples : On utilise des comparaisons de positions (méthode de la "bounding box" ou boîte englobante) pour savoir si la balle touche les murs ou les raquettes.
  • Contrôle du son en jeu : En ajoutant if sound_enabled then, on s'assure que le son ne se déclenche que si l'utilisateur ne l'a pas coupé.

3. love.draw() : Ce que Vous Voyez à l'Écran

Cette fonction est responsable du rendu visuel de votre jeu. Elle est appelée juste après love.update. C'est là que vous dessinez la balle, les raquettes, les scores, et tout le texte.

 
function love.draw()
    love.graphics.setColor(1, 1, 1) -- Définit la couleur de dessin (blanc)
    love.graphics.rectangle("fill", ball.x, ball.y, ball.size, ball.size) -- Dessine la balle
    -- ... (dessin des raquettes, de la ligne centrale)

    -- Affichage du score
    love.graphics.setFont(love.graphics.newFont(30)) -- Définit une police de taille 30
    love.graphics.printf(player1_paddle.score, WINDOW_WIDTH / 2 - 100, 50, 0, "right")
    -- ... (affichage du score du joueur 2)

    -- Affichage des messages d'état du jeu (début, fin de partie)
    if GAME_STATE == "start" then
        love.graphics.setFont(love.graphics.newFont(40))
        love.graphics.printf("Appuyez sur ESPACE pour commencer", 0, WINDOW_HEIGHT / 2 - 20, WINDOW_WIDTH, "center")
        -- NOUVEAU : Affichage des commandes des raquettes
        love.graphics.setFont(love.graphics.newFont(20))
        love.graphics.printf("Raquettes: Flèches HAUT/BAS", 0, WINDOW_HEIGHT / 2 + 50, WINDOW_WIDTH, "center")
    elseif GAME_STATE == "game_over" then
        -- ... (messages de fin de partie)
    end

    -- NOUVEAU : Affichage de l'état du son
    love.graphics.setFont(love.graphics.newFont(16))
    love.graphics.printf(sound_status_text, 0, 10, WINDOW_WIDTH - 20, "right")
end
 

Ce que vous apprenez ici :

  • Dessin de formes : love.graphics.rectangle("fill", ...) est votre ami pour dessiner des carrés ou rectangles.
  • Couleurs : love.graphics.setColor(r, g, b) change la couleur de dessin (valeurs entre 0 et 1).
  • Texte : love.graphics.setFont() et love.graphics.printf() permettent d'afficher du texte stylisé.
  • Informations utilisateur : Il est crucial de donner des indications claires au joueur, comme ici pour les commandes et l'état du son.

4. love.keypressed(key) : Interagir avec Votre Jeu

Cette fonction est appelée chaque fois qu'une touche est pressée une seule fois. C'est idéal pour les actions qui ne doivent se produire qu'une fois (démarrer le jeu, changer d'état, ou... couper le son !).


function love.keypressed(key)
    if GAME_STATE == "start" and key == "space" then
        GAME_STATE = "play" -- Passe en mode jeu
        resetBall()
    elseif GAME_STATE == "game_over" and key == "r" then
        -- ... (logique de redémarrage)
    elseif key == "s" then -- NOUVEAU : Touche 'S' pour le son
        sound_enabled = not sound_enabled -- Inverse l'état du son (true devient false, false devient true)
        if sound_enabled then
            love.audio.setVolume(1) -- Remet le volume global à fond
            sound_status_text = "Son: ON (appuyez sur 'S' pour couper)"
        else
            love.audio.setVolume(0) -- Coupe le volume global
            sound_status_text = "Son: OFF (appuyez sur 'S' pour activer)"
        end
    end
end
 

Ce que vous apprenez ici :

  • Actions à la pression d'une touche : Différent de love.keyboard.isDown qui est pour le mouvement continu.
  • Bascule d'état : sound_enabled = not sound_enabled est une manière très élégante de faire passer une variable de true à false et vice-versa.
  • Contrôle du volume global : love.audio.setVolume(0) coupe complètement tout le son du jeu, et love.audio.setVolume(1) le remet au maximum.

5. resetBall() : Une Fonction Utilitaire

C'est une fonction que nous avons créée nous-mêmes (elle n'est pas appelée automatiquement par Love2D). Son but est de réinitialiser la balle au centre et de lui donner une nouvelle direction aléatoire après qu'un point ait été marqué.

 
function resetBall()
    ball.x = WINDOW_WIDTH / 2
    ball.y = WINDOW_HEIGHT / 2
    ball.dx = (math.random(0, 1) == 0 and -1 or 1) * GAME_SPEED -- Direction X aléatoire
    ball.dy = (math.random(-1, 1) * 0.5 + 0.5) * GAME_SPEED * (math.random(0,1) == 0 and -1 or 1) -- Direction Y aléatoire
    -- ... (ajustements d'angle et de vitesse)
end
 

Ce que vous apprenez ici :

  • Fonctions personnalisées : Vous pouvez créer vos propres fonctions pour organiser votre code et éviter les répétitions.
  • Aléatoire : math.random() est parfait pour ajouter un peu d'imprévisibilité à votre jeu.

Le Code Complet et Final


-- main.lua - Un jeu Pong simple pour Love2D

-- Variables globales du jeu
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600

-- Vitesse du jeu (pour ajuster la difficulté)
GAME_SPEED = 200 -- pixels par seconde

-- Définition des raquettes
PADDLE_WIDTH = 20
PADDLE_HEIGHT = 100
PADDLE_SPEED = 300 -- pixels par seconde

-- Définition de la balle
BALL_SIZE = 15

-- Score
player1_score = 0
player2_score = 0

-- États du jeu
GAME_STATE = "start" -- "start", "play", "game_over"

-- Son (ajoute des fichiers .wav ou .ogg dans un dossier 'sounds')
-- Assurez-vous d'avoir des fichiers audio pour ces noms
collision_sound = love.audio.newSource("sounds/paddle_hit.wav", "static")
score_sound = love.audio.newSource("sounds/score.wav", "static")

-- Nouvelle variable pour gérer l'état du son (true = activé, false = coupé)
sound_enabled = true
-- Nouvelle variable pour le texte de contrôle du son
sound_status_text = "Son: ON (appuyez sur 'S' pour couper)"

-- Fonction love.load() : Initialisation du jeu
function love.load()
    love.window.setTitle("Simple Pong Love2D")
    love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT)
    
    -- Centrer la fenêtre si possible (dépend du système d'exploitation)
    -- love.window.setPosition(nil, nil, true)

    -- Initialisation des raquettes
    player1_paddle = {
        x = 50,
        y = WINDOW_HEIGHT / 2 - PADDLE_HEIGHT / 2,
        width = PADDLE_WIDTH,
        height = PADDLE_HEIGHT,
        score = 0
    }

    player2_paddle = {
        x = WINDOW_WIDTH - PADDLE_WIDTH - 50,
        y = WINDOW_HEIGHT / 2 - PADDLE_HEIGHT / 2,
        width = PADDLE_WIDTH,
        height = PADDLE_HEIGHT,
        score = 0
    }

    -- Initialisation de la balle
    ball = {
        x = WINDOW_WIDTH / 2,
        y = WINDOW_HEIGHT / 2,
        size = BALL_SIZE,
        -- Vecteur de vitesse initial (vers la droite ou la gauche aléatoirement)
        dx = (math.random(0, 1) == 0 and -1 or 1) * GAME_SPEED,
        dy = (math.random(-1, 1) * 0.5 + 0.5) * GAME_SPEED * (math.random(0,1) == 0 and -1 or 1) -- Légèrement aléatoire
    }
    
    -- Ajustement de l'angle initial pour ne pas être trop horizontal
    if math.abs(ball.dy) < GAME_SPEED * 0.2 then
        ball.dy = GAME_SPEED * 0.2 * (math.random(0,1) == 0 and -1 or 1)
    end
end

-- Fonction love.update(dt) : Logique de jeu, mise à jour des positions
function love.update(dt)
    if GAME_STATE == "play" then
        -- Mouvement des raquettes (contrôlé par le joueur 1)
        if love.keyboard.isDown("up") then
            player1_paddle.y = player1_paddle.y - PADDLE_SPEED * dt
        end
        if love.keyboard.isDown("down") then
            player1_paddle.y = player1_paddle.y + PADDLE_SPEED * dt
        end

        -- Mouvement de la raquette 2 (IA simple ou joueur 2)
        -- IA : suit la balle
        if ball.dy < 0 then -- Balle monte
            if player2_paddle.y > ball.y then
                player2_paddle.y = player2_paddle.y - PADDLE_SPEED * dt
            end
        else -- Balle descend
            if player2_paddle.y < ball.y then
                player2_paddle.y = player2_paddle.y + PADDLE_SPEED * dt
            end
        end

        -- Clamper les raquettes dans les limites de l'écran
        player1_paddle.y = math.max(0, math.min(WINDOW_HEIGHT - PADDLE_HEIGHT, player1_paddle.y))
        player2_paddle.y = math.max(0, math.min(WINDOW_HEIGHT - PADDLE_HEIGHT, player2_paddle.y))

        -- Mouvement de la balle
        ball.x = ball.x + ball.dx * dt
        ball.y = ball.y + ball.dy * dt

        -- Collisions avec les bords haut et bas de l'écran
        if ball.y <= 0 or ball.y + ball.size >= WINDOW_HEIGHT then
            ball.dy = -ball.dy -- Inverser la direction Y
            if sound_enabled then collision_sound:play() end -- Joue le son si activé
        end

        -- Collisions avec les raquettes
        -- Collision Player 1
        if ball.x <= player1_paddle.x + PADDLE_WIDTH and
           ball.x + ball.size >= player1_paddle.x and
           ball.y + ball.size >= player1_paddle.y and
           ball.y <= player1_paddle.y + PADDLE_HEIGHT then
            
            ball.dx = -ball.dx -- Inverser la direction X
            -- Augmenter légèrement la vitesse après chaque frappe
            ball.dx = ball.dx * 1.1
            ball.dy = ball.dy * 1.1
            
            if sound_enabled then collision_sound:play() end -- Joue le son si activé
            
            -- Ajuster l'angle en fonction de la position de frappe sur la raquette
            local hit_position = (ball.y + ball.size / 2) - (player1_paddle.y + PADDLE_HEIGHT / 2)
            ball.dy = hit_position * 5 -- Plus le coup est excentré, plus l'angle est grand

        -- Collision Player 2
        elseif ball.x + ball.size >= player2_paddle.x and
               ball.x <= player2_paddle.x + PADDLE_WIDTH and
               ball.y + ball.size >= player2_paddle.y and
               ball.y <= player2_paddle.y + PADDLE_HEIGHT then

            ball.dx = -ball.dx -- Inverser la direction X
            -- Augmenter légèrement la vitesse après chaque frappe
            ball.dx = ball.dx * 1.1
            ball.dy = ball.dy * 1.1

            if sound_enabled then collision_sound:play() end -- Joue le son si activé

            -- Ajuster l'angle en fonction de la position de frappe sur la raquette
            local hit_position = (ball.y + ball.size / 2) - (player2_paddle.y + PADDLE_HEIGHT / 2)
            ball.dy = hit_position * 5
        end

        -- Point marqué
        if ball.x < 0 then
            player2_paddle.score = player2_paddle.score + 1
            if sound_enabled then score_sound:play() end -- Joue le son si activé
            resetBall()
            if player2_paddle.score >= 5 then
                GAME_STATE = "game_over"
            end
        elseif ball.x > WINDOW_WIDTH then
            player1_paddle.score = player1_paddle.score + 1
            if sound_enabled then score_sound:play() end -- Joue le son si activé
            resetBall()
            if player1_paddle.score >= 5 then
                GAME_STATE = "game_over"
            end
        end

    elseif GAME_STATE == "start" or GAME_STATE == "game_over" then
        -- Attendre l'entrée du joueur pour commencer/recommencer
    end
end

-- Fonction love.draw() : Dessin des éléments à l'écran
function love.draw()
    -- Dessin de la balle
    love.graphics.setColor(1, 1, 1) -- Blanc
    love.graphics.rectangle("fill", ball.x, ball.y, ball.size, ball.size)

    -- Dessin des raquettes
    love.graphics.rectangle("fill", player1_paddle.x, player1_paddle.y, player1_paddle.width, player1_paddle.height)
    love.graphics.rectangle("fill", player2_paddle.x, player2_paddle.y, player2_paddle.width, player2_paddle.height)

    -- Dessin de la ligne centrale (optionnel)
    for i = 0, WINDOW_HEIGHT, 20 do
        love.graphics.rectangle("fill", WINDOW_WIDTH / 2 - 2.5, i, 5, 10)
    end

    -- Affichage du score
    love.graphics.setFont(love.graphics.newFont(30)) -- Définit une police de taille 30
    love.graphics.printf(player1_paddle.score, WINDOW_WIDTH / 2 - 100, 50, 0, "right")
    love.graphics.printf(player2_paddle.score, WINDOW_WIDTH / 2 + 100, 50, 0, "left")

    -- Affichage du message d'état du jeu
    if GAME_STATE == "start" then
        love.graphics.setFont(love.graphics.newFont(40))
        love.graphics.printf("Appuyez sur ESPACE pour commencer", 0, WINDOW_HEIGHT / 2 - 20, WINDOW_WIDTH, "center")
        -- Affichage de la mention pour les commandes
        love.graphics.setFont(love.graphics.newFont(20))
        love.graphics.printf("Raquettes: Flèches HAUT/BAS", 0, WINDOW_HEIGHT / 2 + 50, WINDOW_WIDTH, "center")
    elseif GAME_STATE == "game_over" then
        love.graphics.setFont(love.graphics.newFont(40))
        local winner = ""
        if player1_paddle.score > player2_paddle.score then
            winner = "Joueur 1 gagne !"
        else
            winner = "Joueur 2 gagne !"
        end
        love.graphics.printf(winner, 0, WINDOW_HEIGHT / 2 - 40, WINDOW_WIDTH, "center")
        love.graphics.printf("Appuyez sur R pour rejouer", 0, WINDOW_HEIGHT / 2 + 20, WINDOW_WIDTH, "center")
    end

    -- Affichage de l'état du son (en haut à droite, par exemple)
    love.graphics.setFont(love.graphics.newFont(16))
    love.graphics.printf(sound_status_text, 0, 10, WINDOW_WIDTH - 20, "right")
end

-- Fonction love.keypressed(key) : Gestion des événements clavier
function love.keypressed(key)
    if GAME_STATE == "start" and key == "space" then
        GAME_STATE = "play"
        resetBall()
    elseif GAME_STATE == "game_over" and key == "r" then
        GAME_STATE = "play"
        player1_paddle.score = 0
        player2_paddle.score = 0
        resetBall()
    elseif key == "s" then
        -- Bascule l'état du son
        sound_enabled = not sound_enabled
        if sound_enabled then
            love.audio.setVolume(1) -- Rétablit le volume global à 1 (max)
            sound_status_text = "Son: ON (appuyez sur 'S' pour couper)"
        else
            love.audio.setVolume(0) -- Coupe le volume global
            sound_status_text = "Son: OFF (appuyez sur 'S' pour activer)"
        end
    end
end

-- Fonction utilitaire pour réinitialiser la balle après un point
function resetBall()
    ball.x = WINDOW_WIDTH / 2
    ball.y = WINDOW_HEIGHT / 2
    ball.dx = (math.random(0, 1) == 0 and -1 or 1) * GAME_SPEED
    ball.dy = (math.random(-1, 1) * 0.5 + 0.5) * GAME_SPEED * (math.random(0,1) == 0 and -1 or 1)
    
    -- Ajustement de l'angle initial pour ne pas être trop horizontal
    if math.abs(ball.dy) < GAME_SPEED * 0.2 then
        ball.dy = GAME_SPEED * 0.2 * (math.random(0,1) == 0 and -1 or 1)
    end
    
    -- Réinitialiser la vitesse de base de la balle après chaque point
    ball.dx = math.abs(ball.dx) / (math.abs(ball.dx) / GAME_SPEED) * (ball.dx > 0 and 1 or -1)
    ball.dy = math.abs(ball.dy) / (math.abs(ball.dy) / GAME_SPEED) * (ball.dy > 0 and 1 or -1)

end 

N'hésitez pas à modifier les valeurs numériques, à changer les couleurs, à essayer de nouvelles idées. C'est en faisant qu'on apprend le mieux !

Rendez-vous sur le site officiel de Love2D pour télécharger le framework et consulter la documentation. Et partagez vos créations avec la communauté Arcade Forge !

Bon codage et amusez-vous !

Cliquez ici pour jouer à Pong !

Le Phénomène Pac-Man : L'icône qui a dévoré le monde de l'arcade

À l'époque où les bornes d'arcade étaient dominées par les vaisseaux spatiaux et les tirs de laser, un petit personnage rond et jaun...