Développement d'un Jeu de Course Infini en Java Swing

Cet article explore la conception et la mise en œuvre d'un jeu de course de type "runner infini" en utilisant l'API Java Swing. Le projet suit le modèle architectural MVC pour une meilleure organisation du code et une séparation claire des responsabilités.

1. Aperçu du Projet : Jeu de Course Infini (RunQuest)

Type de jeu : Jeu de plateforme à défilement horizontal.

Objectif : Le joueur contrôle un personnage qui doit éviter les obstacles et collecter des objets pour accumuler des points et parcourir la plus longue distance possible.

Modules Fonctionnels :

  1. Écran de Connexion :
    • Champs de saisie pour nom d'utilisateur (visible) et mot de passe (masqué).
    • Boutons de connexion et d'annulation.
  2. Menu Principal :
    • Bouton "Démarrer le Jeu" (graphique).
    • Bouton "Aide".
    • Bouton "Quitter".
  3. Écran de Chargement :
    • Barre de progression visuelle pour simuler le chargement du jeu.
    • Transition automatique vers l'écran de jeu une fois le chargement terminé.
  4. Interface de Jeu Principale :
    • Fond d'écran animé défilant.
    • Personnage principal animé et contrôlable via le clavier (haut, bas, gauche, droite).
    • Apparition continue de divers types d'obstacles.
    • Détection des collisions entre le joueur et les obstacles.
    • Fonctionnalités de pause et de reprise.
  5. Écran de Fin de Partie :
    • Affichage du score final et de la distance parcourue.
    • Options pour rejouer, revenir au menu principal ou quitter le jeu.

Architecture Technique :

Le projet est structuré selon le modèle MVC (Modèle-Vue-Contrôleur) :

  • Modèle (Model) : Gère les données du jeu et les entités (personnage, obstacles, score).
  • Vue (View) : Responsable de l'affichage graphique de l'interface utilisateur.
  • Contrôleur (Controller) : Contient la logique du jeu, gère les interactions utilisateur et met à jour le modèle et la vue.

Convention de nommage des packages : fr.game.runner.ui pour les composants de la vue, fr.game.runner.logic pour le contrôleur, et fr.game.runner.model pour le modèle.

2. Interface de Connexion

L'écran de connexion est le point d'entrée du jeu, permettant l'authentification de l'utilisateur. Nous allons détailler l'implémentation de cette fenêtre Swing.

Configuration Initiale

Avant de coder, assurez-vous que les ressources graphiques (images) sont disponibles. Copiez les fichiers images nécessaires dans un répertoire "images" à la racine de votre projet ou dans le dossier src de votre environnement de développement (par exemple, Eclipse).

Le code suivant met en œuvre la fenêtre de connexion :

package fr.game.runner.ui;

import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;

public class LoginWindow extends JFrame {
    private JLabel usernameLabel;
    private JTextField usernameField;
    private JLabel passwordLabel;
    private JPasswordField passwordField;
    private JButton connectButton;
    private JButton cancelButton;

    public LoginWindow() {
        // Initialisation des étiquettes et champs
        usernameLabel = new JLabel("Nom d'utilisateur :");
        usernameLabel.setFont(new Font("SansSerif", Font.BOLD, 18));

        passwordLabel = new JLabel("Mot de passe :");
        passwordLabel.setFont(new Font("SansSerif", Font.BOLD, 18));

        // Positionnement des composants (layout absolu)
        setLayout(null); // Désactiver le gestionnaire de mise en page par défaut
        usernameLabel.setBounds(50, 220, 150, 30);
        add(usernameLabel);
        passwordLabel.setBounds(50, 280, 150, 30);
        add(passwordLabel);

        // Champs de saisie
        usernameField = new JTextField();
        usernameField.setBounds(200, 220, 180, 30);
        usernameField.setBorder(BorderFactory.createLoweredBevelBorder());
        usernameField.setOpaque(false);
        add(usernameField);

        passwordField = new JPasswordField();
        passwordField.setBounds(200, 280, 180, 30);
        passwordField.setBorder(BorderFactory.createLoweredBevelBorder());
        passwordField.setOpaque(false);
        add(passwordField);

        // Boutons
        connectButton = new JButton("Connexion");
        connectButton.setBounds(120, 350, 100, 36);
        connectButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String inputUsername = usernameField.getText();
                String inputPassword = new String(passwordField.getPassword());

                if (inputUsername.isEmpty() || inputPassword.isEmpty()) {
                    JOptionPane.showMessageDialog(null, "Les champs ne peuvent pas être vides !", "Erreur de saisie", JOptionPane.WARNING_MESSAGE);
                } else if ("player".equals(inputUsername) && "secret".equals(inputPassword)) {
                    JOptionPane.showMessageDialog(null, "Bienvenue " + inputUsername + " dans RunQuest !", "Connexion réussie", JOptionPane.INFORMATION_MESSAGE);
                    new MainMenuWindow().setVisible(true); // Passer au menu principal
                    dispose(); // Fermer la fenêtre de connexion
                } else {
                    JOptionPane.showMessageDialog(null, "Identifiant ou mot de passe incorrect. Veuillez réessayer.", "Erreur de connexion", JOptionPane.ERROR_MESSAGE);
                }
            }
        });
        add(connectButton);

        cancelButton = new JButton("Annuler");
        cancelButton.setBounds(240, 350, 100, 36);
        cancelButton.addActionListener(e -> dispose()); // Fermer la fenêtre
        add(cancelButton);

        // Panel de fond
        BackgroundPanel backgroundPanel = new BackgroundPanel();
        add(backgroundPanel);

        // Propriétés de la fenêtre
        setTitle("Connexion à RunQuest");
        setSize(800, 480);
        setLocationRelativeTo(null); // Centrer la fenêtre
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setUndecorated(true); // Supprimer la barre de titre
        setIconImage(new ImageIcon("images/app_icon.png").getImage());
        setVisible(true);
    }

    public static void main(String[] args) {
        new LoginWindow();
    }

    // Classe interne pour le panneau de fond
    class BackgroundPanel extends JPanel {
        private Image backgroundImage;

        public BackgroundPanel() {
            try {
                backgroundImage = ImageIO.read(new File("images/login_bg.jpg"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (backgroundImage != null) {
                g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this);
            }
        }
    }
}

3. Interface du Menu Principal

Après une connexion réussie, le joueur est dirigé vers le menu principal. Cette interface présente les options pour démarrer le jeu, consulter l'aide ou quitter l'application. Une interaction visuelle est ajoutée aux boutons lors du survol de la souris.

Exigences Spécifiques :

  • Les boutons (Démarrer, Aide, Quitter) changent d'apparence (plus clair/plus foncé) au survol de la souris.
  • Le bouton "Quitter" ferme l'application.

Voici le code pour la fenêtre du menu principal :

package fr.game.runner.ui;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import fr.game.runner.logic.LoadingController;

public class MainMenuWindow extends JFrame implements MouseListener {
    private JLabel startButton;
    private JLabel helpButton;
    private JLabel exitButton;

    public MainMenuWindow() {
        // Initialisation des boutons avec images
        startButton = new JLabel(new ImageIcon("images/start_inactive.png"));
        startButton.setBounds(350, 320, 150, 40);
        startButton.addMouseListener(this);
        add(startButton);

        helpButton = new JLabel(new ImageIcon("images/help_inactive.png"));
        helpButton.setBounds(350, 420, 150, 40);
        helpButton.addMouseListener(this);
        add(helpButton);

        exitButton = new JLabel(new ImageIcon("images/exit_inactive.png"));
        exitButton.setBounds(350, 520, 150, 40);
        exitButton.addMouseListener(this);
        add(exitButton);

        // Panel de fond
        MainBackgroundPanel panel = new MainBackgroundPanel();
        add(panel);

        // Configuration de la fenêtre
        setTitle("Menu Principal RunQuest");
        setSize(1200, 730);
        setLocationRelativeTo(null);
        setUndecorated(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new ImageIcon("images/app_icon.png").getImage());
        setVisible(true);
    }

    public static void main(String[] args) {
        new MainMenuWindow();
    }

    // Classe interne pour le panneau de fond du menu
    class MainBackgroundPanel extends JPanel {
        private Image backgroundImage;

        public MainBackgroundPanel() {
            try {
                backgroundImage = ImageIO.read(new File("images/menu_background.jpg"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (backgroundImage != null) {
                g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this);
            }
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getSource() == startButton) {
            new LoadingController().startLoading(); // Lance l'écran de chargement
            dispose(); // Ferme la fenêtre du menu
        } else if (e.getSource() == exitButton) {
            System.exit(0); // Quitte l'application
        } else if (e.getSource() == helpButton) {
            JOptionPane.showMessageDialog(this, "Contactez le support pour toute question.", "Aide", JOptionPane.INFORMATION_MESSAGE);
        }
    }

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {
        // Changer l'image du bouton au survol
        if (e.getSource() == startButton) {
            startButton.setIcon(new ImageIcon("images/start_active.png"));
        } else if (e.getSource() == helpButton) {
            helpButton.setIcon(new ImageIcon("images/help_active.png"));
        } else if (e.getSource() == exitButton) {
            exitButton.setIcon(new ImageIcon("images/exit_active.png"));
        }
    }

    @Override
    public void mouseExited(MouseEvent e) {
        // Rétablir l'image originale quand la souris quitte le bouton
        if (e.getSource() == startButton) {
            startButton.setIcon(new ImageIcon("images/start_inactive.png"));
        } else if (e.getSource() == helpButton) {
            helpButton.setIcon(new ImageIcon("images/help_inactive.png"));
        } else if (e.getSource() == exitButton) {
            exitButton.setIcon(new ImageIcon("images/exit_inactive.png"));
        }
    }
}

4. Écran de Chargement du Jeu

Pour une transition fluide et pour simuler un temps de chargement, une interface de progression est implémentée. Ce module utilise les threads pour animer une barre de progression, rendant l'expérience utilisateur plus agréable.

Concepts Clés : Threads

Un thread représente un chemin d'exécution au sein d'un programme. Java permet l'exécution simultanée de plusieurs threads, ce qui est idéal pour les tâches de fond comme le chargement, sans bloquer l'interface utilisateur.

États d'un Thread :

  • Nouveau (New) : Le thread est créé mais pas encore démarré.
  • Exécutable (Runnable) : Le thread est prêt à s'exécuter, en attente de l'ordonnanceur.
  • Bloqué (Blocked) : Le thread attend un verrou.
  • Attente (Waiting) : Le thread attend qu'un autre thread effectue une action ou une notification.
  • Attente chronométrée (Timed Waiting) : Le thread attend pendant une durée spécifiée (par exemple, avec Thread.sleep()).
  • Terminé (Terminated) : Le thread a fini son exécution.

Création d'un Thread :

  1. Implémenter l'interface Runnable.
  2. Étendre la classe Thread.

Nous utilisons l'approche Runnable pour notre écran de chargement :

package fr.game.runner.logic;

import java.awt.BorderLayout;
import java.awt.Color;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JProgressBar;

import fr.game.runner.ui.GameScreen; // Import de l'écran de jeu

public class LoadingController extends JFrame implements Runnable {
    private JLabel backgroundLabel;
    private JProgressBar progressBar;

    public void startLoading() {
        LoadingController loadingWindow = new LoadingController();
        Thread loadingThread = new Thread(loadingWindow);
        loadingThread.start();
        // Ne pas fermer immédiatement la fenêtre appelante, la fermeture est gérée par la fenêtre de chargement
    }

    public LoadingController() {
        backgroundLabel = new JLabel(new ImageIcon("images/loading_bg.jpg"));
        add(BorderLayout.CENTER, backgroundLabel); // Centrer l'image de fond

        progressBar = new JProgressBar();
        progressBar.setStringPainted(true); // Afficher le pourcentage
        progressBar.setBackground(new Color(255, 140, 0)); // Couleur orange
        progressBar.setForeground(new Color(0, 191, 255)); // Couleur de progression
        add(BorderLayout.SOUTH, progressBar); // Barre de progression en bas

        // Configuration de la fenêtre de chargement
        setTitle("Chargement du jeu...");
        setSize(600, 360);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // Empêcher la fermeture pendant le chargement
        setUndecorated(true);
        setIconImage(new ImageIcon("images/app_icon.png").getImage());
        setVisible(true);
    }

    public static void main(String[] args) {
        new LoadingController().startLoading();
    }

    @Override
    public void run() {
        int[] progressValues = {0, 5, 15, 25, 35, 45, 55, 65, 75, 85, 90, 95, 98, 99, 100};
        for (int i = 0; i < progressValues.length; i++) {
            progressBar.setValue(progressValues[i]);
            try {
                Thread.sleep(150 + (i * 10)); // Simuler un chargement plus réaliste
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Restaurer l'état interrompu
                e.printStackTrace();
            }
        }
        // Une fois le chargement terminé, passer au jeu
        new GameScreen().setVisible(true);
        dispose(); // Fermer la fenêtre de chargement
    }
}

5. Interface Principale du Jeu

C'est le cœur du jeu, où toute l'action se déroule. Il intègre un défilement de fond, l'animation du personnage, la génération d'obstacles, la détection des collisions et les contrôles de jeu.

1. Fenêtre de Jeu (GameScreen.java)

Cette fenêtre sert de conteneur principal pour le panneau de jeu, où toute la logique de rendu et de mise à jour s'exécute.

package fr.game.runner.ui;

import javax.swing.ImageIcon;
import javax.swing.JFrame;

import fr.game.runner.logic.GameController;

public class GameScreen extends JFrame {
    public static final int SCREEN_WIDTH = 1500;
    public static final int SCREEN_HEIGHT = 900;

    public GameScreen() {
        GameController gamePanel = new GameController();
        gamePanel.startGameLoop(); // Démarrer la boucle de jeu
        addKeyListener(gamePanel); // Écouter les événements clavier sur le panneau
        add(gamePanel);

        // Configuration de la fenêtre
        setTitle("RunQuest");
        setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new ImageIcon("images/app_icon.png").getImage());
        setUndecorated(true);
        setVisible(true);
    }

    public static void main(String[] args) {
        new GameScreen();
    }
}

2. Panneau de Jeu (GameController.java - La Logique Principale)

Ce panneau contient la logique de jeu cruciale :

  • Défilement du fond : Utilise deux images identiques côte à côte pour créer un effet de défilement continu.
  • Animation du personnage : Change les images du personnage à intervalles réguliers pour simuler la course.
  • Génération d'obstacles : Introduit aléatoirement divers obstacles et collectables à l'écran.
  • Détection et gestion des collisions : Vérifie les superpositions entre le joueur et les objets.
  • Fonctionnalités de pause/reprise : Permet au joueur de mettre le jeu en pause avec une touche.

Animation du Personnage : Semblable aux dessins animés traditionnels, le personnage est animé par une séquence d'images (sprites) affichées rapidement. Ici, neuf images sont utilisées pour simuler la course.

Voici l'implémentation des entités de jeu (Personnage et Obstacles) et la classe du contrôleur de jeu :

Entité Personnage (PlayerCharacter.java)
package fr.game.runner.model;

import java.awt.Graphics;
import java.awt.Image;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import fr.game.runner.ui.GameScreen;

public class PlayerCharacter {
    private Image currentSprite;
    private Image[] runSprites;

    public static final int CHAR_WIDTH = 120;
    public static final int CHAR_HEIGHT = 120;

    private int posX, posY;
    private int animationIndex;
    private int currentScore;
    private int distanceTraveled;

    public PlayerCharacter() {
        loadSprites();
        currentSprite = runSprites[0];
        posX = 90;
        posY = 580; // Position au sol
        animationIndex = 0;
        currentScore = 0;
        distanceTraveled = 0;
    }

    private void loadSprites() {
        runSprites = new Image[9];
        for (int i = 0; i < runSprites.length; i++) {
            try {
                runSprites[i] = ImageIO.read(new File("images/player/" + (i + 1) + ".png"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void applyGravity() {
        posY += 5;
        if (posY >= 580) {
            posY = 580; // Limite le joueur au sol
        }
    }

    public void animateStep() {
        // Changer le sprite pour l'animation de course
        currentSprite = runSprites[(animationIndex++ / 3) % runSprites.length];
        distanceTraveled += 1; // Incrémenter la distance parcourue
    }

    public void render(Graphics g) {
        g.drawImage(currentSprite, posX, posY, CHAR_WIDTH, CHAR_HEIGHT, null);
    }

    public boolean isOffScreen() {
        return this.posX >= GameScreen.SCREEN_WIDTH || this.posX <= -CHAR_WIDTH;
    }

    // Getters et Setters (omissions pour concision, mais nécessaires)
    public int getPosX() { return posX; }
    public void setPosX(int posX) { this.posX = posX; }
    public int getPosY() { return posY; }
    public void setPosY(int posY) { this.posY = posY; }
    public int getScore() { return currentScore; }
    public void setScore(int score) { this.currentScore = score; }
    public int getDistance() { return distanceTraveled; }
    public void setDistance(int distance) { this.distance = distance; }
    public int getTotalScore() { return currentScore + (distanceTraveled / 100); }
}

Obstacle Crabe (CrabObstacle.java)
package fr.game.runner.model;

import java.awt.Graphics;
import java.awt.Image;
import java.io.File;
import javax.imageio.ImageIO;
import fr.game.runner.ui.GameScreen;

public class CrabObstacle {
    private Image currentImage;
    private Image[] animationImages;
    public static final int CRAB_WIDTH = 100;
    public static final int CRAB_HEIGHT = 110;
    private int posX, posY;
    private int frameIndex;
    private int movementSpeed;

    public CrabObstacle() {
        animationImages = new Image[2];
        try {
            animationImages[0] = ImageIO.read(new File("images/obstacles/crab_open.png"));
            animationImages[1] = ImageIO.read(new File("images/obstacles/crab_closed.png"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        currentImage = animationImages[0];
        posX = GameScreen.SCREEN_WIDTH + 100;
        posY = 580;
        movementSpeed = 30;
        frameIndex = 0;
    }

    public void updatePosition() {
        currentImage = animationImages[(frameIndex++ / 5) % animationImages.length];
        posX -= movementSpeed;
    }

    public void render(Graphics g) {
        g.drawImage(currentImage, posX, posY, CRAB_WIDTH, CRAB_HEIGHT, null);
    }

    public boolean isBeyondScreen() {
        return this.posX <= -CRAB_WIDTH;
    }

    // Getters (omissions pour concision)
    public int getPosX() { return posX; }
    public int getPosY() { return posY; }
    public static int getWidth() { return CRAB_WIDTH; }
    public static int getHeight() { return CRAB_HEIGHT; }
}

Autres Entités (Pet, Missile, Spear, Coin)

Les autres obstacles et objets (animal de compagnie, missile, fourche, pièce) suivent une structure similaire à CrabObstacle, avec des images, des tailles et des comportements de déplacement différents. Pour alléger l'article, nous omettons leur code détaillé, mais ils seraient nommés PetCompanion, MissileObstacle, SpearObstacle, et CoinCollectable dans le package fr.game.runner.model.

Par exemple, pour les pièces, le concept est le même, mais elles sont supprimées (et le score mis à jour) lors d'une collision au lieu de bloquer le joueur.

Contrôleur de Jeu (GameController.java)
package fr.game.runner.logic;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

import fr.game.runner.model.PlayerCharacter;
import fr.game.runner.model.CrabObstacle;
import fr.game.runner.model.PetCompanion;
import fr.game.runner.model.MissileObstacle;
import fr.game.runner.model.SpearObstacle;
import fr.game.runner.model.CoinCollectable;
import fr.game.runner.ui.EndGameWindow;
import fr.game.runner.ui.GameScreen;

public class GameController extends JPanel implements KeyListener {
    private Image backgroundA, backgroundB;
    private Image scoreDisplayBg;
    private Image pauseIcon, playIcon;

    private PlayerCharacter player;
    private PetCompanion pet;

    private List<CrabObstacle> crabs = new ArrayList<>();
    private List<MissileObstacle> missiles = new ArrayList<>();
    private List<SpearObstacle> spears = new ArrayList<>();
    private List<CoinCollectable> coins = new ArrayList<>();

    private int backgroundScrollX = 0;
    private int spawnCounter = 0;
    private boolean gameActive = true;
    public static boolean gameOverFlag = false;

    public GameController() {
        player = new PlayerCharacter();
        pet = new PetCompanion();

        try {
            backgroundA = ImageIO.read(new File("images/game_bg_1.jpg"));
            backgroundB = ImageIO.read(new File("images/game_bg_2.jpg")); // Assurez-vous d'avoir une deuxième image de fond ou utilisez la même
            scoreDisplayBg = ImageIO.read(new File("images/score_panel.png"));
            pauseIcon = ImageIO.read(new File("images/pause_icon.png"));
            playIcon = ImageIO.read(new File("images/play_icon.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        // Défilement du fond
        if (gameActive) {
            backgroundScrollX -= 10; // Vitesse de défilement
        }
        g.drawImage(backgroundA, backgroundScrollX, 0, GameScreen.SCREEN_WIDTH, GameScreen.SCREEN_HEIGHT, null);
        g.drawImage(backgroundB, backgroundScrollX + GameScreen.SCREEN_WIDTH, 0, GameScreen.SCREEN_WIDTH, GameScreen.SCREEN_HEIGHT, null);
        if (backgroundScrollX <= -GameScreen.SCREEN_WIDTH) {
            backgroundScrollX = 0;
        }

        player.render(g);
        pet.render(g);

        for (CrabObstacle crab : crabs) crab.render(g);
        for (MissileObstacle missile : missiles) missile.render(g);
        for (SpearObstacle spear : spears) spear.render(g);
        for (CoinCollectable coin : coins) coin.render(g);

        // Affichage du score
        g.drawImage(scoreDisplayBg, 120, 50, null);
        g.setColor(Color.WHITE);
        g.setFont(new Font("Arial", Font.BOLD, 24));
        g.drawString("Score : " + player.getScore(), 133, 95);

        // Affichage icône Pause/Lecture
        if (gameActive) {
            g.drawImage(playIcon, 200, 800, 90, 90, null);
        } else {
            g.drawImage(pauseIcon, 200, 800, 90, 90, null);
        }
    }

    // Gestion de la génération des entités
    private void spawnGameEntities() {
        spawnCounter++;
        Random rand = new Random();

        if (spawnCounter % 120 == 0) { // Générer des obstacles majeurs moins souvent
            int obstacleType = rand.nextInt(3);
            if (obstacleType == 0) crabs.add(new CrabObstacle());
            else if (obstacleType == 1) missiles.add(new MissileObstacle());
            else spears.add(new SpearObstacle());
        }
        if (spawnCounter % 20 == 0) { // Générer des pièces plus fréquemment
            coins.add(new CoinCollectable());
        }
    }

    // Mise à jour de la position et de l'état des entités
    private void updateGameElements() {
        player.animateStep();
        player.applyGravity();
        pet.updatePosition();
        pet.applyGravity();

        Iterator<CrabObstacle> crabIt = crabs.iterator();
        while (crabIt.hasNext()) {
            CrabObstacle crab = crabIt.next();
            crab.updatePosition();
            if (crab.isBeyondScreen()) crabIt.remove();
        }

        Iterator<MissileObstacle> missileIt = missiles.iterator();
        while (missileIt.hasNext()) {
            MissileObstacle missile = missileIt.next();
            missile.updatePosition();
            if (missile.isBeyondScreen()) missileIt.remove();
        }

        Iterator<SpearObstacle> spearIt = spears.iterator();
        while (spearIt.hasNext()) {
            SpearObstacle spear = spearIt.next();
            spear.updatePosition();
            if (spear.isBeyondScreen()) spearIt.remove();
        }

        Iterator<CoinCollectable> coinIt = coins.iterator();
        while (coinIt.hasNext()) {
            CoinCollectable coin = coinIt.next();
            coin.updatePosition();
            if (coin.isBeyondScreen()) coinIt.remove();
        }
    }

    // Détection et résolution des collisions
    private void handleCollisions() {
        // Collisions avec les obstacles bloquants (crabs, missiles, spears)
        for (CrabObstacle crab : crabs) {
            if (player.getPosX() + PlayerCharacter.CHAR_WIDTH >= crab.getPosX() &&
                player.getPosX() <= crab.getPosX() + CrabObstacle.CRAB_WIDTH &&
                player.getPosY() + PlayerCharacter.CHAR_HEIGHT >= crab.getPosY() &&
                player.getPosY() <= crab.getPosY() + CrabObstacle.CRAB_HEIGHT) {
                
                // Exemple de résolution de collision simple: repousser le joueur
                if (player.getPosX() + PlayerCharacter.CHAR_WIDTH <= crab.getPosX() + (CrabObstacle.CRAB_WIDTH / 2)) {
                    player.setPosX(crab.getPosX() - PlayerCharacter.CHAR_WIDTH); // Collision par la gauche
                } else {
                    player.setPosX(crab.getPosX() + CrabObstacle.CRAB_WIDTH); // Collision par la droite
                }
                // Optionnel: gameOverFlag = true; pour une fin de partie immédiate
            }
        }
        // Répéter la logique de collision pour MissileObstacle et SpearObstacle

        // Collision avec les pièces (collectables)
        Iterator<CoinCollectable> coinIt = coins.iterator();
        while (coinIt.hasNext()) {
            CoinCollectable coin = coinIt.next();
            if (player.getPosX() + PlayerCharacter.CHAR_WIDTH >= coin.getPosX() &&
                player.getPosX() <= coin.getPosX() + CoinCollectable.COIN_WIDTH &&
                player.getPosY() + PlayerCharacter.CHAR_HEIGHT >= coin.getPosY() &&
                player.getPosY() <= coin.getPosY() + CoinCollectable.COIN_HEIGHT) {
                
                player.setScore(player.getScore() + 10); // Augmenter le score
                coinIt.remove(); // Supprimer la pièce
            }
        }
    }

    // Vérifie les conditions de fin de partie
    private void checkGameOver() {
        if (player.isOffScreen()) { // Si le joueur sort de l'écran
            gameOverFlag = true;
            new EndGameWindow(player).setVisible(true); // Afficher l'écran de fin
            resetGame(); // Réinitialiser l'état du jeu
        }
    }

    private void resetGame() {
        player = new PlayerCharacter();
        pet = new PetCompanion();
        crabs.clear();
        missiles.clear();
        spears.clear();
        coins.clear();
        backgroundScrollX = 0;
        spawnCounter = 0;
        gameActive = true;
        gameOverFlag = false;
    }

    // Boucle de jeu principale
    public void startGameLoop() {
        new Thread(() -> {
            while (!gameOverFlag) {
                if (gameActive) {
                    spawnGameEntities();
                    updateGameElements();
                    handleCollisions();
                    checkGameOver();
                }
                repaint(); // Redessiner l'écran
                try {
                    Thread.sleep(50); // Contrôle la vitesse du jeu (environ 20 FPS)
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Override
    public void keyTyped(KeyEvent e) {}

    @Override
    public void keyPressed(KeyEvent e) {
        int playerSpeed = 25; // Vitesse de déplacement du joueur
        int petSpeed = 15; // Vitesse de déplacement du compagnon

        if (gameActive) { // Seulement si le jeu n'est pas en pause
            switch (e.getKeyCode()) {
                case KeyEvent.VK_UP:
                    if (player.getPosY() > 10) player.setPosY(player.getPosY() - playerSpeed);
                    if (pet.getPosY() > 10) pet.setPosY(pet.getPosY() - petSpeed);
                    break;
                case KeyEvent.VK_DOWN:
                    if (player.getPosY() <= 560) player.setPosY(player.getPosY() + playerSpeed);
                    if (pet.getPosY() <= 560) pet.setPosY(pet.getPosY() + petSpeed);
                    break;
                case KeyEvent.VK_LEFT:
                    if (player.getPosX() >= 0) player.setPosX(player.getPosX() - playerSpeed);
                    if (pet.getPosX() >= 0) pet.setPosX(pet.getPosX() - petSpeed);
                    break;
                case KeyEvent.VK_RIGHT:
                    if (player.getPosX() < GameScreen.SCREEN_WIDTH - PlayerCharacter.CHAR_WIDTH) player.setPosX(player.getPosX() + playerSpeed);
                    if (pet.getPosX() < GameScreen.SCREEN_WIDTH - PetCompanion.PET_WIDTH) pet.setPosX(pet.getPosX() + petSpeed);
                    break;
                case KeyEvent.VK_SPACE:
                    gameActive = !gameActive; // Basculer l'état de pause
                    break;
            }
        } else if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            gameActive = !gameActive; // Permettre la reprise même en pause
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {}
}

6. Interface de Fin de Partie

Lorsque la partie se termine, l'écran de fin affiche les performances du joueur et propose des options pour continuer l'aventure.

Fonctionnalités :

  • Affichage clair du score des pièces collectées et de la distance parcourue.
  • Calcul et affichage d'un "Score Total" (pièces + bonus de distance).
  • Boutons pour "Rejouer", "Retour au Menu Principal" ou "Quitter".

Le bouton "Rejouer" lance un nouvel écran de chargement qui mène ensuite à une nouvelle partie. "Retour au Menu Principal" ouvre la fenêtre du menu, et "Quitter" ferme l'application.

Implémentation de la fenêtre de fin de partie :

package fr.game.runner.ui;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import fr.game.runner.logic.GameController;
import fr.game.runner.logic.LoadingController;
import fr.game.runner.model.PlayerCharacter;

public class EndGameWindow extends JFrame implements MouseListener {
    private JLabel replayButton;
    private JLabel menuButton;
    private JLabel quitButton;

    public EndGameWindow(PlayerCharacter finalPlayerState) {
        // Réinitialiser le flag de fin de partie
        GameController.gameOverFlag = false; 
        
        // Initialisation des boutons
        replayButton = new JLabel(new ImageIcon("images/replay_button.png"));
        replayButton.setBounds(520, 622, 60, 25);
        replayButton.addMouseListener(this);
        add(replayButton);

        menuButton = new JLabel(new ImageIcon("images/main_menu_button.png"));
        menuButton.setBounds(520, 722, 60, 25);
        menuButton.addMouseListener(this);
        add(menuButton);

        quitButton = new JLabel(new ImageIcon("images/exit_button_small.png"));
        quitButton.setBounds(520, 822, 60, 25);
        quitButton.addMouseListener(this);
        add(quitButton);

        // Panneau de fond affichant les scores
        EndGamePanel endPanel = new EndGamePanel(finalPlayerState);
        add(endPanel);

        // Configuration de la fenêtre
        setTitle("Partie Terminée");
        setSize(1500, 900);
        setLocationRelativeTo(null);
        setUndecorated(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new ImageIcon("images/app_icon.png").getImage());
        setVisible(true);
    }

    public static void main(String[] args) {
        // Exemple d'utilisation (pour les tests)
        PlayerCharacter dummyPlayer = new PlayerCharacter();
        dummyPlayer.setScore(500);
        dummyPlayer.setDistance(15000);
        new EndGameWindow(dummyPlayer);
    }

    class EndGamePanel extends JPanel {
        private Image background;
        private PlayerCharacter playerInfo;

        public EndGamePanel(PlayerCharacter player) {
            this.playerInfo = player;
            try {
                background = ImageIO.read(new File("images/endgame_bg.jpg"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (background != null) {
                g.drawImage(background, 0, 0, getWidth(), getHeight(), null);
            }

            g.setColor(Color.CYAN);
            g.setFont(new Font("Arial", Font.BOLD, 30));
            // Affichage du score des pièces
            g.drawString(String.valueOf(playerInfo.getScore()), 1110, 705); 
            // Affichage de la distance
            g.drawString(playerInfo.getDistance() + " m", 1110, 622);

            g.setFont(new Font("Arial", Font.BOLD, 50));
            g.setColor(Color.ORANGE);
            // Affichage du score total (incluant un bonus de distance simple)
            g.drawString(String.valueOf(playerInfo.getTotalScore()), 1075, 500);
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getSource() == replayButton) {
            new LoadingController().startLoading(); // Relancer le chargement
            dispose();
        } else if (e.getSource() == menuButton) {
            new MainMenuWindow().setVisible(true); // Retour au menu
            dispose();
        } else if (e.getSource() == quitButton) {
            System.exit(0); // Quitter l'application
        }
    }

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}
}

Étiquettes: Java Swing GameDevelopment 2DGame Animation

Publié le 22 juillet à 19h12