Dessiner

Ja_graphPanelutiliser paintComponent

Pour les dessiner des formes (cercle, rectangle ..), il faut absolument dessiner sur un composant JPanel, JButton, JApplet, … JLabel (sachant que le fond d’un JLabel est transparent par défaut) et la seule solution est d’écrire une classe qui hérite de ce composant (voir le tuto Classe) et ré-écrire (on dit « surcharger ») la méthode paintComponent().

sur un JPanel

public class Fenetre extends JFrame {
    private Panel panel;
     
    ///// ----- classe interne : un JPanel avec un ovale 
    class Panel extends JPanel { //classe interne
        int x,y;
         
        Panel() {
            x = y = 20;
        }
         
        public void paintComponent(Graphics g)  {
            super.paintComponent(g); // peint le Background
            setBackground(Color.YELLOW); // fond en jaune
            g.setColor(Color.BLACK); // trait en noir
            g.drawOval (x, y, getWidth()-2*x, getHeight()/2 );
        }
 
    } // fin de la classe Panel
       
 
    ///////////////////////////////////////////////////////////////////
    public Fenetre() { /// constructeur de la classe principale
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(230, 180);
        setLocationRelativeTo(null);
        getContentPane().setLayout(null);
 
        panel = new Panel();
        panel.setBounds(20, 10, 160, 100);
        getContentPane().add(panel);
         
        setVisible (true);
    } /////////////////////////////////////////////////////////////////
 
    public static void main(String[] args) {
        new Fenetre();
    }
}

Ja_graphLabelSur un JLabel

Pour le fond d’un JLabel : Il faut savoir que par défaut le JLabel a un fond transparent donc le dessin précédent donne un ovale mais pas de fond jaune. Pour voir le fond jaune, il faut penser à rajouter l’instruction setOpaque(true);

class Label extends JLabel { //classe interne
    int x,y;
     
    Label() {
        x = y = 20;
        setOpaque(true); //dessine le fond du Label (donc jaune)
    }
     
    public void paintComponent(Graphics g)  {
        super.paintComponent(g);
        setBackground(Color.YELLOW);
        g.setColor(Color.BLACK);
        g.drawOval (x, y, getWidth()-2*x, getHeight()/2 );
    }
} // fin de class Label

ja_boutonDegradeSur un JButton

Idem, on surcharge la méthode paintComponent().

public class Bouton extends JButton {
	private String name; 
	public Bouton(String str) { // constructeur
		super(str); //construit d'abord un JButton contenant un texte
		this.name = str;
		this.setContentAreaFilled(false);
//mettre un fond transparent pour pouvoir redessiner le fond
	}
	public void paintComponent(Graphics g) {
		Graphics2D g2d = (Graphics2D) g; // transtypage de Graphics -> Graphics2D
		GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
		g2d.setPaint(gp);
		g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
		super.paintComponent(g2d); // dessine l'étiquette encadrée par dessus le fond
	}
}

 

Outils pour dessiner

  • setStroke(new BasicStroke(5))    met l’épaisseur de trait à 5 px
  • drawLine (int x1, int y1, int x2, int y2)
  • drawRect (int x, int y, int width, int height)
  • drawRoundRect (int x, int y, int width, int height, int arcWidth, int arcHeight)
  • drawOval (int x, int y, int width, int height)  // (x,y) = coordonnées du coin supérieur gauche
  • drawArc (int x, int y, int width, int height, int startAngle, int arcAngle)
  • drawPolyline (int[] xPoints, int[] yPoints, int nPoints)
  • drawPolygon (int[] xPoints, int[] yPoints, int nPoints)
  • fillRect (int x, int y, int width, int height)
  • fillRoundRect (int x, int y, int width, int height)
  • fillOval (int x, int y, int width, int height)
  • fillArc (int x, int y, int width, int height, int startAngle, int arcAngle)
  • fillPolygon (int[] xPoints, int[] yPoints, int nPoints)
  • drawString (String chaine, int x, int y)
  • drawImage (Image im, int x, int y, ImageOberver observateur)
  • drawImage (Image im, int x, int y, int width, int height, ImageOberver observateur)  en déformant l’image pour que ses dimensions soient ‘width’ et ‘height’.
  • drawImage(Image im, int x, int y, Color bgcolor, int width, int height, ImageOberver observateur)  En remplaçant la couleur transparente par la couleur bgcolor.

Effacer une partie de l’image :  clearRect(int x, int y, int width, int height) remet la couleur du fond (le background) sur ce rectangle.

Distance entre 2 points :  Point2D.distance(double x1, double y1, double x2, double y2)

double distance = Point2D.distance(3.0, 4.0, 5.0, 6.0);
System.out.println("Distance entre les 2 points : " + distance);

 

Mettre une image

Ja_GraphBtnImageImgimage sur JButton

Avec package : Cet exemple ayant pour package fr.isn  :

JButton btnTelecharger = new JButton("Télécharger");
btnTelecharger.setIcon(new ImageIcon("bin/fr/isn/imgDownload.png"));

Ja_GraphBtnImageEnsuite il faut placer l’image à 2 endroits :

  1.  dans le répertoire  /src  >  package > à côté des fichiers .java
  2.  dans le répertoire  /bin  >  package > à côté des fichiers .class

Mais cette solution est temporaire … par la suite il faudra créer une classe ResourceLoader pour importer correctement les ressources.

Sans package : L’exemple précédent, sans package, devient :

JButton btnTelecharger = new JButton("Télécharger");
btnTelecharger.setIcon(new ImageIcon("bin/imgDownload.png"));

Et placer l’image dans les 2 répertoires /src et /bin (donc à côté des fichiers .java et .class).

Idem, c’est une solution temporaire … car une classe ResourceLoader est conseillée, voire nécessaire pour faire fonctionner correctement un exécutable JAR.

Ja_GraphLabelImgimage sur JLabel

C’est la même méthode qu’au dessus (c’est exactement comme une image sur un JButton).

Avec un package : Comme mon package est fr.isn alors je place l’image dans les 2 répertoires  /bin/fr/isn  et  /src/fr/isn .

JLabel maLabel = new JLabel("Les couleurs RGB");
ImageIcon img = new ImageIcon("bin/fr/isn/imgRGBcube.png");
maLabel.setIcon(img);

Sans package : Sans package, les 2 répertoires pour placer l’image deviennent évidemment  /bin et /src.
Et dans le code : ImageIcon img = new ImageIcon("bin/imgRGBcube.png");

Mais cette solution est temporaire … par la suite il faudra créer une classe ResourceLoader pour importer correctement les ressources.

Ja_GraphPanelImageimage sur JPanel

1ère solution : Ajouter un JLabel dans le JPanel et mettre l’image sur le JLabel car ça se fait très facilement (voir ci-dessus)  OU opter directement pour un JLabel au lieu du JPanel.

2ème solution : Pour vraiment mettre une image dans un JPanel, il faut surcharger/ré-écrire paintComponent() et donc il faut créer une classe (dérivant de JPanel) !!!

public class MyPanelImage extends JPanel {
	Image image = null;

	public MyPanelImage() {
		try {  // mon package est fr.isn (à adapter selon votre cas)
			image = ImageIO.read(new File("bin/fr/isn/imgRGBcube.png"));
		} catch (IOException e) { }	
	}

	protected void paintComponent(Graphics g) {
		super.paintComponent(g); //repeint le background
		if (image != null) { //s'il y a une image, on la dessine
			g.drawImage(image, 0, 0, null);
		}
	}	
}

NB : super.paintComponent(g); est très important pour une animation car il permet d’effacer le fond du JPanel (opaque par défaut) avant de mettre l’image suivante. Si le JPanel n’est pas opaque, on peut repeindre manuellement le fond en dessinant un rectangle avec fillRect().

Et la classe principale ci-dessous :

public class Fenetre extends JFrame {   
    public Fenetre() { /// constructeur de la classe principale
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(210, 200);
        setLocationRelativeTo(null);
        getContentPane().setLayout(null);
 
        MyPanelImage panel = new MyPanelImage();
        panel.setBounds(0, 0, 168, 140);
        getContentPane().add(panel);
         
        setVisible (true);
    } /////////////////////////////////////////////////////////////////
 
    public static void main(String[] args) {
        new Fenetre();
    }
}

Ja_GraphPanelImage2======= VARIANTE ====================================

Voici une VARIANTE où l’image est chargée dans la classe principale Fenetre.java puis transmise en paramètre à la classe secondaire qui la dessine et de plus, elle la déforme aux dimensions du JPanel (et pourquoi pas !)

public class MyPanelImage extends JPanel implements Serializable {
   BufferedImage image = null;

   public MyPanelImage(BufferedImage image) {
      this.image = image;
      setBackground(Color.GRAY);
      setBounds(0, 0, 208, 120);
   }

   protected void paintComponent(Graphics g) {
      super.paintComponent(g); //peint le background
      if (image != null) { //s'il y a une image, on la dessine
         g.drawImage(image, 0, 0, getWidth(), getHeight(),this);
      }
   }  
}
public class Fenetre extends JFrame {  
   BufferedImage image = null;
  
    public Fenetre() { /// constructeur de la classe principale
      try { // mon package est fr.isn (à adapter selon votre cas)
         image = ImageIO.read(new File("bin/fr/isn/imgRGBcube.png"));
      } catch (IOException e) { }   
      
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(250, 180);
        setLocationRelativeTo(null);
        getContentPane().setLayout(null);
 
        MyPanelImage panel = new MyPanelImage(image);
        getContentPane().add(panel);
         
        setVisible (true);
    } /////////////////////////////////////////////////////////////////
 
    public static void main(String[] args) {
        new Fenetre();
    }
}

Couleur de pixel

sur une image

Dans l’exemple ci-dessous, il faut attraper une exception dû à l’ouverture du fichier (car un échec est possible : fichier absent, pas de droit d’accès …) :

  • soit en ajoutant un bloc try/catch : try {img=ImageIO.read(imageFile);} catch(IOException e){}
  • soit rajoutant une exception à la méthode :   throws IOException
String url = "C:/Users/.../essai.jpg"; //attention à l'extension : jpg, JPG, jpeg ...
//--- Obtention de l'image
File imageFile= new File(url);
BufferedImage img=null;
img = ImageIO.read(imageFile);
//--- Obtenir la couleur du pixel de coordonnées x=50 , y=40  
int rgb = img.getRGB(50, 40);	//x=50, y=40
//--- Séparer les doses de Red, Green, Blue et la transparence Alpha
int r = ((rgb >> 16 ) & 0xFF);
int g = ((rgb >>  8 ) & 0xFF);
int b = (rgb & 0x0000FF);
int alpha = (rgb >> 24) & 0xFF;

OU variante :

Color c = new Color(img.getRGB(50,40)); //x=50, y=40
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();

sur un composant (un Graphics)

Pour récupérer la couleur d’un pixel d’un dessin sur un composant (JPanel, …) avec paintComponent, la solution consiste à utiliser plutôt une BufferedImage : créer une BufferedImage vide, dessiner sur cette BufferedImage (dans son Graphics) puis insérer cette image dans le composant.

class MyPanelImage extends JPanel{ 
    BufferedImage image;
    
    MyPanelImage() { //constructeur
        //--- Créer une BufferedImage vide (noire par défaut) 150x150
        image = new BufferedImage(150, 150, BufferedImage.TYPE_INT_RGB); 

        //--- Dessiner dans l'image (modifications automatiqmt enregistrées)
        Graphics2D g2d = image.createGraphics(); //le Graphics de l'image
        g2d.setColor(Color.RED);
        g2d.drawLine(0, 0, 100, 100); //dessin sur le Graphics de l'image
        g2d.dispose(); // libérer la mémoire du Graphics créé  
    }
       
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        if(image != null){
            g.drawImage(image, 0, 0, this);
        }
    }
}

 

Composant de GUI

getter et setter

  • Couleur du fond : setBackground(Color c)    et    Color getBackground()
  • Couleur du texte : setForeground(Color c)   et    Color getForeground()
  • Police du texte :   setFont(Font f)    et     Font getFont()
  • Dimensions :  setBounds(x,y,largeur, hauteur)   et   Rectangle  getBounds()
    et aussi :  int getWidth(), int getHeight(), int getX(), int getY()Dimension getSize()Point getLocation()

changer / rafraîchir l’affichage

Méthodes d’affichage : Les 3 méthodes que l’on peut ré-écrire (on dit « surcharger ») sont : paintComponent(Graphics2D g)paintBorder(Graphics2D g) et paintChildren(Graphics2D g). C’est surtout paintComponent() qu’on utilise.

Rafraîchir le visuel d’un composant : On appelle la méthode repaint() car elle force l’utilisation de la méthode paint(), qui appelle ensuite les 3 méthodes qu’on a pu surcharger :   paintComponent(), paintBorder() et paintChildren(). Mais on n’appelle jamais paint() ou paintComponent() directement.

Rafraîchir le nombre de composants : On appelle successivement les méthodes revalidate();  repaint();  car elle force le calcul des composants ajoutés et/ou enlevés puis redessine le résultat. C’est le développeur qui décide du moment opportun pour faire ces calculs.

ajout / actif / visible

On peut ajouter ou enlever un composant … mais il faut penser aussi aux autres alternatives : le rendre visible/invisible ; le rendre actif/inactif.

ajouter / supprimer un composant

Penser à faire revalidate(); repaint();  pour rafraîchir l’affichage après un ajout et/ou suppression de composants.

  • monPanel.add(maLabel);    pour ajouter un composant à container :  container.add(child);
  • monPanel.remove(maLabel);  pour retirer un composant au container :  container.remove(child);
  • monPanel.removeAll();  pour retirer tous les composants : container.removeAll();
  • Component[] tab = getContentPane().getComponents();  pour obtenir tous les composants ?ls du container dans un tableau :  Component[] container.getComponents();

rendre visible / invisible

  • monPanel.setVisible(true);    pour rendre visible monPanel.
  • monPanel.setVisible(false);  pour le rendre invisible.

activer / désactiver un bouton

  • monBouton.setEnabled(true);     pour rendre actif monBouton.
  • monBouton.setEnabled(false);   pour le rendre inactif (aspect grisé).

ajout / supprimer un Listener au composant

Pour le rendre interactif un composant, on ajoute un  Listener : un écouteur de clics, de déplacement souris, de bouton …

Buffered Image

  • url :
    String url = "https://www.site.fr/images/im.jpg";
    url = "im/moi.gif";  //chemin relatif
    url = "C:/Users/moi.gif"; //chemin absolu
  • lire un fichier image : Il faudra attraper une exception dû à l’ouverture du fichier :
    • soit en ajoutant un bloc try/catch : try {img=ImageIO.read(imageFile);} catch(IOException e){}
    • soit rajoutant une exception à la méthode :   throws IOException
    BufferedImage b = ImageIO.read (new File("/images/im.jpg"));
    // en 1 ligne (ci-dessus) -- OU --  en 2 lignes (ci-dessous)
    File imageFile = new File(url);    
    BufferedImage b = ImageIO.read(imageFile);
  • largeur / hauteur :  int hauteur = b.getHeight();   int largeur = b.getWidth();
  • couleur d’un pixel :
    int rgb = b.getRGB(x,y);
    int alpha = (rgb >> 24) & 0xff;	  
    int r = ((rgb >>16 ) & 0xFF);	
    int g = ((rgb >>8 ) & 0xFF); 	
    int b = (rgb & 0x0000FF);
    // -- OU avec COLOR (sans Alpha) --
    Color c = new Color( b.getRGB(x,y) );
    r = c.getRed();
    g = c.getGreen();
    b = c.getBlue();
    // -- OU avec COLOR (et Alpha=transparence)
    Color c = new Color( b.getRGB(x,y), true );
    alpha = c.getAlpha();  r = c.getRed();
    g = c.getGreen();      b = c.getBlue();
  • créer une BufferedImage vide : BufferedImage b = new BufferedImage(150, 150, BufferedImage.TYPE_INT_RGB);
  • dessiner dans une BufferedImage :
    Graphics2D g2d = b.createGraphics();//graphics de la BufferedImage
    g2d.setColor(new Color(60,30,50));
    // On dessine sur le g2d, donc directmt dans la BufferedImage.  
    g2d.drawLine(0, 0, 100, 100); 
    g2d.dispose(); // toujours libérer la mémoire du g2d créé
  • déformer une BufferedImage (zoom) : 
    BufferedImage src = ImageIO.read (new File("im.jpg"));
    BufferedImage mini = new BufferedImage(50, 70 
            ,BufferedImage.TYPE_INT_RGB);//noir par défaut
    mini.getGraphics().drawImage(src, 0, 0, 50, 70, null);
  • couper une BufferedImage (crop) : 
    BufferedImage src = ImageIO.read (new File("/ima/im.jpg"));
    BufferedImage mini = new BufferedImage(50, 70, 
                 BufferedImage.TYPE_INT_RGB);//noir par défaut
    mini = src.getSubimage(0, 0, 50, 70);
  • insérer une imagette dans une image : 
    BufferedImage bigImg = new BufferedImage(90, 70, 
                   BufferedImage.TYPE_INT_RGB); //noire par défaut
    BufferedImage miniImg = ImageIO.read (new File("/image/imagette.jpg"));
    Graphics2D g2d = bigImg.createGraphics(); //Graphics de la gde image
    g2d.drawImage(miniImg, 20, 0, null); //insère l’imagette en (20,0)
    g2d.dispose();
  • Copier dans b2 redimensionné à (L2,H2) un rectangle de l’image b1 de dimension (L1,H1) : agrandissement ou réduction :  b2.getGraphics().drawImage(b1, x2,y2,L2,H2, x1,y1,L1,H1, null);  
    b2.getGraphics().drawImage(b1,0,0,b2.getWidth(),b2.getHeight(),
          0,0,b1.getWidth()/2,b1.getHeight()/2,null);
  • Enregistrer la BufferedImage dans un fichier : ImageIO.write (b2, "jpg", new File("/im.jpg"));

Déplacement de sprite

déplacer un personnage au clavier

Ja_PacManAnim

Pour télécharger le projet version 1 (juste le déplacement) : PacMan_v1.zip

Le but est de déplacer un personnage sur l’écran avec les touches du clavier (appelé sprite). Pour animer le sprite, on affiche successivement plusieurs images du mouvement (ces images ont un fond transparent) sur un décor.

Une méthode simple consiste à utiliser 3 classes (ainsi les tâches sont bien séparées) :

  • Fenetre : le programme principal qui ouvre la fenêtre du jeu.
  • GameLoop : le JPanel avec un écouteur de clavier et le graphisme du jeu (les images du décor et du sprite) et un thread qui anime le personnage [contrôle + vue].
  • Player : cette classe contient les caractéristiques du personnage (coordonnées, texture) et la gestion de ses déplacements [mouvement].

Et prévoir une classe ResourceLoader pour charger correctement les 2 images : decor.png (340 x 365 px) et spriteSheet.png (200 x 220 px).

decor                spriteSheet

Fenetre

Le programme principal (la fenêtre du jeu) :

import java.awt.Dimension;
import javax.swing.JFrame;

public class Fenetre extends JFrame {
   private static final long serialVersionUID = 1L;
   GameLoop panel;

   public Fenetre() {
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setLocationRelativeTo(null);
      setSize(new Dimension(500, 500));
      getContentPane().setLayout(null);

      panel = new GameLoop();
      panel.setBounds(0, 0, 450, 450);
      getContentPane().add(panel);

      setVisible(true);
   }

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

GameLoop (JPanel, Runnable, KeyListener)

On commence par créer une classe GameLoop :

  • Cette classe est un JPanel (on dit qu’elle hérite de la classe JPanel).
  • On lui ajoute un KeyListener (un écouteur de touches clavier) qui fait bouger le personnage. Cette classe implémente l’interface KeyListener.
  • De plus le JPanel a besoin d’un thread pour animer le personnage, donc la classe implémente aussi l’interface Runnable.

Ja_Graph_GameLoop_implEn implémentant Runnable et KeyListener :  public class GameLoop extends JPanel implements Runnable, KeyListener  vous devriez avoir une erreur ja_ampoulError . Pour la corriger, il suffit de demander à Eclipse d’ajouter les méthodes à implémenter. Effectivement :

  • Lorsqu’on implémente Runnable, on doit absolument ré-écrire la méthode : run().
  • Lorsqu’on implémente KeyListener, on doit ré-écrire les méthodes : keyPressed(),  keyReleased() et keyTyped()  … quitte à les laisser vide.
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JPanel;

import main.ResourceLoader;  //le Resource Loader pour les images

public class GameLoop extends JPanel implements Runnable, KeyListener {

   private static final long serialVersionUID = 1L;
   private volatile Thread thread;
   BufferedImage offscreen;
   Player player;
   int nbFrame;

   // VARIABLES 'STATIC' => ainsi toutes les classes ont accès aux images
   static BufferedImage decor, spriteSheet, personnage;
   static int spriteWidth = 20;

   public GameLoop() { // constructeur du JPanel du jeu
      this.setBackground(Color.BLACK);
      
      setFocusable(true);  // donne le focus au JPanel
      addKeyListener(this);// ajoute l'écouteur de clavier

      try { // -- on charge les images (variables statiques)
         decor = ResourceLoader.loadImage("images/decor.png");
         spriteSheet = ResourceLoader.loadImage("images/spriteSheet.png");
      } catch (IOException e1) {e1.printStackTrace();}

     
      // on crée le personnage
      player = new Player(); 
      
      demarrer(); // on démarre le thread (l'animation du jeu)    
   }

   public void paintComponent(Graphics g) { // le graphisme du JPanel
      super.paintComponent(g); // dessine le background

      // on dessine le visuel hors écran (dans une BufferedImage 'offscreen')
      offscreen = new BufferedImage(this.getWidth(),
                               this.getHeight(),BufferedImage.TYPE_INT_ARGB);
      Graphics2D gOff = offscreen.createGraphics();// on récupère son Graphics

      // on dessine dans le Graphics d'offscreen : le décor & le personnage
      gOff.drawImage(decor, 0, 0, this);
      gOff.drawImage(player.getSprite(), player.x, player.y, this);
      
      //facultatif : des screenShot : enregistreImage(nbFrame, offscreen);
      
      // puis on affiche cette image dans le graphics 'g' du JPanel
      g.drawImage(offscreen, 0, 0, this);
      gOff.dispose();// on libère la mémoire du Graphics de 'offscreen'
   }

   public void demarrer() {
      thread = new Thread(this); // crée un thread
      thread.start(); // et lance run() [dans une 2e file d'exécution]
   }

   public void arreter() {
      thread = null; // supprime le thread.
   }

   public void run() {
      // INITIALISATION DE L'ANIMATION
      Thread threadEnCours = Thread.currentThread();
      nbFrame = 0;

      // BOUCLE 'SANS FIN' QUI ANIME LE PERSONNAGE (le coeur du programme)
      while (thread == threadEnCours) {
         try {Thread.sleep(10);} catch(InterruptedException e){}//pause de 10ms
         
         // (facultatif) compteur de frames (dans la limite d'un nombre entier)
         nbFrame ++ ;
         if ( nbFrame == Integer.MAX_VALUE ) nbFrame = 0;
                  
          // On bouge les coordonnées : player
         player.commandMoveIt();        

         repaint(); // On redessine le Panel (rafraichissement du visuel)
      }
   }

   public void keyPressed(KeyEvent e) {
      if (e.getKeyCode()== KeyEvent.VK_LEFT ) { //VK_LEFT = flèche gauche
         player.left = true ;
         player.up = player.right = player.down = false ;
      }

      if (e.getKeyCode()== KeyEvent.VK_UP   ) {
         player.up = true;
         player.right = player.down = player.left = false ;
      }

      if (e.getKeyCode()== KeyEvent.VK_RIGHT) {
         player.right = true;
         player.down = player.up = player.left = false ;
      }

      if (e.getKeyCode()== KeyEvent.VK_DOWN ) {
         player.down = true ;
         player.left = player.up = player.right = false;
      }
   }

   public void keyReleased(KeyEvent e) {//Relâcher la touche => arrête le personnage
      if (e.getKeyCode()== KeyEvent.VK_LEFT ) { player.left = false; }
      if (e.getKeyCode()== KeyEvent.VK_UP   ) { player.up   = false; }
      if (e.getKeyCode()== KeyEvent.VK_RIGHT) { player.right= false; }
      if (e.getKeyCode()== KeyEvent.VK_DOWN ) { player.down = false; }
   }

   public void keyTyped(KeyEvent e) { }

   
   //--facultatif : méthode pour enregistrer des images (screenShot)
   public void enregistreImage(int numero, BufferedImage bImage) {
      if (numero %25 == 0)
         try {
            ImageIO.write(bImage, "gif", new File("screenShot"+numero+".gif"));
         } catch (IOException e) {e.printStackTrace();}
   }
}

Player

La classe Player regroupe toutes les caractéristiques du personnage et contrôle ses déplacements.

import java.awt.image.BufferedImage;

public class Player {   
    /** up, up, down, left, right : sont donnés par le clavier (ds GameLoop)
    *   et la méthode commandMoveIt() permet de calculer x, y 
    *   (les coordonnées du coin supérieur gauche du sprite).
    *
    * * Pour déterminer la texture du personnage :
    *     indexMvtSheet = n° de la ligne du mouvement sur la spriteSheet
    *     nbDePas = compteur de pas, qui permet de calculer un n° de mouvement
    */
   boolean up, down, left, right;
   int spriteWidth,indexMvtSheet, x, y, nbDePas;

   public Player() {
      spriteWidth = GameLoop.spriteWidth; //on récupère cette valeur STATIC.
      start();
   }
   
   void start() {    
      x = 160; //position initiale du personnage (à l'arrêt)
      y = 210;     
      nbDePas = indexMvtSheet = 0;
      up = down = left = right = false;
   }     
  
   void commandMoveIt() {  
      /** =============  ON VERIFIE LE MOUVEMENT ===========================================
          D'abord on vérifie que le mouvement demandé ne va pas dans un mur (en bleu).
          Pour cela, on parcourt les 4 segments autour du personnage et s'il y a un pixel bleu
          alors cela signifie qu'il touche un mur (donc on annule ce mouvement).
      */

      //on parcourt le segment haut du sprite ( donc sur y-1 ),
      //si on trouve un pixel bleu, on désactive le mouvement up (et stoppe cette recherche).
      if (up) {
        for (int i=0 ; i < spriteWidth ; i++)  //'UP' (x,y) => (x, y-1)
          if ((GameLoop.decor.getRGB(x+i,y-1) & 0x00FFFFFF) == 0x5B5FFF)  {up = false; break;}
      }

      if (left) { //on parcourt le segment gauche avant d'autoriser (x,y) => (x-1, y)
        for (int i=0 ; i < spriteWidth ; i++)
          if ((GameLoop.decor.getRGB(x-1,y+i) & 0x00FFFFFF) == 0x5B5FFF)  {left = false; break;}
      }

      if (down) { //on parcourt le segment en bas avant d'autoriser (x,y) => (x, y+1)
        for (int i = 0 ; i < spriteWidth ; i++)
         if ((GameLoop.decor.getRGB(x+i,y+spriteWidth+1)&0x00FFFFFF)==0x5B5FFF){down=false;break;}
       }

      if (right) { //on parcourt le segment à droite avant d'autoriser (x,y) => (x+1, y)
        for (int i = 0 ; i < spriteWidth ; i++)
         if ((GameLoop.decor.getRGB(x+spriteWidth+1,y+i)&0x00FFFFFF)==0x5B5FFF){right=false;break;}
       }

      /** =============  ON AVANCE LES COORDONNEES DU PERSONNAGE  =========================
         indexMvtSheet : donne la ligne de la spriteSheet qui illustre cette direction
      */
      indexMvtSheet = 0; // le cas où PacMan est à l'arrêt
      if (up)   { y-- ; indexMvtSheet = 3;}     //on avance de 1 pixel
      if (down) { y++ ; indexMvtSheet = 4;}
      if (right){ x++ ; indexMvtSheet = 1;}
      if (left) { x-- ; indexMvtSheet = 2;}

      //Si pacman passe dans un des 2 tuyaux gris alors il réapparaît de l'autre côté.
      if (x <= 25 && y >= 183 && y <= 208)       { x = 314; }
      else if (x >= 315 && y >= 183 && y <= 208) { x = 26;  }

      //On actualise le nombre de pas
      nbDePas++;
      if (nbDePas >= 16){ nbDePas = 0; }   // nbDePas = 0, 1, .. , 15
   }

   BufferedImage getSprite() {
      /** =============  ON MET UNE TEXTURE AU PERSONNAGE  ==================================
       * Pour 'animer' le sprite, on affiche successivmt les 4 mouvements de la spriteSheet.
       * Donc on compte le nombre de pas que fait le personnage (de 0 à 15, çà suffit)
       * car on n'a que 4 mouvements : n°0, n°1, n°2 et n°3 (pour chaque direction).
       *
       * Mais si on change d'image à chaque pas alors c'est trop rapide pour l'oeil, donc on
       * ne change de texture que tous les 4 pas. Donc on compte les pas de 0 à 15 et on ajoute
       * une variable qui calcule le n° de mvt :  mvt = nbDePas / 4.
       */      
      int mvt = nbDePas / 4;   //mvt = 0, 1, 2, 3 (car  nbDePas = 0, 1, .. , 15)
      return GameLoop.spriteSheet.getSubimage(mvt*spriteWidth,
                                    indexMvtSheet*spriteWidth, spriteWidth, spriteWidth);
   }
}

Ja_GraphSpriteMurPour la détection de mur, on parcourt tous les pixels représentés en rouge (ils sont situés à 1 px du bord du sprite) et on regarde si un de ces pixels a une couleur bleue sur le décor. Si tel est le cas, alors on annule le mouvement demandé au clavier.

collision avec Enemy

Code source complet de cette version 2 : PacMan_v2.zip

  • Il s’agit de créer des ennemis qui se déplacent tout seuls dans le décor ou qui attendent de pouvoir sortir dans une cage au centre. Pour cela, on crée une classe Enemy qui regroupe toutes ces possibilités. Puis dans GameLoop, on crée une liste d’ennemis pour chaque catégorie (une liste de fantômes qui se déplace dans le décor, une liste de fantômes qui sont enfermés dans la cage …). Ensuite lorsqu’un ennemi change d’activité, il passe d’une liste à l’autre. Cependant, on ne peut pas modifier une liste lorsqu’on boucle sur elle … donc la solution est de stocker ceux-ci dans une liste temporaire puis de transvaser une fois le boucle terminée.
  • Il faut aussi déterminer les collisions entre le joueur et les ennemis. C’est très simple, car on utilise Rectangle() qui permet de transmettre les positions et dimensions de n’importe quelle entité (Player ou Enemy) ; puis il existe la méthode intersects() qui renvoie true si les 2 entités ont des Rectangle qui se touchent.
  • Le plus délicat est de gérer les conséquences d’une collision : le joueur se meurt (façon explosion) en une dizaine d’images … et idem les ennemis disparaissent (plus ou moins soudainement selon qu’il est en collision ou pas).

Fenetre

Cette classe n’a pas été modifiée. Reprendre le code précédent (version 1).

Player (déplacement ou collision)

Cette classe s’est enrichie, car le personnage peut être soit en mouvement (commandés par le clavier) comme précédemment OU soit en train de mourir suite à une collision (donc en train de se désintégrer selon un film de 10 sprites sur la ligne 10 de la spriteSheet). Le personnage possède 3 vies et un score (qui n’augmente pas encore, ce sera l’étape suivante). A chaque fois qu’il heurte un ennemi, il meurt, perd une vie, puis recommence une partie.

public class Player {
   /** Le player a 2 états : 
    *   * enCollision=true alors il se désintègre (film de 10 sprites)
    *   * enCollision=false alors il est commandé par le clavier
    * 
    * * up, up, down, left, right : sont donnés par le clavier et la méthode
    * commandMoveIt() permet de calculer x, y (coin supérieur gauche du sprite).
    *
    * * Pour déterminer la texture du personnage : indexMvtSheet = n° de la
    * ligne du mouvement sur la spriteSheet nbDePas = compteur de pas, qui
    * permet de calculer un n° de mouvement
    * 
    * * Lorsque le Player entre en collision, alors 'enCollision=true'
    * et le compteur de pas joue le film de la désintégration (de 0 à 10) dans
    * collisionActing().
    * 
    * * moveIt() : aiguille le mouvement vers collisionActing() ou
    * commandMoveIt() selon que le personnage est en collision ou pas.
    */
   boolean up, down, left, right, enCollision;
   int spriteWidth, indexMvtSheet, x, y, nbDePas, nbDeVie, score, nbSpriteDesintegration;

   public Player() {
      spriteWidth = GameLoop.spriteWidth; // on récupère cette valeur STATIC.
      nbSpriteDesintegration = GameLoop.nbSpriteDesintegration;
      nbDeVie = 3;
      score = 0;
   }

   void start() {
      // position initiale du personnage (à l'arrêt)
      x = 160;
      y = 210;

      nbDePas = indexMvtSheet = 0;
      up = down = left = right = enCollision = false;
   }

   void moveIt(GameLoop gameLoop) {
      if (!enCollision) commandMoveIt();
      else collisionActing(gameLoop);
   }

   private void collisionActing(GameLoop gameLoop) {
      // on avance le film de la désintégration
      nbDePas++;
      indexMvtSheet = 10;

      // à la fin de la désintégration : on arrête la collision et enlève 1 vie
      if (nbDePas >= 4 * nbSpriteDesintegration - 1) {
         nbDeVie--;

         // S'IL reste des vies, on recommence SINON 'GameOver'
         if (nbDeVie > 0) { // NOUVELLE VIE
            gameLoop.addKeyListener(gameLoop);//remet les commandes clavier
            gameLoop.initNewPartie();
         } else {
            gameLoop.lblLblnbdevies.setText("<html>Nombre de VIES : "+nbDeVie+"</html>");
            gameLoop.lblScore.setText("<html>GAME<br :>OVER</html>");
            gameLoop.arreter(); // GAME OVER
         } 
      }
   }

   void commandMoveIt() {
      /**============= ON VERIFIE LE MOUVEMENT ===================================== 
       * D'abord on vérifie que le
       * mouvement demandé ne va pas dans un mur (en bleu). Pour cela, on
       * parcourt les 4 segments autour du personnage et s'il y a un pixel
       * bleu alors cela signifie qu'il touche un mur (donc on annule ce mvt).
       */

      // on parcourt le segment HAUT du sprite ( donc sur y-1 ) :  si on trouve
      // un pixel bleu, on désactive le mouvement up (et stoppe cette recherche).
      if (up) {
         for (int i = 0; i < spriteWidth; i++) // 'UP' (x,y) => (x, y-1)
            if ((GameLoop.decor.getRGB(x+i,y-1) & 0x00FFFFFF) == 0x5B5FFF) {
               up = false;
               break;
            }
      }

      if (left) { //parcours du segment GAUCHE avant d'autoriser (x,y)=>(x-1,y)
         for (int i = 0; i < spriteWidth; i++)
            if ((GameLoop.decor.getRGB(x-1,y+i) & 0x00FFFFFF) == 0x5B5FFF) {
               left = false;
               break;
            }
      }

      if (down) { //parcours du segment BAS avant d'autoriser (x,y) => (x, y+1)
         for (int i = 0; i < spriteWidth; i++)
            if((GameLoop.decor.getRGB(x+i,y+spriteWidth+1)&0x00FFFFFF)==0x5B5FFF){
               down = false;
               break;
            }
      }

      if (right) {//parcours du segment DROIT avant d'autoriser (x,y)=>(x+1,y)
         for (int i = 0; i < spriteWidth; i++)
            if((GameLoop.decor.getRGB(x+spriteWidth+1,y+i)&0x00FFFFFF)==0x5B5FFF){
               right = false;
               break;
            }
      }

      /** ============= ON AVANCE LES COORDONNEES DU PERSONNAGE ============= 
       * indexMvtSheet : donne la ligne de la spriteSheet qui illustre cette direction
       */
      indexMvtSheet = 0; // le cas où PacMan est à l'arrêt
      if (up)   { y--; indexMvtSheet = 3;}      
      if (down) { y++; indexMvtSheet = 4;}
      if (right){ x++; indexMvtSheet = 1;}
      if (left) { x--; indexMvtSheet = 2;}

      // Si pacman passe dans un des 2 tuyaux gris alors il réapparaît de l'autre côté.
      if (x <= 25 && y >= 183 && y <= 208)     { x = 314;   } 
      else if (x >= 315 && y >= 183 && y <= 208) {x = 26;   }

      // On actualise le nombre de pas
      nbDePas++;
      if (nbDePas >= 16) nbDePas = 0; // nbDePas = 0, 1, .. , 15
   }

   BufferedImage getSprite() {
  /** =============  ON MET UNE TEXTURE AU PERSONNAGE  ==================================
   * Pour 'animer' le sprite, on affiche successivmt les 4 mouvements de la spriteSheet.
   * Donc on compte le nombre de pas que fait le personnage (de 0 à 15, çà suffit)
   * car on n'a que 4 mouvements : n°0, n°1, n°2 et n°3 (pour chaque direction).
   *
   * Mais si on change d'image à chaque pas alors c'est trop rapide pour l'oeil, donc on
   * ne change de texture que tous les 4 pas. Donc on compte les pas de 0 à 15 et on ajoute
   * une variable qui calcule le n° de mvt :  mvt = nbDePas / 4.
   */     
      // si pacMan se balade ou à l'arrêt : mvt = 0, 1, 2, 3
      // si pacMan se meurt (ligne 10) : mvt = 0, 1, 2, .. , 9

      int mvt = nbDePas / 4;

      return GameLoop.spriteSheet.getSubimage(mvt*spriteWidth,indexMvtSheet*spriteWidth,
                  spriteWidth, spriteWidth);
   }

   Rectangle getRectangle() {
      return new Rectangle(x, y, spriteWidth, spriteWidth);
   }

   public void doCollision(GameLoop gameLoop) {
      if (!enCollision) { // si PacMan n'est pas déjà en collision
         enCollision = true; // il passe en collision (on change son statut)

         // on se met au début de la ligne 10 de la spriteSheet : désintégration
         indexMvtSheet = 10;
         nbDePas = 0;

         // on immobilise pacman et désactive le clavier le temps de la collision
         gameLoop.removeKeyListener(gameLoop);
         up = down = left = right = false;
      }
   }
}

Enemy (actifs ou en attente)

Voici le code .. le plus délicat est de faire évoluer le fantôme sans qu’il revienne trop souvent sur ces pas (éviter qu’il fasse du ‘sur place’ dans un même couloir) pour cela on évite au maximum qu’il retourne en arrière. C’est pour cette raison que si un fantôme se dirige vers le haut et heurte un mur alors on préfère qu’il aille vers la droite ou la gauche mais on ne garde la direction ‘vers le bas’ qu’en dernier choix. Cette liste de « directions préférées en cas de mur » est stocké dans un tableau tabDirection. Dès que le fantôme change de direction alors ce tableau est recalculé selon le même principe.

public class Enemy { 
   /** Il y a 3 listes d'enemy : 
    *  * ceux qui sont en attente dans la cage centrale => sautille() et enPause = true
    *  * ceux qui se baladent aléatoirmt et changent de direction devant un mur => moveIt()
    *  * ceux qui agonisent (suite à une collision) => agonise() et enCollision = true
    *  
    *    La direction est un entier afin de faciliter le tirage au sort de cette direction.
    *         direction = 1 : vers le haut
    *                   = 2 : vers le bas
    *                   = 3 : vers la gauche
    *                   = 4 : vers la droite
    */
        
   boolean enPause, enCollision;
   int spriteWidth, x, y, nbDePas, direction, index;
   int [] tabDirection;  
   BufferedImage imgFantome;
   Random r;
  
   public Enemy(int index) {
      this.index = index;
      spriteWidth = GameLoop.spriteWidth;//on récupère cette valeur STATIC.
      r = new Random();
      enCollision = enPause = false;
      positionneDansCage(index);
   }
  
   void positionneDansCage(int position) {
      switch (position) {  //positionnemt du trio qui est dans la cage
         case 0 : x = 160; //au centre
               direction = 1;
               break;
                    
         case 1 : x = 160-GameLoop.spriteWidth; //à gauche
               direction = 2;
               break;
                    
         case 2 : x = 160+GameLoop.spriteWidth; //à droite
               direction = 2;
               break;
      }
      y = 160;
      nbDePas = 0;
      enPause = true;
   }  
     
   void start() {
      x=160 ; y=128 ; //position : juste au-dessus de la trappe
      enPause = false;
      direction = 1; // direction vers le haut
  
      // r.nextInt(2) est un entier aléatoire < 2 donc il vaut 0 ou 1 (bref: Pile ou Face)
      //si c'est Pile (si r.nextInt(2)=0) alors tabDirection = {1,2,4} = {droite,gauche,bas}
      //si c'est Face (si r.nextInt(2)=1) alors tabDirection = {2,1,4} = {gauche,droite,bas}
      if (r.nextInt(2) == 0) { tabDirection = new int[] { 4, 3, 2 }; } 
         else { tabDirection = new int[] { 3, 4, 2 }; }
   }
     
   void moveIt() {
      /** Le tableau des directions contient les directions que le fantome testera        
          lorsqu'il rencontrera un mur.  L'ordre est important !
      *Exemple : direction = 1 (le fantome se dirige vers le haut)
         alors au prochain mur, je souhaite qu'il tourne vers la 'droite' ou 'gauche'
         mais pas trop 'vers le bas' pour éviter les va-et-vient dans un même couloir
         ...cependant s'il y a des murs à gauche et droite, alors il ira vers le bas.
        => Lorsque direction = 1 alors tabDirection =  { 4, 3, 2 }     OU    { 3, 4, 2 }
                                                   ={droite,gauche,bas}OU{gauche,droite,bas}
      */  
      int nbDeVirage=0; //on va compter le nombre de changement de direction
        
      while (faceAuMur()) {
         //face à un mur, on change de direction en prenant la valeur suivante du tableau
         direction = tabDirection[nbDeVirage];
         nbDeVirage++;
      }
        
      if (nbDeVirage > 0) // si on a changé de direction, on refait le tableau des directions
         switch (direction) {
         case 1: //si direction=1 ('UP') alors on fait Pile ou Face entre {3,4,2} et {4,3,1}
            if (r.nextInt(2) == 0) {tabDirection = new int[] { 4, 3, 2 };}
            else {tabDirection = new int[] { 3, 4, 2 };}
            break;
         case 2:
            if (r.nextInt(2) == 0)  {tabDirection = new int[] { 3, 4, 1 };}
            else {tabDirection = new int[] { 4, 3, 1 };}
            break;
         case 3:
            if (r.nextInt(2) == 0) {tabDirection = new int[] { 1, 2, 4 };}
            else {tabDirection = new int[] { 2, 1, 4 };}
            break;
         case 4:
            if (r.nextInt(2) == 0) {tabDirection = new int[] { 1, 2, 3 };}
            else {tabDirection = new int[] { 2, 1, 3 };}
            break;
         }
  
      /** =============  ON AVANCE LES COORDONNEES DU FANTOME  =========================*/
      if (direction == 1)  y--; // on avance de 1 pixel
      if (direction == 2)  y++;
      if (direction == 3)  x--;
      if (direction == 4)  x++;
  
      //On actualise le nombre de pas
      nbDePas++;
      if (nbDePas >= 20) nbDePas = 0; // nbDePas = 0, 1, .. , 19
   }
  
   BufferedImage getSprite() {
      /** =============  ON MET UNE TEXTURE AU PERSONNAGE  ================================
       * Pour 'animer' le sprite, on affiche successivmt les 2 mouvements de la spriteSheet.
       * Donc on compte le nombre de pas que fait le personnage, de 0 à 19.
       * qu'on divise par 10, donc le résultat est 0 ou 1 => le n°de mouvement : n°0 ou n°1.
       *
       * On ne change la texture du fantome que tous les 10 pixels !
       */
  
      int mvt = nbDePas / 10;         // mvt = 0, 1
      //index correspond à une couleur de fantome
      imgFantome = GameLoop.spriteSheet.getSubimage((5+direction)*spriteWidth,
               (index+mvt*5)*spriteWidth, spriteWidth, spriteWidth);
   
      return imgFantome;
   }
     
   Rectangle getRectangle() {
        return new Rectangle(x,y,spriteWidth,spriteWidth);
   }
     
   boolean faceAuMur() {
      /** =============  ON VERIFIE LE MOUVEMENT =======================================
      D'abord on vérifie que la position reste au milieu d'un couloir, càd :
      - pas dans un mur (en bleu)
      - pas dans un coin de mur (en gris-noir)
      - pas dans les sous-terrains latéraux (en gris)
      Pour cela, on parcourt les 4 segments autour du fantome et tous les pixels doivent
      être noirs : faceAuMur()= false (on ne touche pas de mur)
      Sinon : faceAuMur()=true (on fonce dans le mur, il faudra changer de direction).
     */
      if (direction == 1) {
         for (int i=0 ; i < spriteWidth ; i++)  //'UP' (x,y) => (x, y-1)
            if ((GameLoop.decor.getRGB(x+i,y-1) & 0x00FFFFFF) != 0) return true;
      }
  
      if (direction == 3) { //parcours du segment GAUCHE avant de faire (x,y)=>(x-1,y)
        for (int i=0 ; i < spriteWidth ; i++)
          if ((GameLoop.decor.getRGB(x-1,y+i) & 0x00FFFFFF) != 0)  return true;
      }
  
      if (direction == 2) { //parcours du segment BAS avant d'autoriser (x,y)=>(x,y+1)
        for (int i = 0 ; i < spriteWidth ; i++)
          if ((GameLoop.decor.getRGB(x+i,y+spriteWidth+1)&0x00FFFFFF)!= 0) return true;
      }
  
      if (direction == 4) {//parcours du segment DROIT avant d'autoriser (x,y)=>(x+1,y)
        for (int i = 0 ; i < spriteWidth ; i++)
          if ((GameLoop.decor.getRGB(x+spriteWidth+1,y+i)&0x00FFFFFF)!= 0) return true;
      }
        
      return false;
   }
  
   public void sautille() {
      // le fantome ne fait que monter & descendre (en se cognant aux murs)
        
      if (direction == 1) { 
         // si le fantome 'monte' et rencontre un mur alors -> vers le BAS 
         for (int i=0 ; i < spriteWidth ; i++)  //'UP' (x,y) => (x, y-1)
            if ((GameLoop.decor.getRGB(x+i,y-1) & 0x00FFFFFF) != 0x00595959) direction = 2; 
      } 
        
      if (direction == 2) {
      // si le fantome 'descend' et rencontre un mur alors -> vers le HAUT 
         for (int i = 0 ; i < spriteWidth ; i++)
          if ((GameLoop.decor.getRGB(x+i,y+spriteWidth+1)&0x00FFFFFF)!= 0x00595959) direction=1;
      }
     
      if (direction == 1) y = y - nbDePas%2; // on avance de 1 pixel qu'une fois sur deux
      if (direction == 2) y = y + nbDePas%2; // selon la parité de nbDePas
        
      //On actualise le nombre de pas
      nbDePas++;
      if (nbDePas >= 20) nbDePas = 0;   // nbDePas = 0, 1, .. , 19
   }
     
   public void doCollision() {
      //si le fantome n'est pas déjà en collision
      if (! enCollision) {
          nbDePas = 0;
          direction = 1; // vers le haut  
          enCollision = true;
      } 
   }
  
   public void agonise(List <Enemy> listTemp, Enemy enemy) {
      // Il fait quelques sautillements ... puis au bout de 20 pas : il meurt !
      // Pour mourir : on l'envoie dans une liste temporaire avant de l'enlever de 
      // listEnemyEnCollision... car meurt() est exécutée dans une boucle de celle-ci 
      if (nbDePas >= 20) listTemp.add(enemy);
      sautille(); 
   }  
}

GameLoop

Dans la boucle sans fin qui caractérise cette classe, il faut rajouter une étape : tester les collisions ‘Player-Enemy’, et les conséquences qui vont avec. Et il faut constituer les listes d’ennemis (bien choisir cet import java.util.List; ).

public class GameLoop extends JPanel implements Runnable, KeyListener {
 
   private static final long serialVersionUID = 1L;
   private volatile Thread thread;
   BufferedImage offscreen;
   JLabel lblScore, lblLblnbdevies;
 
   Player player;
   List <Enemy> listActiveEnemy, listEnemyAuCentre, listEnemyEnCollision, listTemp;
   int nbEnemyMax = 3, nbFrame;
 
   // VARIABLES 'STATIC' => ainsi toutes les classes ont accès aux images
   static BufferedImage decor, spriteSheet, personnage;
   static int spriteWidth = 20, nbSpriteDesintegration = 10;
 
   public GameLoop() { // constructeur du JPanel du jeu
      try { // -- on charge les images (variables statiques)
          decor = ResourceLoader.loadImage("images/decor.png");
          spriteSheet = ResourceLoader.loadImage("images/spriteSheet.png");
       } catch (IOException e1) {e1.printStackTrace();}
             
      setBackground(Color.BLACK);    
      setFocusable(true);  // donne le focus au JPanel
      addKeyListener(this);// ajoute l'écouteur de clavier
      setLayout(null);
       
      lblScore = new JLabel("Score : ");
      lblScore.setForeground(Color.YELLOW);
      lblScore.setFont(new Font("Lucida Console", Font.PLAIN, 16));
      lblScore.setBounds(decor.getWidth()+20, 20, 85, 81);
      add(lblScore);
       
      lblLblnbdevies = new JLabel("Nombre de VIES :");
      lblLblnbdevies.setForeground(Color.YELLOW);
      lblLblnbdevies.setFont(new Font("Lucida Console", Font.PLAIN, 16));
      lblLblnbdevies.setBounds(30, decor.getHeight()+10, 200, 30);
      add(lblLblnbdevies);
      
      // on crée le personnage et les listes d'ennemis (en activité, en cage centrale, en collision)
      player = new Player(); 
      listActiveEnemy = new ArrayList <Enemy> ();
      listEnemyAuCentre = new ArrayList <Enemy> ();
      listEnemyEnCollision= new ArrayList <Enemy> ();
      listTemp = new ArrayList <Enemy> ();
       
      initNewPartie(); // initialisation du personnage et des ennemis
      demarrer();      // on démarre le thread (l'animation du jeu)    
   }
 
   public void demarrer() {
      thread = new Thread(this); // crée un thread
      thread.start(); // et lance run() [dans une 2e file d'exécution]
   }
 
   public void arreter() {
      thread = null; // supprime le thread.
   }
       
   public void initNewPartie() {
      //on actualise les listes d'ennemis : toutes vides sauf 3 fantomes en cage centrale
      listActiveEnemy.clear();
      listEnemyAuCentre.clear();
      listEnemyEnCollision.clear();
      listTemp.clear();    
      for (int i=0 ; i<nbEnemyMax ; i++) listEnemyAuCentre.add(new Enemy(i));
       
      //on actualise l'affichage et initialise le joueur  
      lblScore.setText("<html>SCORE :<br :>"+player.score+"</html>");
      lblLblnbdevies.setText("<html>Nombre de VIES : "+player.nbDeVie+"</html>");    
      player.start(); 
   }
 
   public void paintComponent(Graphics g) { // le graphisme du JPanel
      super.paintComponent(g);
       
      // on dessine le visuel hors écran (dans une BufferedImage 'offscreen')
      offscreen = new BufferedImage(this.getWidth(),
                     this.getHeight(),BufferedImage.TYPE_INT_ARGB);
      Graphics2D gOff = offscreen.createGraphics();// on récupère son Graphics
 
      // on dessine dans offscreen : le décor & le personnage & les ennemis
      gOff.drawImage(decor, 0, 0, this);
      for (Enemy enemy : listEnemyAuCentre) 
         gOff.drawImage(enemy.getSprite(), enemy.x, enemy.y,this);
      
      for (Enemy enemy : listActiveEnemy) 
         gOff.drawImage(enemy.getSprite(), enemy.x, enemy.y,this);
      
      gOff.drawImage(player.getSprite(), player.x, player.y, this);
           
      // facultatif : enregistrer des screenShots du jeu
      // enregistreImage(nbFrame, offscreen);
       
      // puis on affiche cette image dans le graphics 'g' du JPanel
      g.drawImage(offscreen, 0, 0, this);
      gOff.dispose();// on libère la mémoire du Graphics de 'offscreen'
   }
 
   public void run() {
      // INITIALISATION DE L'ANIMATION
      Thread threadEnCours = Thread.currentThread();
      nbFrame = 0;
 
      // BOUCLE 'SANS FIN' QUI ANIME LE PERSONNAGE (le coeur du programme)
      while (thread == threadEnCours) {
         try {Thread.sleep(10);} catch (InterruptedException e){} //pause de 10ms
          
         // (facultatif) compteur de frames (dans la limite d'un nombre entier)
         nbFrame ++ ;
         if ( nbFrame == Integer.MAX_VALUE ) nbFrame = 0;       
          
         //On ajoute un fantome toutes les 400 frames (tant qu'il y en a au centre)
         if( !listEnemyAuCentre.isEmpty() ) {
            if ( nbFrame % 400 == 0 ) {
                Enemy e = listEnemyAuCentre.get(0);
                listEnemyAuCentre.remove( e );
                listActiveEnemy.add(e);
                e.start(); 
                 
                int i=0; //on repositionne les fantomes du centre près de la trappe
                for (Enemy enemy : listEnemyAuCentre) { 
                  enemy.positionneDansCage(i); 
                  i++;
                } 
            }                        
         }
           
         // On bouge les coordonnées : enemy & player
         player.moveIt(this);        
         for (Enemy enemy : listEnemyAuCentre) enemy.sautille();
         for (Enemy enemy : listActiveEnemy)   enemy.moveIt();
         for (Enemy enemy : listEnemyEnCollision)enemy.agonise(listTemp, enemy);
          //listTemp = liste des enemy devenus morts (donc à enlever des listes)
         listEnemyEnCollision.removeAll(listTemp);
         listTemp.clear();
           
         // Gestion des collisions avec Player (si player n'est pas déjà en collision)
         if (! player.enCollision) {
            // On cherche les intersections Player-listActiveEnemy
             for (Enemy enemy : listActiveEnemy) {
                if ((player.getRectangle()).intersects(enemy.getRectangle()))  {             
                   player.doCollision(this);
                   enemy.doCollision();
                   //listTemp = liste des ennemis qui créent une collision
                   listTemp.add(enemy);
                }
             }
              
             // Conséquences : si collision, arrêt de la partie (on supprime les
             //                ennemis sauf celui en collision)
             if (! listTemp.isEmpty() ) {
                listEnemyAuCentre.clear();
                listActiveEnemy.clear();
                listEnemyEnCollision.addAll(listTemp);  
                listTemp.clear();
             }
         }
 
        repaint(); // On redessine le Panel (rafraichissement du visuel)
      }
   }
 
   public void keyPressed(KeyEvent e) {
      if (e.getKeyCode()== KeyEvent.VK_LEFT ) { //VK_LEFT = flèche gauche
         player.left = true ;
         player.up = player.right = player.down = false ;
      }
 
      if (e.getKeyCode()== KeyEvent.VK_UP   ) {
         player.up = true;
         player.right = player.down = player.left = false ;
      }
 
      if (e.getKeyCode()== KeyEvent.VK_RIGHT) {
         player.right = true;
         player.down = player.up = player.left = false ;
      }
 
      if (e.getKeyCode()== KeyEvent.VK_DOWN ) {
         player.down = true ;
         player.left = player.up = player.right = false;
      }
   }
 
   public void keyReleased(KeyEvent e) {// Si on relâche la touche : on arrête le player
      if (e.getKeyCode()== KeyEvent.VK_LEFT ) { player.left = false; }
      if (e.getKeyCode()== KeyEvent.VK_UP   ) { player.up   = false; }
      if (e.getKeyCode()== KeyEvent.VK_RIGHT) { player.right= false; }
      if (e.getKeyCode()== KeyEvent.VK_DOWN ) { player.down = false; }
   }
 
   public void keyTyped(KeyEvent e) { }
    
   //--facultatif : méthode pour enregistrer 1 screenShot toutes les 25 frames
   public void enregistreImage(int numero, BufferedImage bImage) {
      if (numero % 25 == 0)
        try {ImageIO.write(bImage, "gif", new File("screenShot"+numero+".gif"));
        } catch (IOException e) {e.printStackTrace();}
   }
}

_______________________________________________________________