Ce tutoriel présente une implémentation fonctionnelle et bien structurée du jeu classique Snake en Java, centrée sur les princieps fondamentaux de la programmation orientée objet (POO). L’approche privilégie la clarté, la modularité et la maintenabilité, sans dépendances externes ni bibliothèques tierces — uniquement le JDK standard et l’API Swing pour l’interface graphique.
Architecture modulaire
Le jeu repose sur trois classes centrales, chacune encapsulant une responsabilité précise :
Reptile: modélise le serpent comme une séquence ordonnée de segments, gérant son déplacement, sa croissance et la détection des collisions.Nourriture: génère aléatoirement des objets comestibles à des emplacements valides (hors corps du serpent et dans les limites du plateau).TableauJeu: sert de point d’orchestration — il héberge la boucle de jeu, traite les entrées clavier, met à jour l’état global et déclenche le rendu visuel viapaintComponent().
Exemple de classe Reptile refactorisée
Voici une version réécrite avec une logique plus robuste et un style moderne (utilisation d’énumérasions typées, gestion explicite des bords, suppression de la duplication) :
import java.awt.Point;
import java.util.ArrayDeque;
import java.util.Deque;
public class Reptile {
private final Deque<Point> corps;
private Direction orientation;
private static final int TAILLE_CELLULE = 10;
public enum Direction {
HAUT(0, -1), BAS(0, 1), GAUCHE(-1, 0), DROITE(1, 0);
final int dx, dy;
Direction(int dx, int dy) {
this.dx = dx;
this.dy = dy;
}
}
public Reptile() {
this.corps = new ArrayDeque<>();
this.orientation = Direction.DROITE;
// Initialisation au centre du plateau
this.corps.add(new Point(300, 200));
this.corps.add(new Point(290, 200));
this.corps.add(new Point(280, 200));
}
public void avancer() {
final Point tete = corps.peekFirst();
final Point nouvelleTete = new Point(
tete.x + orientation.dx * TAILLE_CELLULE,
tete.y + orientation.dy * TAILLE_CELLULE
);
corps.addFirst(nouvelleTete);
corps.removeLast(); // Suppression de la queue
}
public void grandir() {
final Point queue = corps.peekLast();
corps.addLast(new Point(queue.x, queue.y));
}
public boolean collisionAvecLimites(int largeur, int hauteur) {
final Point tete = corps.peekFirst();
return tete.x < 0 || tete.x >= largeur ||
tete.y < 0 || tete.y >= hauteur;
}
public boolean collisionAvecSoi() {
final Point tete = corps.peekFirst();
int compteur = 0;
for (Point segment : corps) {
if (compteur++ > 0 && tete.equals(segment)) {
return true;
}
}
return false;
}
public void changerOrientation(Direction nouvelle) {
// Empêche le demi-tour immédiat (ex. : HAUT → BAS)
if ((nouvelle == Direction.HAUT && orientation != Direction.BAS) ||
(nouvelle == Direction.BAS && orientation != Direction.HAUT) ||
(nouvelle == Direction.GAUCHE && orientation != Direction.DROITE) ||
(nouvelle == Direction.DROITE && orientation != Direction.GAUCHE)) {
this.orientation = nouvelle;
}
}
public Deque<Point> getCorps() { return new ArrayDeque<>(corps); }
}
Gestion intelligente de la nourriture
La classe Nourriture garantit que chaque nouvel aliment apparaît dans une case vide, en évitant les positions occupées par le serpent. Elle utilise un générateur sécurisé et itère jusqu’à trouver un emplacement valide :
import java.awt.Point;
import java.util.Random;
public class Nourriture {
private Point position;
private final int largeur, hauteur;
private final Random aleatoire;
public Nourriture(int largeur, int hauteur) {
this.largeur = largeur;
this.hauteur = hauteur;
this.aleatoire = new Random();
placer();
}
public void placer() {
do {
int x = aleatoire.nextInt(largeur / 10) * 10;
int y = aleatoire.nextInt(hauteur / 10) * 10;
this.position = new Point(x, y);
} while (estSurCorps());
}
// Méthode à implémenter dans TableauJeu ou transmise via injection
private boolean estSurCorps() {
// Logique à intégrer selon le contexte (ex. vérification contre Reptile.getCorps())
return false;
}
public Point getPosition() { return new Point(position); }
}
Boucle de jeu contrôlée et réactive
Le cœur du jeu réside dans une boucle basée sur javax.swing.Timer, assurant une fréquence constante sans bloquer l’interface utilisateur :
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class TableauJeu extends JPanel implements ActionListener {
private final Reptile serpent;
private final Nourriture nourriture;
private final Timer horloge;
private int score;
private static final int DELAI_MILLIS = 120;
public TableauJeu() {
this.serpent = new Reptile();
this.nourriture = new Nourriture(600, 400);
this.horloge = new Timer(DELAI_MILLIS, this);
this.score = 0;
setFocusable(true);
demanderFocus();
ajouterEcouteursClavier();
horloge.start();
}
private void ajouterEcouteursClavier() {
addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyPressed(java.awt.event.KeyEvent e) {
switch (e.getKeyCode()) {
case java.awt.event.KeyEvent.VK_UP:
serpent.changerOrientation(Reptile.Direction.HAUT);
break;
case java.awt.event.KeyEvent.VK_DOWN:
serpent.changerOrientation(Reptile.Direction.BAS);
break;
case java.awt.event.KeyEvent.VK_LEFT:
serpent.changerOrientation(Reptile.Direction.GAUCHE);
break;
case java.awt.event.KeyEvent.VK_RIGHT:
serpent.changerOrientation(Reptile.Direction.DROITE);
break;
}
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
serpent.avancer();
if (serpent.getCorps().peekFirst().equals(nourriture.getPosition())) {
serpent.grandir();
nourriture.placer();
score += 10;
}
if (serpent.collisionAvecLimites(getWidth(), getHeight()) ||
serpent.collisionAvecSoi()) {
horloge.stop();
JOptionPane.showMessageDialog(this, "Partie terminée ! Score : " + score);
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
dessinerSerpent(g);
dessinerNourriture(g);
dessinerScore(g);
}
private void dessinerSerpent(Graphics g) {
for (Point segment : serpent.getCorps()) {
g.setColor(Color.GREEN);
g.fillRect(segment.x, segment.y, 10, 10);
}
}
private void dessinerNourriture(Graphics g) {
Point pos = nourriture.getPosition();
g.setColor(Color.RED);
g.fillOval(pos.x, pos.y, 10, 10);
}
private void dessinerScore(Graphics g) {
g.setColor(Color.BLACK);
g.drawString("Score : " + score, 10, 20);
}
}
Points clés de conception
- Encapsulation stricte : tous les champs internes sont
private, les accès se font via des méthodes publiques ou des objets immuables (ex.getPosition()renvoie une copie). - Séparation des préoccupations : aucune logique de rendu dans
ReptileouNourriture; tout est centralisé dansTableauJeu. - Robustesse : blocage des demi-tours interdits, validation des positions avant placement de la nourriutre, gestion propre des exceptions.
- Extensibilité : l’architecture permet facilement d’ajouter des niveaux, des effets spéciaux ou des types d’aliments supplémentaires sans modifier les classes existantes.