Revenir au sommaire Tutoriaux

Comment déplacer un dessin (Sprite) sur un fond sans effet de clignotement , par :
L.ROUVAREL & J.Y. QUEINEC

 

Dans tous les jeux "Professionnels" qui utilisent des Sprites il existe un moteur (programme spécialement conçu pour réaliser des déplacements d'images en tenant compte de l'image de fond, de la vitesse de déplacement , des collisions, des animations... et de bien d'autres effets). Nous n'allons pas prendre en compte tous ses paramètres et nous concentrer sur le Déplacement...
.

Essai 1: Déplacement vers la droite, géré dans une boucle.

Paintbox1 = contient l'image de fond (Tpaintbox)
Image1 = contient l'image à déplacer (Timage)
Image2 = contient l'image de fond (Timage)
Paintbox1.canvas.draw(0,0,image2.picture.bitmap); // copier l'image de fond dans le paintbox
for i:=1 to 50 do
begin
  Paintbox1.canvas.draw(100+i,200,image1.picture.bitmap); //mon image est un BMP
end;
Notez que si votre image est un Bmp utilisez Image.Picture.Bitmap , si c'est un Jpeg utilisez Image.Picture.Graphic
.C'est simple mais le dessin déplacé efface l'image de fond et laisse une trace derrière lui
.
Question : Pourquoi réaliser le déplacement dans un Paintbox ??
Je déconseille d'utiliser le canvas d'une form pour effectuer des déplacements d'images... pourquoi ?? autant prendre de bonnes habitudes dès le départ, voici quelques arguments (mais rien ne vous y oblige ;):
- Si en cours de développement on préfère présenter l'animation ailleurs, il suffit de déplacer le Paintbox. Si on affiche directement sur le Canvas de la Form il faut modifier toutes les coordonnées...gallère, temps perdu, risque d'erreurs... et j'en passe.
- Dans de nombreux jeu, le plateau de jeu ne couvre pas toute la Form et une partie est réservée à l'affichage d'informations (score, nombre de vies, Boutons de commandes...). Il devient alors indispensable de gérer l'effacement de notre Image lorsqu'elle sort du plateau de jeu, et si possible de façon progressive... hein ??? ben oui plus elle sort et moins on en voit...ha!! En d'autres termes se prendre la tête. L'interêt d'utiliser un Paintbox c'est que ce probléme se résoud de lui même !!! Comment mais tout simplement parce qu'on fait afficher sur le Canvas du Paintbox qui n'est pas le même que celui de la Form... donc si votre Image commence à sortir du Paintbox elle sera automatiquement coupée ... cool !!! j'ai plus à gérer visuellement la sortie du plateau de jeu...
.
Question : Pourquoi ne pas utiliser un Timage au lieu d'un Tpaintbox ??
Personnellement je préfère utiliser un Tpaintbox ce qui me permet de savoir de suite sur quoi je travaille: une image à déplacer (Timage) ou sur le plateau de jeu (TPaintbox)...oui c'est vrais il est très simple de renommer les composants...bref c'est Mon choix à vous de faire le votre.
.

Essai 2: Déplacement vers la droite, géré dans une boucle sans effacer le fond.

Paintbox1.canvas.draw(0,0,image2.picture.bitmap); // copier l'image de fond dans le paintbox
for i:=1 to 50 do
begin
  Paintbox1.canvas.draw(0,0,image2.picture.bitmap); // copier l'image de fond dans le paintbox
  Paintbox1.canvas.draw(100+i,200,image1.picture.bitmap); //mon image est un BMP
end;
Noter que la première opération que je réalise est de placer le fond, puis de placer mon image. Si dans cet exemple ce n'est pas évident il faut avoir cette habitude.(placer le fond puis tous les éléments qui se trouvent dessus)
C'est déjà beaucoup mieux... mais comment dire ? le déplacement se fait bien mais il y a des décalages qui se produisent parfois dans l'image à déplacer ,ce qui donne l'impression que l'affichage n'est pas stable. C'est tout simplement le balayage de l'écran qui n'est pas syncro avec l'affichage de votre image (il faudrait ajouter dans la boucle d'affichage un test pour attendre que le faiseau d'électrons finisse de balayer l'écran pour faire un nouvel affichage).
Enfin utiliser un outil qui vous permet de voir la charge de votre processeur (dans le gestionaire des tâches ou le programme CPU Plus que j'ai réalisé) pour voir ce qui se passe lorsque votre programme tourne... Votre processeur est fortement utilisé (surtout si vous travaillez avec de grosses images). Inutile de dire qu'un jeu posséde des dizaines d'images et qu'il devient alors impossible d'utiliser cette technique sans faire ramer votre jeu voire à le rendre injouable.
..
Question : Est il utile de faire ré-afficher toute l'image de fond alors que l'image qui bouge est plus petite ???
Non, bien sûr, mais comment faire...???.
.

Essai 3 : Enlever les effets d'affichage indésirables.

Tout d'abord la commande COPYRECT(rect1,image2,rect2) qui permet de copier un morceau d'image.
rect1,rect2 :Trect;
il faut renseigner rect1.top, rect1.left, rect1.bottom, rect1.right pour présiser où va s'afficher rect2.top,rect2.left,rect2.bottom, rect2.right de l'image Image2. En fait rect1 = rect2 puisque ces 2 images ont la même taille et ont la même position (comme nous allons le voir plus loin...)
Nous allons transférer le morceau d'image de fond qui correspond à notre dessin pour l'afficher dessus et donc l'effacer (ce qui nous évitera de réafficher toute l'image..gros gain de temps et de puissance machine!!!), il ne restera plus qu'à réafficher notre image....
Oui mais... ce système marche bien avec 2 images (un fond + une image) mais il reste un clignotement parasite s'il y a plusieurs images à déplacer en même temps, et qu'elles se superposent. Cela est dû aux séries d'effacements + affichages qui se superposent.
Plutôt que d'afficher avec la commande Copyrect le fond directement dans notre Paintbox nous allons utiliser un bitmap de manoeuvre appelé Fond_de_Travail pour y effectuer tous nos déplacements de manière cachée. Quand tout sera prêt, nous afficherons en une seule dans la paintbox le contenu du Fond de travail. C'est le principe de DelphiX, DirectX....
var
Fond_de_Travail:Tbitmap;
...
// dans l'initiallisation de la form
Fond_de_travail:=Tbitmap.create;
Fond_de_travail.Assign(image2.Picture.Bitmap); // Graphic si c'est un jpeg 
...
// dans notre boucle d'affichage
For i:=1 to 50 do
Begin
  Fond_de_travail.Assign(image2.Picture.Bitmap); //copie de l'image de fond dans celle de travail
  Fond_de_travail.canvas.draw(0,0,image2.picture.bitmap); // rien ne s'affiche à l'écran 
  Paintbox1.canvas.draw(0,0,Fond_de_travail); // basuler sur le réel
end;
...
// avant de quitter
Fond_de_travail.free // libérer la mémoire
C'est déjà beaucoup mieux, l'image qui se déplace est très stable et glisse sans effets indésirables....mais si on regarde du côté du processeur on s'apperçoit que si nous avons gagné en qualité d'affichage, nous n'avons pas amélioré la charge machine. Certains d'entre vous se dirons "mais il a oublié d'utiliser la commande copyrect !!!" .. et bien, oui ! pour bien comprendre il faut procéder étape par étape. Voici donc le même programme optimisé avec la commande copyrect.
.

Essai 4 : Enfin nous y voilà... le Copyrect

var
x:integer; // coordonnées X de notre image
y:integer; // coordonnées Y de notre image        
Fond_de_Travail:Tbitmap;        
image1:TBitmap; // notre image de fond       
image2:TBitmap; // image à déplacer      
...
         
// dans l'initiallisation de la form      
Fond_de_travail:=Tbitmap.create;       
Fond_de_travail.Assign(image2.Picture.Bitmap);  // Graphic si c'est un jpeg        
x:=0; y:=50; // initialisation de la position de l'image       
...
         
// dans notre boucle d'affichage        
For i:=1 to 50 do        
Begin       
  rect1.left:=x+i; // décalement de l'image de 1 pixel vers la droite        
  rect1.top:=y;        
  rect1.bottom:=rect1.top+image1.height; // ajouter la hauteur de l'image        
  rect1.right:=rect1.left+image1.width; // ajouter la hauteur de notre image         
  Fond_de_travail.canvas.copyrect(rect1,image1.canvas,rect1);   // copie du morceau du fond qui nous interesse        
  // c'est ici qu'il faudra ajouter les copyrect des autres images à déplacer dans fond_de_travail         
  // avant de basuler dans le réel     
  Fond_de_travail.canvas.draw(x,y,image2.picture.bitmap);  //mettre notre image        
  Paintbox1.canvas.draw(0,0,Fond_de_travail); // basculer sur le réel dans le paintbox        
end;        
...
         
// avant de quitter        
Fond_de_travail.free // libérer la mémoire        
.
Alors, verdict ??? et bien notre image bouge bien sans clignotement et la charge du processeur à considerablement baissée, ce qui va nous permettre de faire d'autres choses (gestion des collisions,...) .
.

Améliorations possibles :

Elles sont très nombreuses. Le but de ce tutoriel étant de mettre en évidence le mécanisme de recouvrement mis en place dans un déplacement...
Dans un jeu les déplacements sont gérés dans un Timer pour permettre à d'autres tâches de s'exécuter en même temps. Il peut aussi être judicieux de créer une Classe dérivée de notre Tbitmap et à laquelle on ajouter les paramétres de coordonnées, de vitesse... et tout ce qui peut être utile à notre Sprite...

En résumé:
1) Préparation: Créez un bitmap de fond plus un bitmap de travail à la taille de la paintbox d'affichage.
   Pour chaque image à déplacer (sprite) gérez deux rectangles: rectangle ancienne position et rectangle nouvelle position
2) Dans la boucle d'affichage

  • Effacez tous les sprites à leur ancienne position en copiant les rectangles ancienne position du bitmap de fond sur le bitmap de manoeuvre (copyrect)
  • Calculez les nouvelles positions pour tous les sprites et mettez à jour leurs rectangles nouvelle position
  • Affichez les sprites dans leurs rectangles nouvelle position. Vous pouvez gérer une priorité pour savoir quel sprite se retrouvera au dessus et lequel sera au dessous. Commencez par ceux au dessous.
  • Copiez les zones modifiées par les déplacements du bitmap de manoeuvre vers le bitmap de fond. Quelles sont donc ces zones à afficher impactées par les déplacements? Il s'agit pour chaque sprite du rectangle UNION du rectangle ancienne position et du rectangle nouvelle position. Delphi ne nous donne pas de fonction pour calculer le rectangle union, mais l'API windows est très complet quand il s'agit de rectangles:   UnionRect(rectangle_union, rectangle1, rectangle2);
    Vous faites donc un copyrect du canvas du bitmap de manoeuvre vers la paintbox du rectangle_union, en respectant la priorité d'affichage (dessus dessous) des sprites. Les copyrect peuvent se chevaucher sans inconvénient parce qu'ils copient des zones  identiques, donc rien ne clignotera. 

3) En fin de travail (formdestroy) n'oubliez pas de désallouer les 2 grands bitmaps par Fond_de_travail .free, image1.free et toute autre bitmap alloué dynamiquement par Tbitmap.create.

.

La super astuce

Votre jeu utilise cette technique mais les déplacements sont lent...essayez d'accélérer le Timer en indiquant la valeur la plus petite possible. La valeur implicite est de 1000 milisecondes = 1 seconde. Si votre Timer est positionné à 1 miliseconde (le minimum) et que c'est toujours lent , c'est qu'en fait le timer ne descend pas en dessous de 50 ms. C'est le plus souvent insuffisant pour une animation fluide.

Les solutions:
- Augmenter le pas de déplacement de vos images (vitesse des sprites), mais le déplacement va devenir saccadé.
- Utiliser le tickcount de l'API windows, mais c'est à vous de tout gérer et de prévoir que d'autres applications ont le droit de travailler, par exemple en utilisant application.processmesssages. Laissons celà aux pros.
- L'astuce indispensable : il suffit, lorque le timer a le contrôle dans la procédure Ontimer de répéter entre 4 et 8 fois la séquence de déplacement.  

procedure TForm1.Timer1Timer(Sender: TObject);
var
n : integer;
i : integer;
begin
  for n := 1 to 5 do

   begin
    ... vos animations ici
   end;
end;

Cette méthode à ses limites... elle est légèrement moins performante que DelphiX ou DirectX qui utilisent des fonctions optimisées et spécialisées, mais elle fonctionne dans une fenêtre windows parfaitement standard.il faut savoir choisir ses outils en fonction de ses besoins....Bon jeu et sutout n'oubliez pas de m'envoyer vos oeuvres ;-)
.
Voici 2 exemples de jeux que j'ai réalisé avec cette technique. Ils donnent une bonne idées de ce qu'on peu faire...
- Ballon (752 Ko)
- Pongo (1.7 Mo)

Revenir au sommaire Tutoriaux 

Hit-Parade