Il est venu le temps de faire une nouvelle démo.

Cette semaine, on va aborder 2 points importants. Il existe plusieurs façons de réaliser chacun d'eux, et donc on peut tout à fait contester mon code moche et peu optimisé, mais c'est juste pour montrer une manière de faire.

Les 2 éléments dont on va parler :
- Gérer une liste d'objets en en retirant/rajoutant un peu tout le temps
- Gérer la trajectoire de type 'tête chercheuse' en cherchant à utiliser le moins de temps CPU possible
Voici donc le résultat de la démo :
Le code source fait une centaine de lignes, et utilise des sprites normaux avec des rotsets pour les rotations. Rien de bien exceptionnel, rien qui ne nécessite une explication.

Bon, il faut bien commencer quelque part, on va voir en premier la gestion des objets.

(c):
typedef struct{
s32 fx, fy, x, y, vitesse;
s16 angle;
s8 chgangle;
} tir_type;
tir_type tirs[100]; // 100 max
u8 ntir[100];
u8 nlibre[100];
u8 ntirs;
Voici pour la déclaration. En gros, pour chaque tir, on a une position en fixed point et en normal (pour ceux qui auraient envie de jouer avec des collisions sans se prendre la tête avec les fixed points), la vitesse, l'angle (direction, de 0 à 511 pour des questions de simplicité et de puissance de 2

), et chgangle, qui est de combien l'angle peut changer par frame.
La variable ntirs correspond au nombre de tirs actifs. On a ensuite 3 tableaux :
- tir_type tirs[100]; pour les infos des tirs
- u8 ntir[100]; pour les tirs en cours. De ntir[0] à ntir[ntirs] on trouve les tirs activés avec le numéro correspondant dans le tableau tirs
- u8 nlibre[100]; pour les slots dispos. Même principe que ntir, mais l'inverse, ici on a les slots non utilisés.

Donc, le principe est tout con. Au début on remplit le tableau des tirs dispos avec les nombres 0 à 99 (simple boucle for), puis quand on a besoin on pioche le dernier nombre de la liste, et on le retire de celle-ci.

On l'ajoute alors à la liste des tirs, et on ajoute 1 au nombre de tirs :
(c):
void AjouterTir(void){
if(ntirs == 100) return; // On ne fait rien
u8 n = nlibre[ntirs]; // Récupérer un slot disponible
ntir[ntirs] = n;
// Position aléatoire pour le nouveau tir...
tirs[n].x = 8+(PA_Rand()%240);
tirs[n].y = 8+(PA_Rand()%176);
tirs[n].angle = PA_Rand()&511; // Direction au hasard
tirs[n].vitesse = 2+(PA_Rand()&3); // Vitesse aléatoire de 2 à 5
tirs[n].chgangle = 2+(PA_Rand()&7); // Vitesse de rotation aléatoire de 2 à 9/VBL
tirs[n].fx = tirs[n].x<<8; tirs[n].fy = tirs[n].y<<8; // Fixed point...
ntirs++;
}
Le reste du code est sans importance, c'est juste de la position/vitesse aléatoire.

Reste maintenant à gérer la destruction d'un tir... La boucle qui permet de faire bouger tous les tirs est presque toute simple :
(c):
for(i = 0; i < ntirs; i++) i+= DeplacerTir(i);
Alors, pourquoi ce i += au lieu de juste exécuter la fonction ? En fait, le problème se pose quand on retire un tir : si on a 10 tirs, qu'on en retire un au milieu, on va avoir ntirs = 9, et donc la boucle va s'arrêter avant le dernier ! Pour pallier ce problème, j'ai choisi de faire mettre le dernier tir du tableau à la place du tir que l'on retire, et donc il faut "réexécuter" la fonction DeplacerTir pour le numéro que l'on vient de faire... Ainsi, dans le cas d'un retrait de tir, en fait la fonction renvoie -1. On passe de 5 à 4, par exemple, et comme on augmente de 1 à la boucle suivante, ça repasse à 5 et on refait le tir numéro 5.

Voici le code en question :
(c):
if(PA_Distance(Stylus.X, Stylus.Y+192, tirs[n].x, tirs[n].y) < 8*8){ // Si touche, on le retire !
ntirs--;
nlibre[ntirs] = n; // On remet dans le pool...
ntir[i] = ntir[ntirs]; // On met à la place le dernier...
PA_SetSpriteXY(0, n+1, 256, 192); // Cacher...
PA_SetSpriteXY(1, n+1, 256, 192); // Cacher...
return -1; // Refaire ce numéro
}
Rien de particulier donc, il suffit de déplacer le numéro du tir dans le tableau.

Maintenant qu'on a vu tout ça, il faut voir comment gérer l'aspect tête chercheuse... J'ai déjà montré la fonction qui initialise un tir, en gros tout est mis au pif (la position, la vitesse de déplacement, et la vitesse de rotation)... J'utilise une technique que j'ai incorporée à PAlib pour diriger le tir, mais je vais tout vous expliquer.

(c):
tirs[n].angle = PA_AdjustAngle(tirs[n].angle, tirs[n].chgangle, tirs[n].x, tirs[n].y, Stylus.X, Stylus.Y+192);
En fait, on donne à la fonction l'angle actuel, la vitesse de rotation, la position actuelle, et la cible, et l'angle sera ajusté de la vitesse de rotation dans un sens ou dans l'autre si besoin.
Le problème principal avec les angles, c'est qu'un angle fait intervenir des calculs relativement complexes pour le déterminer, ce qui est assez gênant dans des jeux avec plusieurs choses à tête chercheuse... Il est donc préférable d'avoir un algorithme plus optimisé, qui permettrait de ne pas avoir à recalculer l'angle systématiquement...
(c):
extern inline u64 PA_Distance(s32 x1, s32 y1, s32 x2, s32 y2) {
s64 h = x1 - x2;
s64 v = y1 - y2;
return(h*h + v*v);
}
u16 PA_AdjustAngle(u16 angle, s16 anglerot, s32 startx, s32 starty, s32 targetx, s32 targety) {
u64 distances[3];
startx = startx << 8; // Fixed point...
starty = starty << 8; // Fixed point...
targetx = targetx << 8; // Fixed point...
targety = targety << 8; // Fixed point...
u16 tempangle = (angle - anglerot) & 511;
// Calcul des distances en fonction des angles
distances[0] = PA_Distance(startx + PA_Cos(tempangle), starty - PA_Sin(tempangle), targetx, targety);
tempangle += anglerot;
tempangle &= 511;
distances[1] = PA_Distance(startx + PA_Cos(tempangle), starty - PA_Sin(tempangle), targetx, targety);
tempangle += anglerot;
tempangle &= 511;
distances[2] = PA_Distance(startx + PA_Cos(tempangle), starty - PA_Sin(tempangle), targetx, targety);
// On regarde si l'angle est optimal. Si ce n'est pas le cas,
// on fait tourner toujours dans le même sens...
if (distances[0] < distances[1]) angle -= anglerot;
else if (distances[2] < distances[1]) angle += anglerot;
return (angle&511);
}
Premièrement, la fonction PA_Distance. Elle renvoie la distance... AU CARRÉ ! Pourquoi ? Parce qu'on ne va pas avoir besoin d'avoir la distance exacte pour faire marcher le tout, et si on peut éviter d'utiliser une fonction racine carrée un peu couteuse en temps CPU, ce n'est pas plus mal.

Ensuite, la fonction PA_AjustAngle à proprement parler... Le principe sous-jacent est relativement simple. Le missile que l'on dirige va avancer dans la direction actuelle. Il peut tourner dans un sens ou dans l'autre, et on voudrait savoir dans quel sens aller... Pour déterminer ce dernier, il suffit en fait de savoir quelle direction lui permettra d'être le plus proche de la cible en cas de mouvement

Il suffit donc de faire comme si on déplaçait le missile à une vitesse petite (pour ne pas dépasser le point cible

) dans 3 directions différentes :
- Angle de base
- Angle de base - rotation
- Angle de base + rotation
On compare alors la distance entre ces 3 points et la cible, le point le plus proche est celui qui nous intéresse

C'est dans cette direction que le missile doit tourner.

Cette méthode est particulièrement rapide et adaptée pour ajuster un angle, mais présente 2 défauts principaux dont il faut avoir conscience :
- Si on est dans la direction parfaitement opposée à la direction optimale, que l'on tourne à droite ou à gauche, on aura la même distance, et donc ça risque de foirer.

- Si on définit un trop grand angle de rotation et qu'il faut tourner de quelques degrés à peine, ça n'ira pas non plus, car ça fonctionne sur un mode 'tout ou rien', sans intermédiaire... Pallier à ce problème est très simple, il suffit de faire exécuter 2 fois avec une vitesse de rotation 2 fois plus petite (ou 3, ou 4, etc.).
Voilà pour les infos.

Je pense que vous avez toutes les clés en main pour comprendre ce code relativement bête, mais pas si méchant. J'espère que ça pourra servir à quelqu'un.
Bon Week-end à tous et à bientôt. Si vous avez des idées particulières de démos à faire, on est toujours preneurs.
