Mejorando Tu Engine I: Sistemas de Partículas

Este es el primer artículo de la serie “Mejorando tu Engineâ€, en la que haremos un repaso a una serie de subsistemas que he escrito para mi propio engine y que me facilita bastante el trabajo reduciendo el tiempo de desarrollo de los juegos que haga con él. De hecho el motor no está íntegramente escrito por mí, sino que se trata del PopCap Games Framework con una serie de extensiones hechas por mí, algunas de las cuales serán precisamente las que revisemos en esta serie.

En los artículos habrá fragmentos de código en C++, aunque normalmente estará reducido respecto al original, ya sea en cuanto a funcionalidad o en cuanto a sintaxis del lenguaje, para hacerlo más claro. En cada artículo pondré disponible el código en C++ real, el que yo uso, de la parte que estemos tratando. Sin embargo, tened en cuenta que todo el framework está relacionado y es difícil de utilizar tal cual, sin el resto del código. Por esto, en el último capítulo pondré disponible el código de todo el motor para el que quiera ver todo el sistema.

Requisitos

A la hora de implementar cualquier porción de código medianamente complejo, lo primero y obvio que tenemos que decidir es lo que va a hacer, su función. En este caso se trata de un código que nos permita crear y utilizar sistemas de partículas en nuestros juegos. Un sistema de partículas es una de las técnicas más utilizadas para crear efectos especiales tales como fuego, lluvia, chispas, sangre, explosiones, fogonazos del motor de una nave, disparos, etc. Tal objeto emite una serie de elementos muy básicos (las partículas) con una serie de propiedades iniciales (posición, velocidad, color, textura..) y modificándolos durante un tiempo (aplicándoles gravedad, cambiando su color…) hasta que pasa un tiempo y desaparecen.

Construir un motor de sistemas de partículas que pueda hacer fácilmente todos los efectos que se nos puedan ocurrir es complicado, pero intentaremos acercarnos todo lo posible a esa idea. La idea para diseñar el sistema de la forma que lo he hecho lo tomé prestado de OGRE, cuyo motor de sistemas de partículas está muy bien organizado. Los requisitos a destacar que debe cumplir el sistema son estos:

  • Fácilmente extensible. Ya hemos dicho que conseguir un motor capaz de crear todos los sistemas de partículas que se nos puedan ocurrir es prácticamente imposible, optaremos por la opción de hacerlo basado en extensiones. Luego veremos cómo lo hacemos, pero básicamente nuestros sistemas de partículas estarán construídos a base de pequeños componentes que pueden ser creados y eliminados en tiempo de ejecución, y lo más importante, podemos escribir nuevos tipos de componentes fácilmente que proporcionen nueva funcionalidad al sistema.
  • Data-driven. Es decir, que nuestros sistemas de partículas se definirián en ficheros externos a la aplicación, por lo que para añadir nuevos no tendremos que compilar de nuevo el proyecto, simplemente crear ficheros de texto. Al igual que en otros subsistemas del motor, que descriviremos en otros artículos, utilizaremos ficheros XML, pues el PopCap Framework contiene ya un parser de XML y además da como resultado ficheros muy cómodos de leer y entender para una persona.
  • Posibilidad de crear sistemas recursivos. No hace falta que busquéis este término porque me lo acabo de inventar, a falta de otro nombre mejor. Con sistemas recursivos me refiero a que un sistema pueda emitr elementos, que a su vez emitan elementos, como por ejemplo en unos fuegos artificiales, cuando lanzamos un cohete hacia arriba y estalla, lanza unos fuegos que al rato estallan de nuevo, mostrando chispas. Esto raramente lo usaréis en un juego, pero está bien tenerlo y ya veréis que no nos supone para nada un problema.

Siempre que tengo que diseñar un código de cierta complejidad, después de saber lo que quiero que haga, me pregunto cómo quiero que lo haga. Escribo un pseudocódigo que haga uso de ese sistema de la forma que me parezca más cómoda, y luego adapto mi implementación a esa interfaz de uso. En este caso, aparte de los ficheros XML donde definiremos nuestros sistemas de partículas, usaremos el sistema de la siguiente forma.

ParticleSystem* myPS = PSYS_MGR->GetPSys(“PSYS_TESTâ€)->Clone();
myPS->SetCenter(Vector2(x, y));
SCENE_MGR->Attach(myPS);

Para entender plénamente el porqué de este código hay que saber como funciona el engine en su totalidad, así que intentaré aclarar una serie de cosas que serán útiles tanto para este como para los siguientes artículos.

PSYS_MGR y SCENE_MGR son dos macros que sirven de atajo a varios subsistemas del engine (el gestor de sistemas de partículas y el gestor de la escena) que utilizo extensivamente para no tener que escribir constantemente el valor de cada macro (por ejemplo, PSYS_MGR se expande en gSefrexAppBase->mParticleManager). “PSYS_TEST†es el nombre que habríamos dado a nuestra definición de sistema de partículas en el fichero externo XML. Este identificador debe ser, obviamente, único dentro de la aplicación. Una idea es crear estos nombres como si de rutas de fichero se trataran, dividiéndolos en categorías (“effects/fire/torch1â€, “effects/snow/windyâ€, etc.). Los archivos XML únicamente los parseamos al iniciar la aplicación, creando de ellos un objeto de tipo ParticleSystem con él. Este objeto actuará de modelo para los sistemas de partículas que nosotros queramos crear a partir de esa definición. Por lo tanto, y para no modificar ese modelo, lo que hacemos es clonarlo utilizando el método Clone(). La segunda línea se explica por sí sola, simplemente posicionamos nuestro sistema de partículas donde queramos. En la tercera lo que hacemos es añadir el objeto al gestor de la escena. Podemos hacer esto porque haremos que la clase ParticleSystem derive de SceneObject. Con esto conseguimos despreocuparnos por completo del sistema de partículas. Es decir, el gestor de escena se ocupará de actualizarlo, dibujarlo y destruirlo cuando este muera. Nosotros sólo tenemos que indicarle al motor “quiero un sistema de partículas de ESTE TIPO y AQUÃâ€. Por supuesto, podemos mantener el objeto myPS para modificarlo manualmente a placer, o si es un sistema que no muere (emite partículas constantemente) para destruírlo cuando queramos. Creo que no es posible simplificarlo más que esto, así que conseguirlo será nuestro objetivo.

Clases del Sistema

Antes de pasar a detallar cada una de las partes del sistema que queremos crear mostraré las clases que intervendrán y las relaciones entre ellas.

  • Particle. Objeto creado por un Emitter.
  • Emitter. Objeto que crea Particles y contiene Affectors.
  • Affector. Objeto que modifica Particles.
  • ParticleSystem. Agrupa Emitters.
  • PSysManager. Carga ParticleSystems de disco y mantiene los modelos disponibles para la aplicación.

Las Partículas

Una partícula es “aquello emitido por un sistema de partículasâ€. Esto podrá ser una imagen estática, animada, una chispa (o spark) o Emitter (recordemos que queremos poder crear sistemas recursivos).

Veamos la propiedades básicas de toda partícula:

class Particle
{
    int mLife;                   // vida inicial
    int mCurLife                 // vida actual
    Vector2 mPosition;
    Vector2 mVelocity;
    Vector2 mAcceleration;
    Vector2 mSize;               // factores de escalado
    float mMass;
    float mRotation;
    Color mColor;
    bool mAdditiveBlend;
    Emitter* mParent;        
 
    Particle(Emitter* theParent);
 
    inline float GetPercent();    
    inline bool IsAlive();    
 
    virtual void Init();        
    virtual void Update();
    virtual void UpdateF(float theFrac);    
    virtual void Draw(Graphics* g)     = 0;
    virtual Particle* Clone() = 0;
};
 

Lo mayor parte se explica por sí sola. GetPercent() nos servirá para interpolar más tarde valores de la partícula según la cantidad de vida que haya consumido ya. Init() nos servirá para reutilizar partículas sin tener que liberar y alojar memoria continuamente, cosa que afectaría al rendimiento del sistema. Update(), UpdateF() y Draw() son tres métodos heredados del PopCap Framework y que uso en toda clase que necesita ser actualizada o dibujada, como esta. Draw(), obviamente se utiliza para dibujar el objeto. Update() es llamado siempre cien veces por segundo, independientemente de la máquina donde se ejecute el programa y del refresco del monitor; y se utiliza para la lógica del programa, en esteo caso para decrementar la vida de la partícula. Puesto que es asegurado que esa función se llame con esa frecuencia, normalmente utilizaremos estos “ticks†para medir tiempo (luego 100 ticks son 1 segundo), en este caso la vida de la partícula. UpdateF() es llamado un número variable de veces por segundo, por eso se pasa el parámetro theFrac, que estará siempre en el rango [1.0, 1.67], y se utiliza para actualizar valores que afectan a la visualización del objeto, como su posición. En este caso actualizaremos la velocidad en base a la aceleración, luego la posición en base a la velocidad y finalmente pondremos la aceleración a cero, ya que esta la incrementaremos si nos interesa en cada frame utilizando un Affector. Veamos un ejemplo:

void MyClass::Update()
{
    mX += mVel.x;         // opción A: actualizamos mX a un ratio fijo
}
void MyClass::UpdateF(float theFrac)
{
    mX += mVel.x*theFrac; // opción B: actualizamos mX en base a theFrac
}

La opción A tendría como resultado un objeto que aparentemente se movería “a golpes†mientras que el objeto que usara la opción B se movería fluídamente.

Los Emisores

Un emisor es aquello que emite partículas, entendiendo por emitir el hecho de crearlas, actualizarlas y destruírlas cuando éstas mueren. De hecho, más que destruírlas, las resetearemos para evitar tener que liberar y alojar memoria constantemente. La clase Emitter será la primera que veremos que puede definirse mediante XML, así que veamos el formato que tendría.

NOTA: si probáis el código tened en cuenta que el parser distingue mayúsculas de minúsculas, luego no es lo mismo Emitter que emitter como nombre de una etiqueta. Os lo digo porque no consigo que en WordPress, el gestor de weblogs que utilizo, me muestre los nombres de las etiquetas como quiero, que es con la primera letra de cada palabra en mayúsculas. Por ejemplo, emitcontroller sería realmente EmitController.
.. dentro de un particlesystem ..
    <emitter>
        <life>int</life>    
        <quota>int</quota>
        <loop>true | false</loop>                    -- default: true
        <position x=â€int†y=â€int†/>                 -- default: (0, 0)
        <staticparticle>IMAGE HANDLE</staticparticle>    
        <animatedparticle>ANIM HANDLE</animatedparticle>
        <emitcontroller>                            -- default:
            <sine />                                 --    Constant
            <base>int</base>                         --    0
            <amplitude>int</amplitude>               --    1
            <frequency>int</frequency>               --    100
            <phase>int</phase>                       --    0
        </emitcontroller>
        .. Affectors ..
    </emitter>
.. seguimos con el ParticleSystem  ..

Bueno, hay mucho que comentar, vayamos por partes.

  • Las propiedades que tienen valores por defecto no hace falta especificarlas si nos sirve ese valor.
  • StaticParticle y AnimatedParticle indican el tipo de partícula que emite este emisor, por lo que sólo debe haber una de ellas. Si hay más, se utilizará la última que aparezca. Además de estas dos opciones también puedes añadir otra definición de Emitter anidada, en ese caso el emisor que estamos definiendo emitirá a su vez emisores.
  • Life indica la vida del sistema. Si Loop es igual a “false†cuando el emisor haya estado activo durante ese tiempo, será eliminado.
  • Quota es el máximo número de partículas que el sistema podrá tener activas, de modo que no acabe teniendo cinco mil, que al fin y al cabo son entidades independientes que deben dibujarse y por tanto consumen recursos.
  • Position indica la posición relativa al ParticleSystem o Emitter al que pertenece.
  • Con IMAGE HANDLE y ANIM HANDLE me refiero al nombre que tiene asignado ese recurso. En el PopCap Framework todos los recursos que la aplicación debe cargar se definen en el fichero properties/resources.xml, donde cada recurso (imagen, fuente de texto, sonido…) tiene asignado una cadena de texto para poder utilizarlos en vez de rutas de ficheros, que pueden cambiar durante el desarrollo de un juego. En el documento “Using PopCap Resource Manifests.doc†que acompaña al PopCap Framework podéis leer más sobre esto.
  • EmitController define un interpolador para poder especificar con más precisión como queremos que se emitan las partículas. Este interpolador puede actuar de diferentes formas, definidas por una propiedad sin atributos (en el ejemplo, ): Sine (actúa como una onda senoidal), Constant (valor constante equivalente a Base + Amplitude), Triangle (onda triangular), Square (onda cuadrada, medio periodo en Base-Amplitude y el otro medio en Base+Amplitude), Sawtooth (onda con forma de sierra, aumenta linealmente y luego cae en seco) e InverseSawtooth. La propiedad Base es algo así como un “mínimoâ€, es un valor que siempre se añade al resultado final del resto de la interpolación.
  • Echemos ahora un vistazo a la clase Emitter:

    class Emitter : public Particle
    {
        int mQuota;
        bool mLoop;
        WaveformIpol mEmitCtrl;
        Particle** mParticles;
        AffectorList mAffectors;
        ParticleSystem* mPSys;
        Particle* mPrototype;        
        
        Emitter(ParticleSystem* thePSys, Emitter* theParent = 0);
     
        virtual void Init();
        virtual void Update();
        virtual void UpdateF(float theFrac);
        virtual Particle* Clone();
        virtual Vector2 GetRealPos();
        virtual void InitParticle(Paricle*& theParticle);
    }

Vemos como están ahí las propiedades que hemos visto en el XML. Tenemos además una propiedad mPrototype, que crearemos cuando leamos en el XML una etiqueta StaticParticle, AnimatedParticle o Emitter, de modo que luego simplemente podamos clonarla para crear nuevas partículas sin preocuparnos de su clase específica.

Los primeros cuatro métodos tienen el mismo propósito que la versión de Particle, sólo que ahora también se delega a los métodos de las partículas activas y se emiten partículas cuando es necesario. GetRealPos() devuelve la posición real, en la pantalla, del emisor, ya que la que especificamos en el XML es relativa al sistema de partículas.

Los Affectors

Un affector es un objeto que modifica las partículas emitidas por un Emitter. Puede modificarlas tanto al ser creadas como al actualizarse. El hecho de que la modificación de las partículas esté basada en affectors en vez de codificada directamente en la clase Emitter nos da flexibilidad absoluta para añadir cualquier tipo de funcionalidad a nuestros sistemas de partículas. Sólo tenemos que escribir el affector que nos interese y añadirlo a un emitter. La clase base abstracta Affector no tiene sintaxis XML propia, ya que utilizaremos directamente el nombre de la clase para definir cada tipo de affector en el fichero, y tampoco tiene más propiedades que el Emitter que lo contiene.

class Affector
{
    Emitter* mParent;
 
    Affector(Emitter* theParent);
 
    virtual void OnSpawn(Particle* thePart) {}
    virtual void Update() {}
    virtual void UpdateF(float theFrac) {}
    virtual Affector* Clone() = 0;
}

Los affectors, lejos de ser meros componentes opcionales para añadir efectos a un sistema en determinadas ocasiones, son algo fundamental y totalmente necesario en cualquier sistema de partículas. Si os fijáis, la clase Emitter no sabe ni qué vida asignar a las partículas, ni qué velocidad, color, ni nada; él únicamente se dedica a crearlas, sin darles valores concretos. Es tarea de los affectors hacer tal cosa, por lo que es necesario que creemos al menos una serie de clases más específicas de affectors.

/////////////////////////////////////////////////////////
// <circlespawner>
//    <radius>double</radius>        // default: 0
//    <justarc>true|false</justarc>  // default: crear solo en perímetro?
//    <minangle>int</minangle>       // default: 0
//    <maxangle>int</maxangle>       // default: 360
// </circlespawner>
/////////////////////////////////////////////////////////
class CircleSpawner : public Affector
{
    float mRadius;
    bool mJustArc;
    int mMinAngle;
    int mMaxAngle;
 
    CircleSpawner(Emitter* theParent);
 
    virtual Affector* Clone();
    virtual void OnSpawn(Particle* thePart)
    {
        float a = DegToRad(Random(mMinAngle, mMaxAngle));
        if (mJustArc)
          thePart->mPosition = Vector2(cos(a), sin(a)) * mRadius;
        else
          thePart->mPosition = Vector2(cos(a), sin(a)) * Random(mRadius+1);
        thePart->mPosition += thePart->mParent->GetRealPos();
    }
}

Crear un affector que se encargue de “crear las partículas en círculo, en vez de en un punto†es tan fácil como esto. El resto del código es prácticamente copiar y pegar de un tipo de affector a otro, la verdadera gracia está en los métodos OnSpawn(), Update() y UpdateF().

/////////////////////////////////////////////////////////
// <lifemodifier>
//    <minlife>int</minlife>    // default: 25
//    <maxlife>int</maxlife>    // default: 50
// </lifemodifier>
/////////////////////////////////////////////////////////
class LifeModifier : public Affector
{
    int mMinLife;
    int mMaxLife;
 
    LifeModifier(Emitter* theParent);
 
    virtual Affector* Clone();
    virtual void OnSpawn(Particle* thePart)
    {
        thePart->mLife = Random(mMinLife, mMaxLife);
        thePart->mCurLife = thePart->mLife;
    }
}

Fácil, ¿verdad? En el código descargable tenéis dos ejemplos más: LinearForce y ColorFader. El primero es tan sencillo como los que hemos visto aquí, mientras que el segundo ya tiene algo más de complejidad, pues permite interpolar el color de las partículas en base a una serie de keyframes.

Como podéis comprobar, tal como hemos diseñado todo podemos añadir cualquier funcionalidad que se nos ocurra tan fácilmente como creando pequeños affectors y combinándolos luego entre sí.

Finalmente, los Sistemas de Partículas

Ya estamos terminando y además lo hacemos con lo más fácil, pues nuestra clase ParticleSystem no es más que una colección de Emitters, por así decirlo.

class ParticleSystem : public SceneObject
{
    string mName;
    EmitterList mEmitters;
    
    ParticleSystem* Clone();
    virtual void Update();            // llama al Update() de cada Emitter
    virtual void UpdateF(float theFrac);    // llama al UpdateF() ...
    virtual void Draw(Graphics* g);    // llama al Draw() ...
}

La clase PSysManager es un buen trozo de código pero muy sencillo. Un 90% del código es parseo de XML y el resto tampoco tiene mucho que ver con sistemas de partículas, por lo que no lo comentaré. Podéis echarle un vistazo al código si queréis.

Con todo lo que tenemos lo único que falta es un bonito editor gráfico para ver en tiempo real los cambios que hagamos, quizá me decida a hacer uno si saco tiempo de algún sitio. Por cierto, os dejo con un archivo XML de ejemplo para que veáis cómo definiríamos un sistema de partículas ahora que ya lo tenemos todo hecho. Intentad adivinar cómo quedaría antes de probarlo.

< ?xml version="1.0"?>
 
<psysfile nameprefix="PSYS_">
    <particlesystem>
        <name>TEST</name>
        <emitter>
            <life>100</life>
            <quota>1000</quota>
            <loop>true</loop>
            <staticparticle>IMAGE_PARTICLE</staticparticle>
            <emitcontroller>
                <constant />
                <amplitude>10</amplitude>
            </emitcontroller>
            <circlespawner>
                <radius>50</radius>
            </circlespawner>
            <lifemodifier>
                <minlife>50</minlife>
                <maxlife>150</maxlife>
            </lifemodifier>
            <linearforce>
                <impulse>true</impulse>
                <force x="2" y="-2" />
            </linearforce>
            <linearforce>
                <force x="0" y="0.05" />
            </linearforce>
            <colorfader>
                <additiveblending>true</additiveblending>
                <color r="255" g="0" b="0" />
                <time>0.25</time>
                <color r="0" g="255" b="0" />
                <time>0.75</time>
                <color r="0" g="255" b="0" />
                <time>1.0</time>
                <color r="0" g="0" b="255" a="255" />
            </colorfader>
        </emitter>
    </particlesystem>
</psysfile>

Hasta aquí hemos llegado en este primer artículo sobre cómo mejorar nuestro motor de juegos, espero que os haya sido útil. Podéis descargar el código fuente de las clases aquí discutidas desde este enlace.

Demo

A petición de algunas personas he incluído una demo ejecutable de lo que hemos construído en este artículo.


Screenshot de la demo
[ Descargar Demo (791 Kb) ]

2 respuestas a “Mejorando Tu Engine I: Sistemas de Partículas”

  1. Edevi, desarrollo indie de juegos » Demo del Motor de Sistemas de Partículas dice:

    […] A petición de algunas personas he incluído una demo ejecutable de lo que hemos construído el artículo sobre sistemas de partículas. [ Descargar Demo (791 Kb) ] […]

  2. Edevi, desarrollo indie de juegos » Archivo del weblog » Mejorando Tu Engine IV - GUI: Especificaciones dice:

    […] Soy un gran partidario de los sistemas data-driven, y últimamente suelo utilizar XML para definir datos estáticos en casi cada cosa que hago (animaciones, sistemas de partículas, archivos de configuración, niveles…); así que esto no será una excepción. […]

Deja una respuesta