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.
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.
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
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 RedMaskAvant de pouvoir réduire ses valeurs, il faut les mettre tous au même ordre de grandeur, en shiftant les bits vers la droite :
GreenComposant = Pixel AND GreenMask
BlueComposant = Pixel AND BlueMask
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 + MapXA partir de la, pour avoir le tile auquel correspond ces points, on fait :
Y = MouseY + MapY
TileUnderMouseX = X div 32A 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.
TileUnderMouseY = Y div 32
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) * 32Pour 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.
MapY = YonMiniMap – (TilesPerScreenHeight div 2) * 32
L’Engin : The Next Generation
Introduction
A chaque itération de la boucle principale, le programme fait plusieurs choses :
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é ) :
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 32A 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 :
TilesPerHeight = ScreenHeight div 32
TileX2 = TileX + TilesPerWidth
TileY2 = TileY + TilesPerHeight
OffsetX = -(MapX mod 32)
StartX = OffsetX
OffsetY = -(MapY mod 32)
StartY = OffsetY
Pour j = TileY jusqu'à TileY2 faire
Pour i = TileX jusqu'à TileX2 faire
:
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.
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.
(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.