Au sanglier codeur

Un générateur de particules

La théorie

Qu'est-ce qu'un générateur de particules ?
A quoi ça sert ?
Comment ça marche ?

Voilà les trois questions auxquelles nous répondrons dans ce chapitre, en commençant par ce que c'est et à quoi ça sert.

Un générateur de particules est un composant d'une application qui va... générer des particules. ^^
En théorie ces particules peuvent être de formes et de couleurs diverses, dans notre cas il s'agira bien sur toujours de surfaces en 2 dimensions, rectangulaires qui plus est.

Si vous ne vous rendez pas bien compte de ce que peut être une particule, regardez les screens suivants :

Oh les belles particules ! Oh les belles particules ! Oh les belles particules !

Et maintenant la question fatidique : A quoi ça sert ?
Et bien principalement à faire joli. :p

On peut représenter des tas de phénomènes physiques avec :

Basiquement le fonctionnement d'un générateur de particules est le suivant :

On part d'un point qui va être l'origine du générateur, et de là on envoie pleins de particules dans tous les sens.
Les particules font leurs petits bonhomme de chemin et disparaissent au fur et à mesure (elles deviennent transparentes).
Jusqu'au moment où elles sont entièrement transparentes, alors on les régénère, c'est à dire replacement au centre du générateur avec une couleur toute neuve.

En pratique il y a beaucoup plus de choses à prendre en compte :

Par exemple vous n'avez pas forcément envie que vos particules partent dans tous les sens.
Dans ce cas là il faut définir un intervalle pour le vecteur de déplacement que vous allez leurs appliquer.

Vous n'avez peut être pas envie non plus qu'elle soient de toutes les couleurs, vous les voulez juste blanches pour faire de la neige.
Qu'à cela ne tienne, nous définirons un intervalle supplémentaire pour la couleur.
Et ainsi de suite...

Les paramètres qui nous permettrons de régler le générateur sont les suivants :

Un intervalle est associé à chacun de ces paramètres.
Et au moment de la création des particules, une valeur sera choisit aléatoirement dans cet intervalle pour chaque particule.

Ainsi si vous voulez que vos particules se déplacent toutes à la même vitesse, il suffit de mettre les même valeurs pour le minimum et le maximum de l'intervalle.

Avant de se lancer dans le code, voyons l'algorithme général :

Tant que le générateur fonctionne :
|----Pour toutes les particules :
|----|----Si la particule est morte :
|----|----|----On régénère la particule
|----|----Sinon :
|----|----|----On met la particule à jour
|----|----Fin si
|----Fin pour
Fin tant que

Vous savez maintenant comment ça marche. Dans les grandes lignes du moins. ^^

Pour réaliser le générateur nous allons nous servir de 2 classes, CParticle et CParticleEngine.

CParticle

Nous allons maintenant voir de quoi est composée une particule.

La classe CParticle est une dérivée de CSurface, les attributs qui lui sont propres sont les suivants:

//! Les "points de vie" de la particule.
float m_life;

//! Le nombre de points retirés a chaque mise à jour.
float m_disappearance;

//! La "veritable" position de la particule.
CVect m_truePos;


Le dernier paramètre doit vous intriguer. Il est là pour que la position de la particule ne soit pas faussée.
Les attributs des vecteurs CVect sont des float, des nombres décimaux avec une précision très largement suffisante.

Mais la position d'un surface est stocké par défaut dans une instance de SDL_Rect, une structure composée de Uint16 et Sint16, des variables contenant des nombres entiers.
De par la façon dont la position est mise à jour (vous allez le voir un peu plus loin ;) ), si on passait directement par la variable SDL_Rect, on perdrait en précision et les particules se déplaceraient de façon saccadée ou fausse.

En utilisant une variable "tampon" qui stocke la véritable position avant de la convertir en nombre entiers, on est sur d'être au plus près de la position réelle. :-)

Voyons un peu maintenant les méthodes de cette classe :

//Le constructeur par défaut.
CParticle(Uint32 w = 4, Uint32 h = 4,
	Uint32 x = 0, Uint32 y = 0, Uint8 r = 255,
	Uint8 g = 255, Uint8 b = 255,
	float disappearance = 10, const CVect &move = CVect(0,0));

//Le destructeur par défaut.
~CParticle();

//Reinitialise une particule.
void reload(Uint32 w, Uint32 h, Uint32 x, Uint32 y,
	Uint8 r, Uint8 g, Uint8 b, float disappearance,
	const CVect &move);

//Effectue les mises à jour sur la particule.
int update(SDL_Surface *ecran);
//Modifie la valeur de m_life.
void setLife(float newLife);


Nous imaginons qu'il n'y a pas grand chose à expliquer...

Le constructeur et reload se contentent de modifier les attributs avec les valeurs passées en paramètre.
Le destructeur est vide, et setLife ne sert, comme son nom l'indique, qu'à modifier l'attribut m_life.

La méthode update quand à elle est un peu plus intéressante.
Voici son contenu :

/* 01 */ int CParticle::update(SDL_Surface *ecran)
/* 02 */ {
/* 03 */     //Si la particule est complètement morte, on le signale.
/* 04 */     if (m_life - m_disappearance < 0)
/* 05 */         return 1;
/* 06 */     else
/* 07 */     {
/* 08 */         //On modifie la véritable position selon le vecteur.
/* 09 */         m_truePos += m_moveVect;
/* 10 */         //Et on cast la véritable position en nombre entier.
/* 11 */         m_position.x = static_cast<Sint16>(m_truePos.m_x);
/* 12 */         m_position.y = static_cast<Sint16>(m_truePos.m_y);
/* 13 */         m_life -= m_disappearance;
/* 14 */         //Le degré de transparence de la particule dépend de la variable m_life.
/* 15 */         SDL_SetAlpha(m_baseSurf, SDL_SRCALPHA, m_life);
/* 16 */         paint(ecran);
/* 17 */         return 0;
/* 18 */     }
/* 19 */ }


Voilà l'algorithme simplifié de la fonction :

Si la particule est morte
|----On renvoie un 1
Sinon
|----On la met à jour
|----On renvoie un 0
Fin si

Vous pouvez voir qu'à la ligne 4 on teste la vie de la particule moins la vitesse de disparition.
En effet puisque la mise à jour consiste entre autre à soustraire la vitesse de disparition à la vie (ligne 13), si celle ci tombe à 0 après la mise à jour, plus aucun intérêt de l'afficher, elle sera entièrement transparente. Autant la régénérer dès maintenant.

En ce qui concerne le déplacement de la particule, vous pouvez voir qu'à la ligne 9 on ajoute le vecteur à la position théorique de la particule.
Puis on cast la véritable position en nombre entier (lignes 11 et 12) pour modifier l'attribut m_position (de type SDL_Rect) qui détermine quelle position effective aura la particule.

CParticleEngine

Comme dit au début du chapitre, plusieurs paramètres vont nous permettre de régler le générateur, et chacun de ces paramètres va correspondre à un attribut des particules.

Nous avons également dit que ces paramètres seraient des intervalles à l'intérieur desquels nous choisiront aléatoirement une valeur.
Et bien pour faciliter la gestion de ces intervalles, nous allons créer une structure composée tout bêtement de 2 variables qui seront les 2 extremums de l'intervalle :

struct SInterval
{
	float min, max;
};

Voyons tout de suite la fonction qui va nous permettre de générer un nombre aléatoire compris dans cet intervalle :

float CParticleEngine::geneAlea(const CVect &interval)const
{
	//On commence par récupérer un nombre entre 0 et 1.
	//Puis on le multiplie par la taille de l'intervalle.
	return interval.m_x + ((float) rand() / RAND_MAX * (interval.m_y - interval.m_x));
}


Nous pensons que les commentaires suffisent à comprendre le fonctionnement.
Si vous n'êtes pas familier des nombres pseudo aléatoires en langage C et C++, reportez vous aux pages de man.

Voyons maintenant de quoi va être composé le constructeur du générateur.
En premier lieu, il faut savoir qu'il ne prendra qu'un argument : le nombre de particules à gérer.

Pour les gérer justement, nous nous servirons d'un std::vector en attribut.
Le paramètre indiquant le nombre de particules va nous permettre de redimensionner ce vector.

//m_particles est un std::vector de CParticle et m_nbParticles est
//le nombre de particules que le moteur doit gerer.
m_particles.resize(m_nbParticles);


Maintenant que nous avons un tableau avec le nombre exact de cases, il nous faut créer une CParticle dans chacune d'entre elles :

/* 01 */ for (int i = 0; i < m_nbParticles; i++)
/* 02 */ {
/* 03 */        //On cree la particule.
/* 04 */        m_particles[i] = new CParticle();
/* 05 */        //on cree un vecteur a partir des intervalles pour les coordonnees polaires.
/* 06 */        SPolarCoord coord =
/* 07 */        {
/* 08 */               geneAlea(interAngle), geneAlea(interSpeed)
/* 09 */        };
/* 10 */
/* 11 */        //On initialise la particule
/* 12 */        m_particles[i]->reload(
/* 13 */               //La taille de la particule
/* 14 */               static_cast<Uint32>(geneAlea(interWidthParticles)),
/* 15 */               static_cast<Uint32>(geneAlea(interHeightParticles)),
/* 16 */               //La position de depart
/* 17 */               static_cast<Uint32>(geneAlea(interBeginPosX)),
/* 18 */               static_cast<Uint32>(geneAlea(interBeginPosY)),
/* 19 */               //La couleur
/* 20 */               static_cast<Uint8>(geneAlea(interRed)),
/* 21 */               static_cast<Uint8>(geneAlea(interGreen)),
/* 22 */               static_cast<Uint8>(geneAlea(interBlue)),
/* 23 */               //La vitesse de disparition
/* 24 */               geneAlea(interDisappearance),
/* 25 */               //On genere aleaoirement les composantes du vecteur de mouvement.
/* 26 */               CVect(coord));
/* 27 */        //On met la vie a zero pour qu'une regeneration des
/* 28 */        //particules soit effectuee des la prochaine mise a jour
/* 29 */        //pour prendre en compte les parametres de l'utilisateur.
/* 30 */        m_particles[i]->setLife(0);
/* 31 */ }


Voyons un peu tout ça :

Premièrement, on peut voir ligne 1 qu'on fait une boucle pour passer en revue toutes les particules du générateur.

On commence par les créer avec une allocation dynamique ligne 4.
Ensuite on crée une instance de SPolarCoord en générant aléatoirement ses 2 composantes.

Peut être est il temps d'expliquer comment se déplacent les particules :
Plutôt que d'utiliser des coordonnées cartésiennes pour définir le vecteur de déplacement des particules, il est plus simple d'utiliser des coordonnées polaires.
Ainsi on peut facilement définir les paramètres "angle de dispersion" et "vitesse de déplacement".

Pour un intervalle d'angle de dispersion entre 0 et 360, les particules seront projetées dans toutes les directions.
En revanche si l'intervalle est entre 0 et 90, les particules ne seront projetées que dans le quart supérieur droit à partir de l'origine du générateur.

Une fois que nous avons cette instance de SPolarCoord, nous n'avons plus qu'à reloader la particule avec les bonnes valeurs pour son constructeur.

Ensuite, le constructeur : son rôle sera de désallouer toute la mémoire prise par les particules dans le sdt::vector :

//On cree un iterateur pour parcourir le vector et supprimer chaque element.
std::vector<CParticle*>::iterator iter;
for(iter = m_particles.begin(); iter != m_particles.end(); iter++)
{
	delete (*iter);
	m_particles.erase(iter);
}

La dernière méthode qu'il nous reste à voir est celle faisant la mise à jour du générateur :

//On passe tout le tableau de particules en revue.
for (int i = 0; i < m_nbParticles; i++)
{
	//On fait la mise en jour en verifiant que la particule n'est pas morte.
	if (m_particles[i]->update(ecran) == 1)
	//Si c'est le cas on la recharge.
	{
		SPolarCoord coord =
		{
			geneAlea(interAngle), geneAlea(interSpeed)
		};
	//On initialise les particules
	m_particles[i]->reload(
			static_cast<Uint32>(geneAlea(interWidthParticles)),
			static_cast<Uint32>(geneAlea(interHeightParticles)),
			static_cast<Uint32>(geneAlea(interBeginPosX)),
			static_cast<Uint32>(geneAlea(interBeginPosY)),
			static_cast<Uint8>(geneAlea(interRed)),
			static_cast<Uint8>(geneAlea(interGreen)),
			static_cast<Uint8>(geneAlea(interBlue)),
			geneAlea(interDisappearance), CVect(coord));
	}
}


Les commentaires parlent d'eux mêmes. :)
On se sert de la valeur de retour de update (que l'on a vu plus haut) pour savoir s'il faut régénérer la particule ou pas.

Conclusion

Les générateurs de particules sont les amis de tout programmeur voulant coder un jeu sympa aux graphismes élaborés.
Leur caractère aléatoire permet d'ajouter une touche de réalisme au jeu (si tant est qu'un jeu 2D puisse être réaliste).

Les possibilités d'utilisation sont quasi-infinies, et le moteur présenté ici est somme toute assez basique.
Essayez de l'améliorer par vous même pour obtenir des résultats encore meilleurs. ;)


Valid XHTML 1.0 Strict - Le Sanglier Codeur, par GuilOooo & Kevin Leonhart - Remonter en haut - Valid CSS !