Aller au contenu
BackJavajeux-vidéosRetroGaming

Papa, dessine-moi un jeu vidéo

Créer un jeu vidéo peut sembler complexe. Pourtant, l’apprentissage passe par la simplicité. Avec Pong, jeu culte et intemporel, on découvre les bases du développement tout en partageant un moment entre générations. Un projet minimaliste, pédagogique et toujours efficace.

Un match de pong endiablé

Quand on parle de créer un jeu vidéo avec un enfant, on imagine parfois tout de suite des mondes immenses, des graphismes complexes, des moteurs sophistiqués.

Mais pour apprendre, il faut parfois faire l’inverse.

Revenir à l’essentiel.

Et s’il y a bien un jeu qui traverse les générations, les langages et les supports, c’est PONG.

Mais pourquoi PONG ?

PONG, c’est la simplicité même :

  • Deux raquettes
  • Une balle
  • Un terrain
  • Des règles simples

Et pourtant, c’est déjà un jeu vidéo complet.

C’est aussi un jeu que beaucoup d’entre nous ont déjà programmé, un rite de passage :

  • En C ou C++ en cours d’informatique (levez la main si vous êtes passé par là)
  • Sur calculatrice Texas Instruments pour passer le temps en cours de maths 🤫
On pouvait en coder des choses sur cette machine mine de rien
  • Comme premier projet d’apprentissage
  • Parfois même plusieurs fois, dans différents langages

Le programmer aujourd’hui avec son enfant, c’est créer un pont entre générations.

Mais au fait, c'est quoi PONG ?

Sorti en 1972, Pong est souvent considéré comme le premier grand succès commercial du jeu vidéo, même s’il n’est pas techniquement le tout premier jeu vidéo de l’histoire. Son créateur, Nolan Bushnell, cofondateur d’Atari, avait une vision simple mais ambitieuse : transformer une technologie encore confidentielle en un divertissement accessible au grand public.

Pour concrétiser cette idée, Bushnell confie le projet à un jeune ingénieur, Allan Alcorn. L’objectif initial est presque anodin : créer un jeu simple inspiré du tennis de table, dans lequel deux joueurs renvoient une balle à l’aide de palettes. Le concept paraît minimaliste, mais il est redoutablement efficace. Très vite, Pong devient un phénomène culturel, notamment grâce à son gameplay immédiat, compréhensible en quelques secondes par n’importe qui (y compris de jeunes enfants).

On connaît tous cet écran

La première borne est installée dans un bar californien en 1972. Quelques jours plus tard, le patron appelle Atari pour signaler une panne… qui n’en était pas une : la machine était simplement saturée de pièces de monnaie. Ce succès fulgurant confirme qu’Atari tient quelque chose d’unique.

"Sa place est dans un musée", dirait un célèbre archéologue

Construire PONG en Java - comprendre le programme bloc par bloc

La fenêtre et le panneau principal

On commence par créer une classe qui hérite de JPanel et qui va tout gérer : affichage, mise à jour, entrées clavier.

public class Pong extends JPanel implements ActionListener {
    private static final int WINDOW_WIDTH = 800;
    private static final int WINDOW_HEIGHT = 600;

    public Pong() {
        setPreferredSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
        setBackground(Color.BLACK);

        Timer timer = new Timer(16, this); // ~60 FPS
        timer.start();

        addKeyListener(...);
        setFocusable(true);
    }
    
    // ... suite du code
}

C’est le cœur du programme : un panneau noir, un timer pour les mises à jour fluides, et la possibilité de capter les touches.

Le choix de swing est délibéré pour garder un côté rétro.

Les constantes du jeu (les tailles et vitesses)

Regrouper toutes les valeurs fixes au début permet de les modifier facilement.

private static final int PADDLE_WIDTH = 10;
private static final int PADDLE_HEIGHT = 60;
private static final int BALL_SIZE = 15;
private static final int PADDLE_SPEED = 6;
private static final int AI_PADDLE_SPEED = 5;
private static final int PADDLE_X_OFFSET = 50;

private static final int INITIAL_BALL_SPEED = 5;
private static final double BALL_SPEED_INCREASE_FACTOR = 1.05;
private static final double MAX_BALL_SPEED = 15;
private static final int MAX_BOUNCE_Y_SPEED = 7;

private static final int GAME_DURATION_MS = 60 * 1000; // 1 minute

Les variables d’état du jeu

On utilise des doubles pour la balle afin d’avoir une meilleure précision lors des petits déplacements.

private double ballX = WINDOW_WIDTH / 2.0;
private double ballY = WINDOW_HEIGHT / 2.0;
private double ballXSpeed = INITIAL_BALL_SPEED;
private double ballYSpeed = INITIAL_BALL_SPEED;

private int paddle1Y = WINDOW_HEIGHT / 2 - PADDLE_HEIGHT / 2;
private int paddle2Y = WINDOW_HEIGHT / 2 - PADDLE_HEIGHT / 2;

private int score1 = 0;
private int score2 = 0;

private boolean p1_up, p1_down, p2_up, p2_down;
private boolean soloMode = false;
private boolean gameStarted = false;
private boolean gameOver = false;

Le menu de sélection de mode

Avant de lancer la partie, on affiche un petit menu avec deux choix : Solo (vs IA) ou 2 joueurs (notre cible pour jouer avec nos chères têtes blondes).

private int menuSelection = 0; // 0 = Solo, 1 = 2 joueurs

private void handleMenuKeyPress(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_UP) {
        menuSelection = 0;
    } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
        menuSelection = 1;
    } else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
        soloMode = (menuSelection == 0);
        gameStarted = true;
        startTime = System.currentTimeMillis();
    }
}

L’affichage se fait dans drawMenu() avec un curseur jaune pour indiquer la sélection.

Gestion des touches (contrôles)

On utilise des booléens pour savoir quelles touches sont enfoncées (plus fluide qu’un switch à chaque frame).

Joueur 1 → Z (haut) / S (bas)
Joueur 2 → Flèche haut / Flèche bas

private void handleInGameKeyPress(KeyEvent e) {
    switch (e.getKeyCode()) {
        case KeyEvent.VK_Z:    p1_up   = true; break;
        case KeyEvent.VK_S:    p1_down = true; break;
        case KeyEvent.VK_UP:   p2_up   = true; break;
        case KeyEvent.VK_DOWN: p2_down = true; break;
    }
}

Et on les remet à false dans keyReleased.

La boucle principale - actionPerformed

C’est ici que tout se passe 60 fois par seconde :

@Override
public void actionPerformed(ActionEvent e) {
    if (gameStarted && !gameOver) {
        updateGame();
    }
    repaint();
}

private void updateGame() {
    if (System.currentTimeMillis() - startTime >= GAME_DURATION_MS) {
        gameOver = true;
        return;
    }
    movePaddles();
    moveBall();
    checkCollisions();
}

Déplacement des raquettes

Séparé en deux cas : joueur ou IA.

private void movePaddles() {
    // Joueur 1 (gauche)
    if (p1_up   && paddle1Y > 0)               paddle1Y -= PADDLE_SPEED;
    if (p1_down && paddle1Y < WINDOW_HEIGHT - PADDLE_HEIGHT) paddle1Y += PADDLE_SPEED;

    if (soloMode) {
        // IA très simple (suit la balle)
        if (paddle2Y + PADDLE_HEIGHT / 2 < ballY) paddle2Y += AI_PADDLE_SPEED;
        if (paddle2Y + PADDLE_HEIGHT / 2 > ballY) paddle2Y -= AI_PADDLE_SPEED;
    } else {
        // Joueur 2 (droite)
        if (p2_up   && paddle2Y > 0)               paddle2Y -= PADDLE_SPEED;
        if (p2_down && paddle2Y < WINDOW_HEIGHT - PADDLE_HEIGHT) paddle2Y += PADDLE_SPEED;
    }
}

Déplacement et rebonds de la balle

La physique de base est très simple :

private void moveBall() {
    ballX += ballXSpeed;
    ballY += ballYSpeed;
}

private void checkCollisions() {
    // Haut et bas
    if (ballY <= 0 || ballY >= WINDOW_HEIGHT - BALL_SIZE) {
        ballYSpeed = -ballYSpeed;
    }

    // Collision raquettes → (voir partie suivante)

    // Sortie gauche → point pour joueur 2
    if (ballX <= 0) {
        score2++;
        resetBall();
    }
    // Sortie droite → point pour joueur 1
    if (ballX >= WINDOW_WIDTH - BALL_SIZE) {
        score1++;
        resetBall();
    }
}

Collision avec les raquettes (point clé du feeling)

C’est la partie la plus importante pour le gameplay :

private void handlePaddleCollision(int paddleY, boolean isLeftPaddle) {
    // Inversion et accélération
    if (Math.abs(ballXSpeed) < MAX_BALL_SPEED) {
        ballXSpeed *= -BALL_SPEED_INCREASE_FACTOR;
    } else {
        ballXSpeed = -ballXSpeed;
    }

    // Angle selon l'endroit où on touche la raquette
    double relativeIntersectY = (paddleY + PADDLE_HEIGHT / 2.0) - (ballY + BALL_SIZE / 2.0);
    double normalized = relativeIntersectY / (PADDLE_HEIGHT / 2.0);
    ballYSpeed = normalized * -MAX_BOUNCE_Y_SPEED;

    // Anti-colle
    ballX = isLeftPaddle 
        ? PADDLE_X_OFFSET + PADDLE_WIDTH 
        : WINDOW_WIDTH - PADDLE_X_OFFSET - PADDLE_WIDTH - BALL_SIZE;
}

Calcul de l’angle de rebond

// 1. Point d’impact relatif (centre raquette - centre balle)
double relativeY = (paddleY + PADDLE_HEIGHT / 2.0) - (ballY + BALL_SIZE / 2.0);

// 2. Normalisation → valeur entre -1 (haut) et +1 (bas)
double normalized = relativeY / (PADDLE_HEIGHT / 2.0);

// 3. Vitesse Y proportionnelle → plus on touche loin du centre, plus l’angle est fort
ballYSpeed = normalized * -MAX_BOUNCE_Y_SPEED;   // le - inverse haut/bas
       Haut ──────────────  normalized ≈ -1.0  → ballYSpeed ≈ +7   (monte fort)
            │
            │   Un peu haut   ≈ -0.5         → +3.5
            │
   MILIEU ──┼─────────────────  0.0          → 0 (horizontal)
            │
            │   Un peu bas     ≈ +0.5         → -3.5
            │
       Bas  ──────────────   +1.0            → -7   (descend fort)

Plus la balle touche loin du centre de la raquette → plus l’angle est prononcé.
La formule transforme la position relative en vitesse verticale contrôlable.

Conclusion

Programmer Pong aujourd’hui, ce n’est pas seulement écrire quelques lignes de code.

C’est comprendre comment un jeu vit, réagit, et devient amusant avec presque rien : un peu de logique, un peu de mathématiques, et beaucoup d’essais.

C’est aussi redécouvrir une époque où la contrainte technique obligeait à aller à l’essentiel. Là où chaque pixel comptait. Là où chaque comportement devait être pensé simplement, mais intelligemment.

Et c’est peut-être ça, la vraie richesse d’un projet comme Pong.


Si vous souhaitez retrouver nos autres articles sur les jeux vidéo je vous conseille :

Tetris : un classique intemporel à redécouvrir par le code
Né en pleine Guerre froide, Tetris a su conquérir le monde grâce à la simplicité de ses mécaniques et la profondeur de son gameplay. Dans cet article, nous revenons sur son histoire fascinante et vous montrons comment lui donner vie en Java, bloc par bloc.
Snake : un classique intemporel à redécouvrir par le code
Né sur les écrans monochromes des téléphones d’antan, Snake est bien plus qu’un simple jeu d’arcade : c’est une madeleine de Proust pour toute une génération. Cet article vous propose de plonger dans l’histoire fascinante du serpent numérique, puis d’explorer comment lui donner vie en Python.
Anatomie d’un Démineur : recoder sa nostalgie en Angular
Nouvelle série estivale sur sfeir.dev : on démonte les jeux de notre enfance, on les reconstruit pièce par pièce… et on en profite pour explorer des frameworks modernes. Dans ce premier épisode, Anatomie d’un Démineur 🧨, je m’attelle à recoder ma nostalgie avec Angular

Et si vous souhaitez jeter un coup d'oeil au code c'est par ici que ça se passe :

GitHub - ErwanLT/RetroGaming: retro gaming style arcade games
retro gaming style arcade games. Contribute to ErwanLT/RetroGaming development by creating an account on GitHub.

Dernier