Evolution des Types

Dans le premier rapport de soutenance, je parlais de la structure Tile.
La structure a évidement été élargie avec la progression de l’engin.
Je vais donc expliquer les extensions de cette structure, et vous parler des nouvelles structures importantes, pour les unités et bâtiments.

  1. Le Tile

  2. Tile = record
      PosX,PosY: integer;
      Walkable : boolean;
      Layer1SrcRect : TRect;
      HasLayer2: boolean;
      BatimentPtr: BatimentPointer;
      Layer2SrcRect: TRect;
      xoffset,yoffset: integer;
      UnitList: UnitListPointer;
    end;

    Le paramètre qui a été rajoute depuis la dernière fois est BatimentPtr.
    C’est un pointeur vers un bâtiment. Ce pointeur sera utiliser pour dessiner le bâtiment qu’il contient, et pour savoir s’il y a un bâtiment sous la souris.
     

  3. Les Unités

  4. UnitSt = record
      x,y: Integer; {coords x,y du centre de gravite de l'unit sur la carte}
      x2,y2: Integer; {coords x,y de destination sur la carte}
      XOffset,YOffset: byte; {x-xoffset = DestRect.left ; debut de rectangle de l'image}
      UType: byte; {index dans le tableau des types d'units}
      Selected: Boolean;
      Direction: byte;
      Frame: Byte;
      State: byte;
      ACounter,SCounter,
      HCounter: byte;
      SpeedDelay: byte;
      Delay1,Delay2,Delay3: byte;
      TileList: TileListPointer;
      Sound: MIDASsamplePlayHandle;
      Energy: byte;
      pcoordonnee: pcoordonnees;
    end;

    Je ne vais pas expliquer en détail chaque paramètre car la plupart sont assez explicites et certains ne sont même pas encore utilises, par contre il y en a quelques-uns qui méritent une courte explication.
    TileList est une liste chaînée de tiles sur laquelle l’unité se trouve. Elle est utilisée entre autre pour détermine si l’unité est entrée en collision avec d’autres unités, le tiles ayant à leur tour des UnitList.
    A chaque fois que l’unité se déplace, une procédure est appelée qui va déterminer tous les tiles sur laquelle se trouve l’unité, ces tiles seront places dans son TileList, et on mettra l’unité dans la UnitList de chacun de ces tiles.
    Pcoordonnee sera explique plus en détail par Alexandre, c’est une liste de Coordonnées Monde du chemin a suivre, utilise pour les mouvements des unités.
    Les counters et délais sont pour pouvoir attribuer des vitesses particulières d’animation et de mouvement a chaque unité.
    UType est le type d’unité, cette variable est utilise pour récupérer des infos communes a toutes les unités du même type, par le tableau de structure UnitTypes :

    UnitType = record
      SrcWidth,SrcHeight: byte;
      XOffset,YOffset: byte;
      NameID: string;
      NbOfFrames: byte;
      ADelay, SDelay: byte; {Animation,Speed}
      Flying: boolean;
      Surface: ^IDirectDrawSurface7;
      SelSurface: ^IDirectDrawSurface7;
      GroundRect: TRect;
      WeaponStrength: byte;
      WeaponRange: integer;
      Visibility: integer;
      Energy: integer;
      ShootingSound: string;
      OkSound: array[1..3] of string;
    end;

    UnitTypes : array[1..10] of UnitType;

    Ce tableau est rempli au début du jeu par une procédure MakeUnitTypes
     

  5. Les Bâtiments

  6. Batiment = record
      PosX,PosY: integer; // Sa position sur la carte, Coordonees Monde
      army: byte; // L'armée auquel le bâtiment appartient
      BType: byte; // Type de bâtiment
      Energy: byte;
      Frame: byte;
      selected: boolean;
    end;

    Ici aussi les variables sont assez explicites.
    BType, comme avec les unités, est un index dans un tableau de structure BatimentsTypes qui regroupe toutes les caractéristiques de chaque type de bâtiment :
    BatimentType = record {constantes pour chaque type de bâtiment}
      SrcWidth,SrcHeight: integer;
      StartX,StartY,MidY: integer;
      NameId: string;
      NbOfFrames: byte;
      Energy: integer;
    end;

    BatimentTypes : array[1..10] of BatimentType ;

    Ce tableau est rempli au début du jeu par une procédure MakeBatimentsTypes


La Procédure ClippedBltFast

Quand on veut dessiner une image dans une surface avec la fonction BLT de directdraw, il faut être sur que cette image ne déborde pas de la surface, sinon l’image ne sera pas dessine du tout.

Ceci est très important pour l’affichage des tiles, le scroller ne scroll pas en Coordonnées Tile mais en Coordonnées Monde, c’est à dire que la partie visible de la carte ne commence pas forcement pile au début d’un tile, elle peut commencer à n’importe quelle coordonnée sur ce tile.

Comme on le voit sur cette image, la partie rouge du tile ne devra pas être dessine.
La procédure ClippedBltFast prend en paramètres les coordonnées de destination X,Y sur la surface de destination, un pointeur vers cette surface, un pointeur vers la surface source, et un rectangle TRect, qui est le rectangle source sur la surface source des tiles.
La procédure doit donc regarder si X ou Y est négatif, et si c’est le cas, les mettre à 0 et raccourcir le rectangle source (pour donner le rectangle vert sur l’image).
Le même genre d’opération est fait pour raccourcir le rectangle source si les extrémités sortent de la partie visible.
Une fois les tests accomplis, les coordonnées X,Y, les surfaces Source et Dest et le Rectangle source sont passes à la procédure BltFast de directdraw.
Cette procédure sera utilise presque pour tous les rendus d’images dans le jeu.

La Technique d’Ombrage

Pour pouvoir distinguer les unités volantes des unités terrestres, il etait nécessaire d’implémenter un algorithme qui dessine l’ombre d’une unité. J’ai donc cherché un peu sur le net, et j’ai trouve un site qui expliquait comment il fallait procéder : http://members.tripod.com/~fireshaker/alpha.html.
L’ombre d’une unité est en fait crée en prenant chaque pixel du sprite de l’unité, si la couleur du pixel est différente de la couleur utilisée pour la transparence, on assombri le pixel juste en dessous dans la surface destination. La question est donc, comment assombrir un pixel ?
Nous sommes en 16bit, donc un pixel, c’est en fait deux bytes qui contiennent 5 bits pour le composant rouge de la couleur, 6 bits pour le vert, et 5 bits pour le bleu.

Assombrir un pixel est donc lire la valeur d’un pixel, prendre chaque composant séparément, en soustraire une certaine constante, recompose le pixel et l’écrire dans la surface.
Comment fait on pour avoir chaque composant ?
On procède par extraction, ou Bit Masking :

RedMask = 1111100000000000
GreenMask = 0000011111100000
BlueMask = 0000000000011111
RedComposant = Pixel AND RedMask
GreenComposant = Pixel AND GreenMask
BlueComposant = Pixel AND BlueMask
Avant de pouvoir réduire ses valeurs, il faut les mettre tous au même ordre de grandeur, en shiftant les bits vers la droite :
RedComposant = RedComposant SHR 11
GreenComposant = GreenComposant SHR 5
BlueComposant = BlueComposant


Ensuite on soustrait une même constante a chaque composant :

RedComposant = RedComposant - 10
GreenComposant = GreenComposant - 10
BlueComposant = BlueComposant - 10


On doit aussi vérifier que chaque composant reste supérieur ou égal a 0.

Ensuite on recompose le pixel :

RedComposant = RedComposant SHL 12
GreenComposant = GreenComposant SHL 5
NewPixel = RedComposant OR GreenComposant OR BlueComposant


Cela paraît déjà assez rapide et simple, mais on peut beaucoup optimise cette technique :
Au lieu de faire Composant - 10, faisons carrément une division par 2. Et une division par 2 se résume à un shift des bits vers la droite.
Et au lieu d’extraire chaque composant, le shifter, et de reconstruire le pixel, on peut tout simplement faire un seul shift vers la droite de tout le pixel.
Un petit problème surgit :
A ce moment la, le bit de poids faible du composant rouge déborde dans le composant vert, et le bit de poids faible du composant vert déborde dans le composant bleu, ce qui fait que nous n’avons absolument pas assombri la couleur :

              R       G         B
              01101 010101 00111
SHR 1 = 00110 101010 10011

La solution simple a ce problème est de mettre à zéro le bit de poids fort de chaque composant en utilisant un mask :

         R        G          B
         00110  101010  10011
AND 01111  011111  01111
=       00110  001010  00011

L’assombrissement d’un pixel se résume donc a cette formule :

Mask = 0111101111101111

Pixel = (Pixel SHR 1) AND Mask

Dans DirectDraw, si on veut lire et écrire directement dans la mémoire d’une surface, il faut d’abord la locker. Donc pour faire l’ombre d’une unité je lock déjà la surface contenant l’unité et la surface destination.
Ensuite, pour chaque x,y de la surface unité, je lis la couleur et si elle est différente de la couleur de transparence (nous utilisons le rose), je lis la couleur a x,y dans la surface destination, je lui applique la formule d’assombrissement, et je la réécris au même endroit.
Cet algo ralenti beaucoup le jeu, les écritures et lectures en mémoire vidéo étant assez lent et cassant le rythme de l’accélération matérielle. C’est pour cela que je vais essayer de passer par les capacités matérielles d’Alpha Blending de la carte vidéo. Une fine couche Direct3d va donc devoir être intégrée à l’engin.

La Sélection Des Unités et Des Bâtiments

Pour pouvoir faire des actions avec les unités ou bâtiments, il faut déjà pouvoir les sélectionner avec la souris.
La méthode est la suivante :
Il faut déjà savoir ou se trouve la souris au-dessus du Monde.
MouseX,MouseY sont les Coordonnées Ecran, et MapX,MapY sont les Coordonnées Monde en haut a gauche de l’écran.
Donc pour avoir les Coordonnées Monde de la souris on fait :

X = MouseX + MapX
Y = MouseY + MapY
A partir de la, pour avoir le tile auquel correspond ces points, on fait :
TileUnderMouseX = X div 32
TileUnderMouseY = Y div 32
A partir de la, on peut regarder si le point représenté par la souris est dans le rectangle d’une des unités dans la UnitList du tile.
On parcourt donc la UnitList, et a chaque itération, on construit un rectangle autour de l’unité en prenant sa largeur et hauteur, et on appelle la fonction API32 PtInRect qui renvoie VRAI si le point est à l’intérieur du rectangle. Si c’est le cas, on a un pointeur d’unité UnitUnderMouse auquel on attribut le pointeur de l’unité qu’on a trouve.
Pour le BatimentUnderMouse, il suffit de lui attribue le BatimentPtr du tile.
Ces opérations sont faites à chaque fois que l’utilisateur bouge la souris ou que le jeu scroll.
Les unités sélectionnées sont dans un tableau de 12 UnitPointeurs appelé SelectedUnits.
Il y a aussi une variable NbOfSeldUnits qui définit le nombre d’unités sélectionnées.
Vu qu’il ne peut y avoir qu’un seul bâtiment sélectionné, on utilisera un pointeur appelé SelectedBatiment.
Quand l’utilisateur click sur le bouton gauche de la souris, on regarde d’abord si le pointeur UnitUnderMouse n’est pas nul, si c’est le cas, on vide le tableau SelectedUnits, et on met cette unité toute seule dans le tableau, mettant NbOfSeldUnits a 1.
Si le UnitUnderMouse est nul, on regarde le BatimentUnderMouse, s’il est non-nul on l’injecte dans SelectedBatiment.
Si on a ni l’un ni l’autre, c’est que le joueur a clické sur du terrain vide, et donc on doit dessiner un rectangle jusqu'à ce que le joueur relâche la souris (wm_LbuttonUp).
A la réception de ce message, le programme lance une procédure SelectUnitsFromSelRect qui va sélectionner un maximum de 12 unités qui sont présentes dans le rectangle.
Les unités sélectionnées ont leur variable Selected mises à VRAI.
 

L’Animation Des Unités

Si une unité est en déplacement, on doit changer de frame a intervalle régulier pour donner l’illusion qu’elle marche.
Pour ce, on utilise un système de délais a compteurs. La variable Acounter de l’unité est incrémenté a chaque fois que l’unité est parcourue dans la procédure DoUnitStuff (qui processe toutes les unités l’une après l’autre). Quand ce compteur est égal a la variable Adelay, on le remet à zéro, et on augmente la variable Frame de l’unité, comme ca, quand l’unité sera dessinée, le nouveau frame sera dessiné.
Le même genre de technique est applique pour les mouvements d’unités, on utilise le Scounter et le Sdelay (S pour speed).

La MiniMap

La minimap est un carré de 128x128 pixels en bas a gauche de l’écran sur lequel le joueur peut clicker pour changer d’endroit sur la carte. Pour une carte de 128x128 tiles, chaque pixel correspond à un tile, et quand le joueur click dessus, on change les valeurs de MapX et MapY avec l’équation suivante :

MapX = XonMiniMap – (TilesPerScreenWidth div 2) * 32
MapY = YonMiniMap – (TilesPerScreenHeight div 2) * 32
Pour dessiner la minimap, on parcourt tous les tiles de la map, et on met le pixel correspondant à rouge sur la minimap si la UnitList est non-vide, sinon on le met noir.

L’Engin : The Next Generation

Introduction

A chaque itération de la boucle principale, le programme fait plusieurs choses :

  1. Il prend en compte les entrées de l’utilisateur (clavier, souris) et fais des opérations selon le type d’entrée. Si l’utilisateur a bouge la souris, le code regarde si la souris est sur la bordure de l’écran, a ce moment la, le jeu doit scroller, et donc une variable booléenne précisant qu’il faut scroller est mise à vrai.
  2. Il lance la procédure principale de gestion des unités : tous leurs déplacements, actions, changements d’état…
  3. Il lance la procédure principale de gestion des bâtiments : les incrémentations de compteurs de construction, les créations de nouvelles unités..
  4. Il lance la procédure principale de gestion d’intelligence artificielle.
  5. Il affiche le résultat a l’écran.
Ce qui nous intéresse ici, c’est le 5, le rendu d’un frame.

Le Rendement

Le rendu d’un frame comporte aussi plusieurs parties bien définies et qui sont exécutées dans un ordre bien précis pour qu’on puisse tout voir correctement avec de la perspective ( il est évident qu’il faut d’abord dessiner l’herbe en dessous d’une unité, et ensuite l’unité ) :

(Il y aura bien sur plus tard dans le projet, entre la mini-map et la souris, le rendu du panneau d’informations sur les bâtiments et unités sélectionnes.)

Pour dessiner ce frame, il nous faut donc un point d’origine sur la carte a partir duquel nous allons tirer les informations nécessaires pour dessiner seulement ce qui est visible a l’écran. Il est hors de question de parcourir tout le tableau des unités pour voir si elles sont dans la zone visible pour les dessiner. MapX et MapY sont 2 variables globales qui représentent le point haut gauche en Coordonnées
Monde, du rectangle de visibilité de la carte. Ces variables sont mises à jour quand l’utilisateur scroll.
En divisant ces deux valeurs par 32 ( 32 est la hauteur et la largeur d’un tile), on obtient les premières Coordonnées Tile sur la carte ( le tableau BackTileMap ).

TileX = MapX div 32
TileY = MapY div 32


A partir de ces deux valeurs, on peut définir un rectangle de tiles a dessiner, l'autre point étant trouve comme ceci :

TilesPerWidth = ScreenWidth div 32
TilesPerHeight = ScreenHeight div 32
TileX2 = TileX + TilesPerWidth
TileY2 = TileY + TilesPerHeight
A partir de ca, nous pouvons donc parcourir l'espace visible de la carte en faisant une double boucle simple. Voici l’algo en pseudo code :

OffsetX = -(MapX mod 32)
StartX = OffsetX
OffsetY = -(MapY mod 32)
StartY = OffsetY
Pour j = TileY jusqu'à TileY2 faire
   Pour i = TileX jusqu'à TileX2 faire :

   Fin Pour
OffsetY = OffsetY + 32
OffsetX = StartX
Fin Pour

On a donc maintenant la base de la carte dessinée dans BackSurface, et les unités et objets dans des listes, prêts a être dessinés.
Il faut maintenant les dessiner, et dans le bon ordre.

  1. D’abord on dessine la base des bâtiments : on parcourt la liste chaînée de tiles qu’on a crée au-dessus, et quand on trouve un tile qui a la propriété BatimentPtr non-nulle, on dessine la deuxième partie de ce bâtiment. Les bâtiments ont une propriété MidY qui est le point de séparation des ses deux parties. On utilise deux parties pour le relief ; pour les bâtiments hauts, comme des tours, il peut y avoir une unité, derrière la partie haute, qui est donc partiellement recouverte, et une juste au pied du bâtiment, donc qui la recouvre partiellement le bâtiment, donc il faut d’abord dessiner les parties basses des bâtiments, ensuite les unités, et finalement les hauts des bâtiments.
  2. Ensuite on dessine les unités terrestres en parcourant la liste chaînée d’unités qu’on a crée au-dessus. Pour savoir si une unité est terrestre ou non, on regarde si la propriété Flying est VRAI ou non. La procédure ClippedBltFast est utilisée.
  3. On dessine ensuite les hauts des bâtiments et autres objets de la liste de tiles, comme des panoplies d’arbres ou des rochers.
  4. Ensuite, on dessine les ombres des unités volantes (voir La Technique D’Ombrage plus haut). Les unités terrestres n’ont pas d’ombres pour l’instant car l’ombrage consomme beaucoup de temps. Ceci changera peut être lors de l’incorporation de Direct3d dans le code.
  5. Finalement on dessine les unités volantes, qui sont au-dessus de tout.
On peut ici vider les deux listes chaînées.
Ensuite on rafraîchit la minimap. D’abord on met toute la surface de 128x128 de la minimap en noir, ensuite on parcoure tous les tiles de la carte, on met un point rouge sur la minimap si le tile^.unitList est non-vide.
Une fois ceci fini, on Blt le MiniMapSurface sur le BackSurface.
Finalement, on dessine la souris sur la BackSurface. S’il y a une unité ou un bâtiment sous la souris, le curseur de sélection est dessine, sinon c’est le curseur normal.
Voilà, le frame est fini, on copie maintenant la BackSurface a l’écran en faisant un Blt sur la FrontSurface.
 

L’éditeur de Cartes

Pour faire des jolies cartes comme dans StarCraft, il faut un éditeur de cartes. Il faut que cet éditeur soit suffisamment puissant pour faire des cartes rapidement et simplement, sans avoir à comprendre les mécanismes internes du jeu.

L’interface
Contrairement au jeu, l’éditeur utilise des TForms de Delphi et est donc en mode fenêtres.
Il y a des fenêtres toolwindow pour choisir les unités, les bâtiments, les objets, et les tiles.
Ces fenêtres sont StayOnTop, c’est à dire qu’on les voit tout le temps, même si c’est l’application principale qui a le focus. Elles peuvent cependant être fermées et recouvertes par le menu de la fenêtre principale.
La carte est affichée dans la fenêtre principale, on peut scroller en utilisant les scroll bar.
Contrairement au jeu, il n’y a pas un rendu continuel de frames, le rendu est mis à jour seulement si nécessaire : si l’utilisateur scroll, si la fenêtre bouge, si une fenêtre bouge par-dessus notre fenêtre principale, si la fenêtre reçoit le message WM_PAINT…
En gros, le rendu est Event-Driven.
Le code pour le rendu est exactement le même que dans le jeu.
Un clipper directdraw est utilise pour pas que le rendu ne s’affiche par-dessus des morceaux d’autres fenêtres qui se situerait devant la notre.
Il y a plusieurs modes de sélection, contrôle par des options :

Quand l’utilisateur passe la souris par-dessus la zone ou est dessine la carte, le code regarde laquelle des options est sélectionnée.

Quand l’utilisateur click le bouton gauche de la souris, le code regarde laquelle des options est sélectionnée. Après les Patchs de Nicotine, les Patchs d’Herbe !

(vous ne pensez tout de même pas que je vais écrire 10 pages d’une traite sans perdre mon sérieux )

En bidouillant pendant longtemps avec l’éditeur de StarCraft, j’ai fini par remarque que quand on changeait un morceau d’herbe, il y avait en fait des parties de 4 tiles qui changeaient ensemble, et que pour chaque partie, il y avait toujours que 3 possibilités.

Donc 4 tiles fois 3 possibilités, et ceci en 6 zones distinctes (j’ai nomme A,B,C,D,E,F), soit 72 tiles a extraire des fichiers mpq de starcraft pour les rajouter dans mon jungle.bmp.
Et bien je l’ai fait. Cela ma pris un week-end entier mais je l’ai fait.
J’ai ensuite regroupe dans un fichier tous les index des tiles qui se regroupaient ensemble :
Index Des Tiles Pour Patchs d'herbe pour l'éditeur de cartes
*A
1: 80,81,82,83
2: 0,0,124,125
3: 0,0,126,127
*B
1: 113,114,115,116
2: 100,101,0,0
3: 128,129,0,0
*C
1: 87,84,88,89
2: 118,117,120,119
3: 0,102,104,105
*D
1: 122,121,0,123
2: 110,109,111,112
3: 92,93,96,97
*E
1: 85,86,90,91
2: 103,108,106,107
3: 130,0,131,132
*F
1: 94,95,98,99
2: 133,134,135,136
3: 137,138,139,0

J’ai ensuite mis toutes ces données dans un fichier que j’ai appelé Edit42.dat, et j’ai crée une procédure qui charge ce fichier dans un tableau d’index, pour pouvoir créer des patchs d’herbe facilement. J’appliquerai la même méthode pour rajouter d’autres types de décors.
Pour la création d’un patch, il faut regarder au premier tile de chaque zone pour voir s’il n’y a pas déjà de l’herbe, et a ce moment la, on converti les Index en SrcRect (pour cela j’ai crée une petite procédure Index2SrcRect, et son inverse SrcRect2Index) et on les met dans les Layer1SrcRect des tiles appropries. S’il y a déjà de l’herbe a cet endroit, on ne fait rien.
Avec cette méthode nous pouvons créer des patchs d’herbe de la taille que nous voulons.
Il faut préciser que les 6 zones autour du tile en dessous de la souris ne changent pas directement quand le tile sous la souris change, sinon on pourrait créer des patchs non-uniformes (des trucs très moches). J’ai fait en sorte que 2 de ces losanges ne s’intersectent jamais.
Pour cela il faut changer RegionTileX et RegionTileY seulement si

((mouseTileX mod 4 = 3) and (MouseTileY mod 2 = 1)) or
((mouseTileX mod 4 = 1) and (MouseTileY mod 2 = 0))


Comme je n’ai pas eu le temps de m’amuser à tester la position de la souris par rapport à l’équation paramétrique du losange engendre par les tiles, j’utilise cette méthode, qui n’est pas parfaite (on peut monter tout droit dans les tiles sans que ca change les 6 zones.
Pour créer un patch, il faut donc sélectionner les Tiles dans les options et clicker sur le bouton droit de la souris sur la carte.
A noter que cette méthode n’est pas encore tout à fait au point, je l’améliorerai si j’ai du temps, mais en attendant, elle marche assez bien et m’a permis de faire une carte dont la similitude avec les cartes de StarCraft est assez surprenante.

La Sauvegarde et le Chargement – Le Format E42
La sauvegarde d’une carte est en fait très facile, il suffit d’écrire dans un fichier toutes les variables statiques de la structure TileSt pour chaque tile de la carte, en écrivant au début du fichier le nombre de tiles en horizontal et vertical, pour que le jeu puisse la charger correctement.
Une variable dite statique est une variable qui ne change pas a travers le déroulement du jeu, par exemple UnitList n’est pas statique, par contre, Layer1SrcRect l’est.
Ensuite on écrit le nombre de joueurs et pour chaque joueur, toutes les unités les unes après les autres.
Quand je dis on écrit les unités, je veux dire leurs variables statiques : leur type, et leur position.
On fait ensuite pareil pour les bâtiments.
Le chargement est tout simplement la lecture dans le même ordre avec le remplissage des Tiles, et des tableaux Army[i,j], Buildings[i,j]
Le format du fichier E42 est donc :

E42 (le header)
TilesX
TilesY
Tous les tiles
Toutes les unités
Tous les bâtiments

Ce genre de fichier est un fichier texte faisant à peu près 300k de taille. On utilisera donc sûrement de la compression pour les fichiers E42, sachant qu’un fichier texte faisant 300k se compresse à environ 20k.