1. Introduction
La 3eme soutenance etait seulement quelques semaines avant les partiels, donc la période entre ces deux événements ne fut pas très productive pour le projet, mais je pense avoir plus que rattrape le retard après les partiels, en me consacrant au code jour et nuit, sans sortir, et en me nourrissant par intraveineuse pour gagner du temps.
Donc la première chose que j’ai fait après les partiels etait de repenser ce code entièrement.
Il y avait en fait des problèmes d’interdépendance de liste chaînees, les unités qui se faisaient attaquer contenaient des listes des unités attaquantes, listes qui etaient mal géré quand l’unité mourraient. Les unités attaquantes contenaient ainsi des pointeurs invalides qui faisait tout planter magnifiquement.
La méthode que j’ai employée est donc la suivante :
Chaque unité a un pointeur Utarget qui est utilise si elle attaque une autre unité, quand on prend une unité ennemie en cible, le Utarget de l’unité attaquante reçoit l’adresse de l’unité attaquée.
Chaque unité comporte aussi 2 listes chaînées : Followers et Attackers.
Followers représente les unités qui sont en train de suivre cette unité. Donc quand une unité veut attaquer une autre, elle met son propre pointeur dans la liste des Followers de l’unité qu’elle veut attaquer. Ainsi, si l’unité attaquée meurt avant que l’unité attaquante arrive jusqu'à elle, l’unité attaque pourra dire à toutes les unités dans ses Followers d’arrêter de la suivre, et de mettre leur Utarget a nil, pour pas que les unités attaquantes essayent de suivre ou d’attaquer une unité qui n’existe plus.
Attackers représente les unités qui sont actuellement en train de lui tirer dessus. Cette liste permet à une unité de pouvoir riposter à des attaques qu’elle subit, ou de s’enfuire. Elle permet aussi, au cas ou l’unité mourrait, de dire à ses attaquants d’arrêter de tirer et de repasser à l’état 1.
Donc en résumé, pour qu’une unité attaque une autre, elle charge le pointeur Utarget avec la cible voulue, elle se met dans la liste des Followers de cette cible, elle prend les coordonnées de la cible et les met dans sa destination x2 y2, et elle passe à l’état 2. L’état 2 va calculer le chemin (A*) pour aller vers l’unité cible, et va mettre l’unité a l’état 3. L’état 3 va faire déplacer l’unité jusqu'à ce qu’elle soit à porté de tir, et a ce moment la, elle passera à l’état 6, l’état de tir, ou elle se mettra dans la liste Attackers de sa cible et ouvrira le feu.
A noter que la portée de tir est différente pour chaque unité, c’est un élément de la structure UnitTypes, qui contient la visibilité des unités, ainsi que d’autres propriétés characteristiques comme la puissance de feu, etc...
Dans l’état 6, si l’unité n’est plus a porté de tir, elle suit la cible, donc elle repasse à l’état 2 avec pour destination la position de la cible. La poursuite pourra être active ou non, comme je l’expliquerai tout à l’heure dans la partie Unit Behaviour BitMasking.
Pour ce qui est du rendu du tir, c’est le même système qu’avant, l’unité a un frame de tir pour chaque direction sur lequel est dessine une flamme au bout du canon, pour l’animer, un compteur est augmente modulo une constante de délai, et s’il est égal a zéro, on alterne les frames.
Une fois le système de tir en bon état de marche (plus aucun plantage !), je m’empressai de faire tirer les hélicoptères. Ceux-ci tirent avec la mitrailleuse sur les unités au sol et les bâtiments. Le système de tir est identique pour toutes les unités, sauf que certaines unités ont des capacités de tir spécial, ce que j’expliquerai tout à l’heure dans les parties Missiles, et Les Chars.
Le système fonctionnait bien, mais je trouvais qu’il manquait quelque chose : les éclats des balles quand une unité se faisait attaquée.
J’ai donc extrait l’image des éclats des fichiers de StarCraft, je les ai mis dans une nouvelle surface directdraw.
Les éclats sont dessines dans la boucle de rendu des unités si celles ci ont une liste Attackers qui n’est pas vide. Il y a donc une autre variable dans la structure de l’unité qui retient le frame actuel sur les 8 frames utilisées pour les éclats, et donc a chaque fois qu’un frame est dessine, le compteur augmente modulo un certain délai, et si le compteur est égal a 0, on augmente la variable du frame (modulo 8 car il y a 8 frames).
Le même système de tir est utilise pour des unités qui attaquent un bâtiment, elles ont un Btarget.
Jusqu'à présent les unités tuées disparaissaient
de la carte sans laisser de traces sur la carte, ce qui fait un peu bizarre,
et vu que j’avais envie d’essayer de maintenir l’ambiance trouvée
dans Starcraft, il fallait ajouter des frames d’explosions aux images de
nos unités.
Cédric a donc extrait les images (du fichier stardat.mpq de StarCraft) des frames d’animation d’un soldat qui explose.
Quand une unité meurt, elle passe à l’état 42.
L’état 42 fait cycler les frames d’animation de mort des unités qui en ont, jusqu'à ce que le dernier frame soit atteint, si le dernier frame est atteint, le compteur utilise pour faire cycle est augmenter plus lentement, pour que le dernier frame reste affiche plus longtemps a l’écran. Dans le cas des soldats, le dernier frame est une flaque de sang, qui reste donc un petit moment sur la carte avant de disparaître (comme dans StarCraft).
Les frames de mort sont indépendants des frames de mouvement et de tir pour les unités. C’est à dire qu’ils sont pas dans la même surface. Dans la boucle de rendu, si l’unité est à l’état 42, on dessine le frame de mort définie par une variable dans la structure de l’unité, a partir de la surface des frames de mort, sinon on dessine l’unité normalement avec les variables frame et direction, en prenant la surface normale de l’unité.
J’ai choisi ce système car les frames d’explosions des bâtiments et des véhicules sont aussi a part de leurs images normales, car tous les bâtiments utilisent la même explosion que celle des véhicules au sol. Ce procédé me permet donc de faire un algorithme general qui va traiter toutes les explosions de la même façon, avec seulement des paramètres différents.
Une fois que ce système etait mis en place, je m’aperçu vite fait qu’il y avait un défaut :
Si des unités sélectionnées se faisaient tuer, et que l’on cliquait pour les faire déplacer alors qu’elles réduites à des taches de sang, elles se relevaient et se remettaient à marcher !
Ceci etait du au fait qu’avant le système d’explosions, une unité a l’état 42 etait supprimée de la liste des unités de l’armée directement, ne donnant pas le temps a l’utilisateur d’agir dessus.
J’ai donc cherche tous les endroits dans le code ou une unité passait à l’état 42, et a chaque fois je remplaçai le unitp^.state := 42 par DieU(UnitP), ou DieU est une procédure que j’ai crée qui s’occupe de de-selectionée l’unité si elle l’est, et de dire à toutes les unités attaquantes d’arrêter de l’attaquer, vu que ca ne sert à rien d’attaquer une unité qui est en train de faire cycler ses frames de mort, ou une tache de sang. La procédure regarde aussi si l’unité a un Utarget, et dis à cette cible qu’elle ne l’attaque plus (elle enlève le pointeur de la liste Utarget^.Attackers), pour que les éclats ne soient pas dessines, et surtout pour que la cible ne riposte pas sur une tache de sang.
J’ai du aussi modifier les collisions légèrement pour qu’une tache de sang ne soit pas un obstacle. Quand on fait le test pour voir si l’unité est en collision avec une autre, on rejète cette collision si l’unité gênante est à l’état 42. La même modification a du être faite dans la procédure VerifAttack d’Alex, sans quoi l’unité prenait pour cible une unité a l’état 42.
Je me suis donc mis à coder la possibilité pour les unités de rentrer dedans et ensuite tirer à travers les meurtrières.
Pour les faire rentrer dedans, c’est pas complique, quand l’utilisateur a des unités sélectionnées et qu’il click avec bouton droit sur un bunker de la même armée, ces unités reçoivent le bunker comme Btarget, et elles passent à l’état 2, avec comme destination les coordonnées monde du bunker.
Elles vont donc calculer le chemin, passer à l’état 3, et marcher jusqu'à ce qu’elle soit arrivée au bunker. A ce moment la, a la fin de l’état 3 de l’unité, on regarde si son Btarget est différent de nil, et que c’est un bunker de la même armée. Si c’est le cas, on enlève l’unité des tiles sur lesquels elle se trouve ( je rappelle que chaque tile a une liste d’unités), et la rajoute a la liste chaînée d’unités du bunker (bunker^.UnitList).
Si le bunker se fait détruire, il va prendre toutes les unités dans sa UnitList et les remettre dans des tiles ou se trouvait le bunker, pour qu’elles réapparaissent sur la carte.
Les unités a l’intérieur d’un bunker sont à l’état 0. Dans cet état, elles appellent une procédure qui va rechercher des unités ennemies autour d’elles, a un certain rayon (procédure VerifAttack, code par Alex). Si une unité ennemie est trouve, l’unité dans le bunker va passer à l’état 60. J’ai choisi 60 car l’état 6 normal ne convenait pas tout à fait pour des unités dans un bunker. Le principe est le même, avec les Attackers et Followers, et le Utarget, mais ici, si l’unité attaquée s’éloigne, l’unité attaquante arrête de tirer et doit repasser à l’état 0, sinon elle sortirait du bunker pour poursuivre sa cible.
Du cote de l’unité attaquée, j’ai du modifier légèrement la procédure Riposte (code par Alex) pour que l’unité attaque le bunker et non l’unité a l’intérieur qui l’attaque, sinon des ennemis pourraient tuer les unités dans un bunker sans que le bunker leur serve de protection, ce qui ne servirait à rien.
Faire tirer le bunker (graphiquement) m’a pris un peu plus de temps car j’ai du y réfléchire plus longtemps avant de l’implémenter, pour être sur que ce que je voulais faire allait marcher.
Le bunker a 8 meurtrières, qui pointent vers 8 sens différents. Il etait hors de question de faire des images de bunker qui tire dans toutes les combinaisons de sens possibles.
J’ai donc fait une image qui contenait à chaque frame une flamme de tir de direction différente, donc en tout, 8 frames. Chaque frame devait être blit par-dessus le bunker au bon endroit, pour qu’on est l’impression que le feu sorte des meurtrières, donc avant de faire le code du tir, j’ai trouve tous les points x,y, pour chaque frame, ou allait être blit ce frame par rapport au x,y du haut gauche du bunker.
Pour afficher ces frames, il fallait que je sache quelles meurtrières etaient en train de tirer.
Comme il y a 8 sens possibles pour le bunker et 16 sens pour les unités, j’ai utilise la procédure GetDirection que j’avais fait pour savoir quelle direction devait prendre une unité pour qu’elle soit en train de regarder le point voulu. Le point voulu dans ce cas est le x,y de la cible de l’unité dans le bunker. La procédure GetDirection me renvois donc la direction que je devrais mettre à l’unité, mais au lieu de cela, je la divise par deux et j’obtiens ainsi le numéro de la meurtrière qui est utilisée.
Pour rendre les images de tir par-dessus le bunker, il faut savoir l’état
de chaque fenêtre de tir.
Pour cela j’utilise un octet (BitMask) dans lequel chaque bit représente
une fenêtre. Si le bit est à 1, il faut dessiner une flamme
a cette fenêtre.
Donc pour chaque unités a l’intérieur (4 maximum), on regarde si elle est en train de tirer (et non en train de recharger son arme) trouve la fenêtre de tir correspondante, et on met le bit correspondant à 1 dans le BitMask.
Ceci est fait à l’état 1 du bâtiment.
Donc en résumé :
L’état 60 des unités dans le bunker gèrent l’attaque avec les listes sur la cible.
L’état 1 du bunker met à jour son BitMask en regardant si les unités sont en recharge ou pas
La boucle de rendu pour les bâtiments dessine une flamme pour chaque fenêtre qui a son bit du BitMask a 1.
J’ai utilise le bitmask pour éviter d’utiliser 8 variables booléennes pour chaque bunker, ce qui équivaut à 7 octets gagnes pour chaque bunker. Et le code est beaucoup plus compact et general avec un système de masks. En gros c’est plus beau :)
5. Les Missiles
Plus haut j’avais parle des cas de tirs spéciaux pour certains véhicules. Ici je vais vous parler du système de missiles que j’ai fait pour les hélicoptères.
Si un hélicoptère tir sur un char ou un autre hélicoptère, il va utiliser des missiles.
Un missile est un sprite autonome, comme une unité. Quand l’attente de recharge est écoulée pour un hélicoptère, il créé un nouveau missile. Après juste un appel de la procédure NewMissile, avec comme paramètre l’unité qui tire, l’unité n’a plus a se soucie du missile, il est guide et suivra sa cible jusqu'à ce qu’elle soit touchée.
NewMissile va en fait donner la cible de l’unité a la cible du missile, et va trouver son point de départ. Le point de départ est trouve par rapport à la direction de l’hélicoptère ; comme ce qui a été fait pour les frames de tir pour les bunker, il faut trouver une position du missile par rapport au frame de l’unité, et ceci pour toutes les directions.
Le missile va être ensuite ajoute à une liste chaînée de pointeurs de missiles de l’unité, et le missile va recevoir le pointeur de l’unité qui l’a lance (son Owner)
Le missile va aussi être ajoute à la liste générale de tous les missiles, pour qu’une boucle puisse traiter leurs déplacements et leur explosion.
Si la cible est tuée avant que le missile ne l’atteigne, il explosera au dernier endroit ou se trouvait la cible, sans faire de dégâts, sinon, il explosera en infligeant des dégâts a l’unité cible.
Le missile change de direction comme le ferait une unité poursuivant sa cible, sauf que les missiles ont la plus grande vitesse parmis toutes les vitesses des unités, donc un missile finit toujours par rattraper sa cible (un peu comme la mafia).
Bien sur les missiles ne seraient pas complets s’ils ne laissaient pas une belle traînée de fumée alpha-blendée derrière eux. Donc je l’ai fait.
J’ai trouve les images de fumée utilises dans StarCraft, et je les ai mis dans une nouvelle surface. Cette surface est donc composée de 8 images, commencant par une boule de feu, et finissant par un nuage très sombre.
Pour faire une traînée de fumée, je crée un nouveau nuage de frame 0 a chaque fois que le missile a fait 5 déplacements. Ce nuage sera crée à l’endroit ou se trouve le missile, et il va ensuite évoluer tout seul, comme les missiles. Il ne se déplacera pas, mais changera de frame, pour qu’il se dissipe peu a peu: La boucle qui traite les nuages va augmenter le frame tous les X délais.
Donc on a l’impression que les missiles traînent derrière eux une traînée de feu avec de la fumée.
L’explosion d’un missile se fait exactement de la même façon que pour les unités, un cycle de frames.
Si l’utilisateur a demande l’option Hardware Alpha-Blending, les nuages et les explosions de missiles seront alpha-blendé avec du code direct3d, si l’option est à Software, ca sera mon propre algorithme d’alpha-blending en assembleur qui sera utilise, et si l’utilisateur a demande aucun alpha-blending, il y aura un simple blit, donc pas de transparence. Ceci est aussi le cas pour les ombres et tous les autres objets que je mets en transparence.
Le principe est le même qu’avec les soldats, je fais cycler une série d’image, et quand la dernière est passe, je supprime le bâtiment ou l’unité de la liste.
J ‘ai pris les images d’explosion/eclat de base qu’utilise StarCraft, donc ceux-ci animes comme d’habitude, en alpha-blending, et j’ai rajoute des nuages de fumes, les mêmes que ceux qu’utilisent les missiles, sauf qu’ici le délai de dissipation est plus long, créant ainsi plusieurs nuages statiques qui se dissipes peu a peu.
Pour les hélicoptères j’ai trouve d’autre frames d’explosion qui me semblait bien, et j’ai fait pareil, j’ai aussi mis quelques nuages.
7. Optimisations Direct3d
Avant la 3eme soutenance j’avais code une procédure qui blitait des surfaces en utilisant l’alpha-blending matériel avec Direct3d, mais cette procédure recréait a chaque fois les vertex et autres objets nécessaires pour faire le RenderSurface, ce qui etait inutile. Donc j’ai tout refais proprement, en mettant les objets qui ne changeraient pas dans des procédures d’initialisation au début du jeu. J’ai fait ceci jusqu'à réduire mes procédures direct3d a seulement 5 ou 6 lignes, par rapport aux vingtaines que j’avais avant…
On a quand même gagne plus que 10 FPS dans cette histoire, donc j’etais assez content de moi, même si on ne peut pas vraiment appeler ca de l’optimisation, vu que c’etait du code porc des le début… Mais bon, toute amélioration est un progrès.
8. Le système de Groupes
Pour poursuivre ma StarCraftisation du jeu, j’ai ensuite code le système de groupes.
Celui ci permet d’attribuer un ensemble d’unité sélectionnées a un groupe, qu’on peut ensuite rappeler. Pour attribuer il faut taper sur la touche G en ayant des unités sélectionnées, et ensuite le numéro du groupe que l’on veut créer. Si on sélectionne d’autres unités, et qu’on appuie sur la touche du numéro d’un groupe, les unités de ce groupe seront sélectionnées.
C’est un principe extrêmement simple a coder, les groupes sont en fait un tableau de 10 listes chaînées d’unités. Quand on veut créer un groupe, on copie la liste des unités sélectionnées vers une des listes du tableau, l’indice étant le numéro qu’a demande le joueur. Quand on veut rappeler un groupe, on copie la liste du tableau a l’indice demande vers la liste des unités sélectionnées.
Chaque unité a donc une variable Group, qui contient l’index du group dans lequel il se situe, ou 255 s’il n’est pas dans un groupe. Comme ca, si elle meurt, on peut l’enlever de son groupe pour pas que l’on ne puisse ressusciter de taches de sang en rappelant le groupe des qu’elle meurt.
9. Plantages
A ce stade dans le projet je pensais avoir corrige la plupart des bugs, mais les plantages etaient quand même assez fréquents et je n’osais pas vraiment continuer le code avant d’avoir régler ce problème.
Donc j’ai relu l’intégralité du code.
Cela m’a pris une semaine mais j’ai du corriger une dizaine de choses qui n’allaient pas, plus un énorme bug qui m’etait passe sous le nez en faisant les missiles. Les listes Followers et Attackers n’etaient pas tout le temps bien mises a jour, et on finissait par avoir une erreur de violation, un pointeur qui pointait vers de la mémoire non allouée.
Après cette relecture j’ai fait une séquence importante de tests sans que le projet ne plante.
Je pouvais donc continuer.
10. Unit Behaviour BitMasking
Les procédures VerifAttack et Riposte qu’avait fait Alex me donnèrent une idée géniale qui simplifierait deux aspects difficiles que nous avions mis dans le cahier des charges :
Chaque bit d’un octet spécial dans la structure des unités représenterait un comportement particulier que l’on pourrait mettre en marche ou empêcher.
Voici le système que j’ai tire de cette idée :
Unit Behaviour BitMask
Constantes:
Si une unité se fait tirer dessus pendant qu'elle est a l'état 1, elle ripostera.
bit 0: 00000001
nom: Riposter Toujours
description: l'unité ripostera aux tirs ennemis si elle est en déplacement .
implémentation: unités a l'état 3 regardent si elles ont des attackers et ripostent si oui. (procédure Riposte)
bit a 0: priorité sur le mouvement (commande)
bit a 1: priorité sur l'attaque (initiative)
bit 1: 00000010
nom: Etat d'Alerte
description: l'unité attaquera des unités approchantes.
implémentation: unités a l'état 1 regardent dans un rayon de leur visibilité, et s'il y a des unités ennemis, les attaquent. (procédure VerifAttack)
bit a 0: priorité sur la non-hostilité (commande)
bit a 1: priorité sur l'attaque (initiative)
bit 2: 00000100
nom: Préservation
description: l'unité fuira si elle est en dessous d'un seuil critique d'énergie.
implémentation: unités a tout état iront a l'état 3 avec les coords du main bunker si l'énergie est trop basse. (procédure Fuite)
bit a 0: priorité sur le mouvement/attaque (commande)
bit a 1: priorité sur la préservation (initiative)
bit 3: 00001000
nom: Agressif
description: l'unité cherchera toujours le combat.
implémentation: unités de tout état iront a l'état 6 si elles voient des ennemis a proximité. (procédure VerifAttack)
bit a 0: priorité sur le mouvement (commande)
bit a 1: priorité sur l'attaque (initiative)
bit 4: 00010000
nom: Follow
description: l'unité suivra sa cible si celle si n'est plus a porte de tir.
implémentation: unités a l'état 6 iront a l'état 3 si la cible est trop loin. (procédure Follow)
bit a 0: priorité sur la maintien de position (commande)
bit a 1: priorité sur le mouvement/attaque (initiative)
Alex ayant fait les deux premières procédures importantes pour ce système, j’ai moi même fait Fuite et Follow.
La procédure Fuite fait partir l’unité a l’oppose de l’unité qui l’attaque, et la procédure Follow a déjà été explique plus haut.
Ce système permet de paramétrer des unités de son armée et de les envoyer au combat sans trop s’inquiéter du résultat. Par exemple si vous voulez envoyer un groupe attaquer une base et que vous préfériez quand même qu’il s’arrête pour attaquer des unités ennemies rencontrées en route, vous activer le bit 3 (Agressif).
Si vous voulez que vos unités restent en place en cas d’attaque, vous désactivez le bit 4 (Follow), elles ne poursuivront ainsi pas leurs cibles si celles-ci s’éloignent.
Vous pouvez aussi activer le bit 2 (Préservation) pour que vos unités fuient au cas ou leur énergie serait en dessous du quart.
On peut ainsi combiner les bits pour avoir 32 différents comportements, et élaborer des stratégies avec.
Ceci simplifies aussi grandement la gestion d’une partie de l’IA Générale ; on peut avoir des comportements préalablement établis par rapport a certaines conditions, par exemple si on veut détruire une cible particulière mais sans subir trop de pertes, on peut activer le bit de Préservation et désactiver le bit Agressif sur des unités qu’on envois ensuite au combat. Je parlerai plus amplement de tout ceci dans la dernière partie IA Générale.
Pour pouvoir paramétrer les unités pendant le jeu, j’ai fait un système de menu ou l’on peut activer ou désactiver les différents mode. Le menu est une image que j’ai faite, qui est blit en transparence si le hardware support est active, avec deux boutons OK et CANCEL pour que le joueur puisse confirmer ou annuler son choix. Les paramètres qu’il met seront appliques a toutes les unités sélectionnées.
11. Les Chars
Cédric m’ayant finalement donne le fichier image du char LeClerc, j’ai pu coder le système qui ferai tourner la tourelle pour tirer.
Pour les autres unités, si la cible n’est pas dans la bonne direction par rapport a l’unité qui tire, celle ci doit tourner sur elle même. Pour le char, les mêmes tests sont effectues, mais c’est la direction de la tourelle que l’on change. La tourelle a un délai plus long que le châssis pour qu’elle tourne plus lentement.
Le char a deux modes de tir, le premier est a la mitrailleuse, c’est exactement pareil que le tir des soldats, et le deuxième est le tir du canon. Pour le tir du canon j’ai du trouver tous les offsets x,y par rapport au haut gauche de l’image de la tourelle, ou j’allais mettre 2 nuages de fumée, et ceci pour chaque direction de la tourelle.
Une fois ceci accompli le code etait facile, le char recharge, comme tout le monde, et tir. Quand il tire je met donc 2 nuages qui se dissipent plus lentement que ceux utilisées pour les missiles. Les dommages sont donc ensuite attribues a la cible, et il repasse en mode recharge.
12. Panel Upgrade
Il manquait jusqu'à la beaucoup de jouabilité au jeu car on ne pouvait pas encore utiliser le panel pour voir de l’information sur ses unités et bâtiments, et on ne pouvait pas contrôler la construction des unités, et des upgrades possibles.
J’ai donc rectifier cette situation.
J’ai fait pleins de petite icônes de 32x32 pixels, que j’ai rajoute a mon fichier d’icônes.
Le système d’IconGrid étant pratique, je n’ai rien eu a changer. J’ai tout simplement rajouter les index des icônes et les procédures qu’ils doivent appeler dans ma procédure ProcessIconClick.
Pour la construction des unités, un compteur est utilise pour faire un délai de construction, et une variable qui contient la progression de la construction. Quand cette variable atteint l’énergie de l’unité que l’on est en train de construire, je remet tous les compteurs a 0 pour la prochaine fois, et j’appelle la procédure CreateUnit qu’a code Alex. Cette procédure se charge de trouver un endroit libre autour du bâtiment pour poser l’unité pour pas qu’elle soit en collision avec un objet ou une autre unité.
Le même procédé de compteurs est utilise pour l’ugrade d’OilMax, quand l’upgrade est termine, les extracteurs de pétrole remplissent 10 barils au lieu des 5 au début de la partie.
Pour la gestion du Satellite SOL, il faut d’abord le positionner au dessus de la zone de combat, pareil avec un système de délai a compteurs, et une fois prêt, les soldats ont une icône grise qui devient jaune, l’icône du SOL-Strike, quand cette attaque est appelle, le satellite tir sur le récepteur que porte le soldat et tout dans un rayon de 20 tiles autour de lui est sévèrement endommage, voir même détruit.
Pour les ravitaillements, il suffit de cliquer sur l’icône avec une fourchette et un couteau pour que le camion arrive d’un point défini sur l’éditeur de cartes. L’icône restera grise tant que le camion ne sera pas reparti.
Toutes ces informations (progression, type de construction) sont affichées dans la boucle de rendu après le blit du panel.
Pour les unités, si plusieurs unités sont sélectionnées, on peut voir leurs types ainsi que leur énergie sur le panel, et on peut cliquer sur l’un des noms affiche pour la sélectionner uniquement, ou shift-clicker pour la désélectionner.
Si on sélectionne un bunker, on la liste des unités a l’intérieure est afficher, et on peut cliquer dessus pour les faire sortir, ou alors cliquer sur l’icône avec les flèches pour les faire toutes sortir en même temps.
13. Editeur Upgrade
Vu que beaucoup de choses avaient changées dans le projet sans que l’éditeur n’est été touche, il etait temps de le mettre un peu a jour.
J’ai donc mis des images pour chaque nouvelle unité. J’ai code deux procédures qui permettent d’enlever une unité ou un bâtiment d’une armée, ce n’avait pas été fait auparavant, et je ne pouvais pas reprendre les procédures utilisées dans le jeu car tout dans l’éditeur avait été fait en tableaux statiques, par rapport aux listes chaînées du jeu.
Pour enlever une unité ou un bâtiment maintenant il suffit de cliquer le bouton droit au dessus de l’objet non-desiré.
J’ai aussi rajoute des options pour pouvoir choisir les nationalités des joueurs (Français ou Américains), la météo voulue sur la carte (aucune, neige ou pluie), les points de ravitaillements pour chaque armée (points ou apparaissent les camions de ravitaillement quand on les appelle).
Il y a aussi maintenant possibilité de mettre les ressources de départ pour chaque armée.
J’ai donc évidemment du modifier la structure des fichiers carte e42 pour prendre en compte tous ces nouveaux paramètres, et donc convertir toutes les cartes que j’avais fait avant.
J’en ai aussi profiter pour agrandir la carte que nous utilisons pour tester le jeu.
14. Intelligence Artificielle Générale
Dans cette dernière partie je vais vous parler de ce que j’ai fais pour que le joueur puisse jouer tout seul contre l’ordinateur, et qu’il ne puisse pas gagne tout le temps.
Au bout d’un lapse de temps assez court d’échange d’idées entre Alex et moi, nous avions trouve une méthode algorithmique efficace et propre pour gérer l’IA sans que l’ordinateur ne soit trop difficile a battre. Je ne vas pas expliquer le principe ici car Alex le fait dans sa partie, mais en gros c’est un système ou a chaque itération de la boucle principale, on prépare les données en faisant des tests particuliers, dans la boucle des unités et celle des bâtiments, pour ensuite les traiter dans une procédure a la fin de la boucle principale.
Cette procédure a un compteur qui varie de 0 jusqu'à 200, et a chaque dizaine un certain algo est exécute si les données qui ont été établies avant sont suffisantes.
On a ainsi un système qui ne ralenti pas le jeu pour faire des parcours de listes pour calculer des trucs complexes, et qui accompli progressivement dans le temps les taches qu’il juge nécessaire.
Alex c’est concentre sur la partie construction ; construction d’unités et de la base. Moi je me suis occupe des stratégies d’attaque et de défense.
Extracteurs
La première chose dont je me suis occupe fut la conquête des extracteurs neutres sur la carte.
L’algo est exécute pour une armée si l’extracteur ne possède pas déjà des unités de cette armée dans sa liste Followers, ce qui voudrait dire que des unités ont déjà été envoyés mais qu’elles ne sont pas encore arrivées. La condition pour qu’une unité y aille, si un extracteur de tel type est trouve, est qu’elle soit a l’état 1, pour pas qu’on la dérange dans une action, et qu’elle soit d’un type attaquant, les unités de construction ne pouvant pas hijacker les extracteurs (voir la partie d’Alex). L’unités doit aussi se trouver dans un certain rayon par rapport au bâtiment, pour pas que trop d’unités convergent vers lui des le début.
Si toutes les conditions sont réunies, l’unité va être rajoute dans la liste des Followers du bâtiment, et va être mis a l’état 2 avec pour destination les coordonnées du bâtiment. Elle va donc essayer d’aller conquérir l’extracteur.
Fuite
J’ai ensuite fait un algo qui guide les unités en fuite vers le bunker le plus proche de sa propre base, ou alors le main bunker.
L’état de fuite que j’avais crée avec le Unit Behaviour BitMasking met une variable booléenne fuite a VRAI dans l’unité si celle si fuie.
Donc mon algo d’IA se contente d’abord de trouver une unité qui comporte le bit 3 du mask actif, et qui soit a l’état 3, avec la variable fuite a VRAI.
Si telle unité est trouver, la procédure d’IA générale va lui donner des nouvelles coordonnées de deplacment, et va désactiver tous ses bits de comportements pour pas qu’elle se fasse divertire en route. La destination prise est en fonction des bâtiments dans sa base ; s’il y a des bunkers, elle va aller vers le plus proche, ainsi, si des ennemies la suivent, ils vont aller se faire fusiller. Sinon elle va tout simplement vers le Main Bunker.
Le Style
Pour pouvoir contrôler le niveau d’agressivité d’une armée, j’ai fait un tableau de de 4 octets (4 étant le nombre de joueurs maximum) qui contient donc une valeur pour chaque armée.
Cette valeur est mise a jour pour chaque armée par une procédure d’IA générale, en comptant le nombre d’unités ennemies attaquantes et en regardant le ratio entre ce nombre et le nombre de ses propres unités attaquantes. Si l’armée a plus d’unités, son Style sera élevé, sinon il sera bas.
Cette variable sera utiliser dans d’autres algorithmes d’IA pour que l’armée puisse choisir entre défendre et attaquer.
L’Entre Aide
L’aide sera appelle si une on trouve une unité qui est en train de se faire attaquer, et donc le test est qu’elle ait une liste d’Attackers non-vide.
Dans ce cas, si le style est supérieur a 40, on envois des helicos s’il y en a de disponible, sinon des chars, sinon des soldats pour aider l’unité attaquée a se défendre.
Pour envoyer des unités j’utilise les listes NB qu’Alex crée lors des créations d’unités, je sais donc tout de suite si j’ai tel ou tel type d’unités disponibles a envoyer.
Change Position
Pour ce qui est de l’attaque ou de la défense, j’ai eu une idée assez originale :
Chaque armée a 5 points x,y autour de leur Main Bunker que j’appelle points StratEgos.
Ces points sont calcules au début de la partie de façon aléatoire sur un rayon de 500 pixels a partir du centre de chaque Main Bunker.
Les armées peuvent donc se servir des ces points pour circuler a l’intérieur de sa propre base, ou d’aller dans les bases ennemies.
J’ai crée pour cela une procédure d’IA qui fait un random(100). Si cette valeur est inférieure ou égale au Style de l’armée, l’unité sera envoyée sur un point StratEgos au hasard parmis tous les points des armées adverses, en activant le bit Agressif. Ainsi, le Unit Behaviour BitMasking s’occupera du reste.
Si cette valeur est supérieure, on refait un random (100), et si la valeur est supérieure a 50, on envois l’unité vers un point StratEgos au hasard mais dans sa propre base.
Ce système fonctionne très bien, quand on lance le jeu avec 2 armées contrôlées par l’IA, en leur mettant le Style a 0 et en bloquant la mise a jour du Style, toutes les unités convergent en nuées vers leurs propres bases, c’est assez impressionnant a voir sur la MiniMap, et si l’on met le Style a 100, elles se jettent toutes pratiquement a l’attaque d’une base ennemie.
En testant le jeu avec les algos de constructions d’Alex, je trouvais quand même que les armées passaient trop vite a l’attaque, donc j’ai rajouter un délai avant lequel il n’y a pas de mise a jour de Style, donc au début il reste a 0, le temps que les armées se développent un peu, ensuite c’est la guerre.
Attaque de Batiment
Jusqu'à la tout allait bien, mais les unités n’attaquaient pas les bâtiments, donc j’ai rajoute un algo d’IA qui fait une unité attaquer un bâtiment au hasard si le Style est au dessus de 30 et si elle est proche d’un point StratEgos ennemi.
Les unités allaient ainsi essayer de détruire la base de l’autre, c’etait beau a voir.
Défense de Bâtiment
Le dernier algo que j’ai fait pour le projet fut celui-ci. C’est exactement
la même chose que l’entre aide des unités, sauf qu’ici les
unités vont défendre un bâtiment qui se fait attaquer,
en prenant donc pour cible les unités attaquantes.
E) Conclusion Personnelle
Bien qu’étant un peu decu par le manque de temps nécessaire pour trouver la cause d’un plantage, et de mieux développer l’IA, je suis quand même satisfait de mon travail sur ce projet durant la totalité de son déroulement.
Je me rend compte qu’on avait été un peu ambitieux dans l’écriture de l’échéancier car on n’a pas eu le temps de faire le mode réseau, mais il faut dire que j’avais aussi mal juge la volonté et la capacité de travail de l’un des membres du groupe…
Je n’avais encore jamais réalisé de programme si complexe, et j’avoue que la prochaine fois je serai plus tenté a utiliser les objets, chose que je n’ai pas fait pour 42 Minutes Pour Vivre.
J’ai appris que même étant sur de son code, plus la taille d’un projet est important et qu’il y a plusieurs personnes qui le développent en essayant de battre des records de vitesse, moins il y aura de chances que le programme soit complément bug free.
Ce projet m’a apprit a mieux réfléchir et organiser mes idées avant de faire d’implémentation, et a me forcer a expliquer les choses plus clairement et en détail que je n’avait l’habitude de faire. J’ai aussi du m’organiser par rapport au travail scolaire, dont je regrette fortement ne pas avoir su mieux équilibrer, mais ce sera mieux l’année prochaine.