Un des systèmes les plus importants dans l'Usine en Folie, c'est la gestion des particules. En fait, le plus difficile ce n'est pas vraiment de les gérer, mais de ne pas créer pendant le jeu des instances de particules, et de faire en sorte que la consommation de mémoire reste stable.
Petite vidéo comparative entre la version C# et la version Ruby (dont la consommation de mémoire change d'un Mo par seconde environ).
Maintenant, petits détails techniques sur comment c'est fait...
Les contraintes
Il faut savoir que, la plus grosse contraintes que j'ai niveau programmation, c'est la mémoire. Vous me direz que maintenant les PC ont presque tous 4Go de RAM... oui, mais pas la Xbox 360, qui ne possède que 512Mo !
Une autre contrainte se rajoute aussi, mais cette fois-ci elle vient de C# : la Garbage Collection, qui produit souvent dans les jeux des "glitches" qui durent un court moment. En gros, après une certaine quantité de mémoire allouée, le Garbage Collector supprime tous les objets qui ne sont plus utilisés, ce qui inclut qu'il lui faut qu'il vérifier plus ou moins toutes les références (ce qui signifie une grosse boucle :p).
Du coup, y'a deux options pour éviter d'avoir des tonnes de "glitches" : réduire le nombre de références, ou ne pas faire d'allocations de mémoire (par exemple en créant des objets en jeu).
J'ai choisi la deuxième option, ne pas faire d'allocation de mémoire.
Difficile me direz-vous, lorsqu'on a des centaines de particules qui sont créées à l'écran !
Mais, rassurez-vous, les développeurs ont trouvé une parade pour ce genre de chose, et ça s'appelle...
... Le pooling
Le principe est simple : il faut pré-créer des instances d'objets, les utiliser, et lorsque l'on en a plus besoin ne pas les supprimer, mais les conserver pour les réutiliser : on les stocke dans la "pool". Eh oui, la même instance pourra donc servir pour afficher plusieurs particules différentes à différents moments !!
C'est pas tout ça, mais concrètement, ça donne quoi ?
Le pooling pour les particules de l'Usine
Bon, il n'existe pas qu'une méthode pour faire du pooling, mais je vais décrire celle utilisée dans l'Usine en Folie.
Le principe :
- Une liste d'objets actifs, qui seront les objets sortis de la pool. On pourra utiliser cette liste pour itérer à travers toutes les particules par exemple...
- Un tableau d'objets inactifs : la pool. Eux sont juste stockés, ils ne doivent pas être visibles à l'écran (dans le cas des particules). Ce sont juste des instances
- Une méthode GetFromPool qui retourne une particule. Elle sort un objet de la pool pour le mettre dans la liste des objets actifs, et retourne finalement l'objet sorti. Une exception doit être levée s'il n'y a plus d'instances dans la pool. Après avoir appelé cette méthode, il est nécessaire d'appeler une méthode de l'objet obtenu, qui l'initialise à partir de certains arguments (dans le cas des particules, une classe qui contient des informations comme la position, l'image à utiliser, et quelques méthodes si nécessaire).
- Une méthode Free, qui prend en argument un objet, et qui le retire des objets actifs pour le mettre dans la pool, elle peut appeler une méthode de "destruction partielle" de l'objet à retirer, par exemple, dans le cas des particules, le retirer de l'écran, et réinitialiser les valeurs des variables.
Le problème de cette méthode, c'est qu'on ne peut pas gérer les sous classes de la classe qui est stockée dans la liste, à moins bien sur, de faire plein d'opérations de boxing / unboxing, qui produisent des allocs de mémoire cachée, ce qu'on veut justement éviter. La classe qu'on va créer doit donc être générique, et une instance pourra être crée par sous-classe de particule. Le paramètre de Type aura comme contraintes d'hériter de Particule, et d'avoir un constructeur qui ne prenne pas d'argument.
Voici le code C# utilisé actuellement pour dans l'Usine en Folie pour la ParticulePool. J'espère que vous n'êtes pas allergique à l'Anglais, car je commente toujours mon code en anglais...et j'espère aussi que ça sera utile à quelqu'un =D !
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace UsineEnFolie.Particles
{
/// <summary>
/// Particle pool.
/// An instance must be created for each subtype of particle, because using
/// a single class handling every subtypes would need to make a lot of
/// boxing and unboxing operations.
/// </summary>
/// <typeparam name="ParticleClass"></typeparam>
/// <typeparam name="InitClass"></typeparam>
public class ParticlePool<ParticleClass> where ParticleClass : Particle, new()
{
/* -------------------------------------------------------------------------
* Variables
* ------------------------------------------------------------------------*/
#region Variables
public const int MAX_COUNT = 400;
private List<ParticleClass> active;
private ParticleClass[] pool;
// Use it in iterations.
private int i = 0;
private int j = 0;
#endregion
/* -------------------------------------------------------------------------
* Methods
* ------------------------------------------------------------------------*/
#region Methods
/// <summary>
/// Constructeur.
/// </summary>
public ParticlePool()
{
active = new List<ParticleClass>(MAX_COUNT);
pool = new ParticleClass[MAX_COUNT];
for (i = 0; i < MAX_COUNT; i++)
{
pool[i] = new ParticleClass();
pool[i].DisposeBasic();
}
}
/// <summary>
/// Returns a List containing every active object.
/// Use it to iterate through Particles.
/// </summary>
/// <returns></returns>
public List<ParticleClass> GetActive()
{
return active;
}
/// <summary>
/// Returns one particle from the pool.
/// This item becomes active and goes as the first item of the list.
/// pool -> active.
/// LoadData() must be called on the returned Particule !
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public ParticleClass GetFromPool()
{
for (i = 0; i < MAX_COUNT; i++)
{
// Removes a reference from the pool and adds it into the active objects.
if (pool[i] != null)
{
ParticleClass ev = pool[i];
active.Add(pool[i]);
pool[i] = null;
return ev;
}
}
throw new Exception("Not enough objects in pool");
}
/// <summary>
/// Removes an event from the active ones and put in the pool.
/// active -> pool
/// </summary>
/// <param name="p">The particule to free</param>
public void Free(ParticleClass p)
{
// Removes the event from the actives ones.
active.Remove(p);
// Adds the event in the pool.
for (i = 0; i < MAX_COUNT; i++)
{
if (pool[i] == null)
{
pool[i] = ev;
break;
}
}
}
/// <summary>
/// Clears the pool, and marks all item as available.
/// pool -> active
/// </summary>
public void Clear()
{
foreach (ParticleClass ev in active)
{
for (i = 0; i < MAX_COUNT; i++)
{
if (pool[i] == null)
{
pool[i] = ev;
active.Remove(ev);
}
}
}
}
#endregion
}
}