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 !

Aucun commentaire:

Enregistrer un commentaire

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