[XNA] Sistemas de partículas en 2D

Otro “must-do” de la programación gráfica es implementar nuestro propio sistema de partículas. Un sistema de partículas, a pesar de lo sofisticado de su nombre, no es más que un montón de “objetos”, a los que llamaremos partículas, cada uno de ellos con un comportamiento autónomo, pero con cierta relación con el de las demás partículas del mismo sistema. Esto se acostumbra a utilizar para generar efectos parecidos a los que se producen en la naturaleza, como humo, fuego, explosiones… la idea básica aplica tanto para 2D como 3D, sólo cambiarían los tipos de vectores y la forma de pintar, pero la lógica sería la misma o muy parecida.

Como no se si os habré aclarado o liado más con mi particular definición de lo que es un sistema de partículas… os pongo un enlace a la definición técnica de la wikipedia

Así pues, sin más dilación, en este ejemplo os explicaré como implementar un sencillo sistema de partículas para generar un efecto parecido al humo, básicamente el código hace lo que podéis ver en este vídeo:

[View:http://www.youtube.com/watch?v=W4_SX4DNf5c:550:0]

Cómo podéis ver el efecto es muy sencillo, pero trabajándolo se pueden conseguir cosas muy interesantes. Veamos el código de una vez…

Toda la lógica se almacena en una clase que he llamado ManagerParticulas. Con esto conseguimos tener un objeto reutilizable para aplicar las partículas de ese tipo en el momento que queramos. Contiene un montón de constantes, la textura (que es la misma para todas las partículas) y la posición inicial de la que parten todas las partículas “en principio”. Y lo pongo entre comillas porqué al inicializar cada partícula es muy importante que apliquemos un factor de aleatoriedad a cada propiedad de la misma… Todas esas constantes definen valores máximos y mínimos relativos a las partículas.

managerParticulas

La clase Particula es la que nos permite almacenar la información de los cientos de partículas que podemos manejar a la vez.

particula

Vamos a ver algunos de los métodos más interesantes… El constructor cargará la textura y inicializa el array de partículas, y establece la posición inicial.

   1: public ManagerParticulas(ContentManager contentManager, int numeroParticulas, Vector2 posicionInicial) 

   2: {

   3:     this.posicionInicial = posicionInicial;

   4:  

   5:     // Inicializar textura (es la misma para todas las partículas)

   6:     this.textura = contentManager.Load<Texture2D>("humo");

   7:  

   8:     // Inicializar array de partículas, aplicando aleatoriedad en los parámetros para que parezca más "natural"

   9:     particulas = new Particula[numeroParticulas];

  10:     

  11: }

El método Update, que se llamará desde fuera del Manager de Partículas, llama a los métodos encargados de mover a las partículas y de parametrizar a las no inicializadas.

   1: public void Update(GameTime gameTime) 

   2: {

   3:     // Animar partículas "vivas"

   4:     this.AnimarParticulas(gameTime);

   5:  

   6:     // Reparametrizar partículas "muertas"

   7:     this.ParametrizarParticulas(this.posicionInicial, gameTime);

   8: }

La animación como he comentado mueve las partículas y además establece un fade-in fade-out para que las partículas no aparezcan y desaparezcan de golpe, creando un efecto más natural. Esto lo hacemos estableciendo la propiedad alfa de cada instancia de la clase Particula.

   1: private void AnimarParticulas(GameTime gameTime) 

   2: {

   3:     for (int i = 0; i < particulas.Length; i++) 

   4:     {

   5:         if (particulas[i] != null) 

   6:         {

   7:             // Movemos las partículas

   8:             particulas[i].edad += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

   9:             particulas[i].Mover();

  10:  

  11:             // Aplicamos fadeout

  12:             if (particulas[i].edad >= particulas[i].edadMuerte * TIEMPO_INICIO_DECREMENTO_FADEOUT)

  13:             {

  14:                 if (particulas[i].tiempoDesdeUltimoFadeout >= INTERVALO_FADEOUT)

  15:                 {

  16:                     particulas[i].tiempoDesdeUltimoFadeout = 0.0f;

  17:                     particulas[i].alfa -= DECREMENTO_FADEOUT;

  18:                 }

  19:             }

  20:             else

  21:             {

  22:                 if(particulas[i].alfa<MAX_ALFA)

  23:                     particulas[i].alfa += INCREMENTO_FADEOUT;

  24:             }

  25:  

  26:             particulas[i].tiempoDesdeUltimoFadeout += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

  27:         }                

  28:     }

  29: }

Este método es el que tiene más “chicha”, es el que parametriza las partículas, las crea en el intervalo establecido y les aplica un factor de “aleatoriedad” a sus propiedades, para que todas las partículas sean “parecidas pero no iguales” (sinó menudo humo más raro no?).

Comentar que hay varios bucles for que se podrían poner en una sola función, y ahorraríamos tiempo de procesamiento (lo cual se traduce en que podríamos meter más parículas en el sistema sin afectar al rendimiento), pero aquí lo he separado para que el código sea más limpio y fácil de entender.

   1: private void ParametrizarParticulas(Vector2 posicionInicial, GameTime gameTime)

   2: {

   3:     for (int i = 0; i < particulas.Length; i++)

   4:     {

   5:         if (tiempoCreacionUltimaParticula >= INTERVALO_CREACION_PARTICULAS)

   6:         {

   7:             // Instanciamos una nueva partícula si todavía no lo está

   8:             if (particulas[i] == null)

   9:             {

  10:                 particulas[i] = new Particula();

  11:             }

  12:  

  13:             // Sólo reparametrizamos las partículas "muertas"

  14:             if (particulas[i].edad >= particulas[i].edadMuerte)

  15:             {

  16:                 // Reinicializamos la edad

  17:                 particulas[i].edad = 0.0f;

  18:  

  19:                 // Reinicializamos alfa

  20:                 particulas[i].alfa = 0.0f;

  21:                 particulas[i].tiempoDesdeUltimoFadeout = 0.0f;

  22:  

  23:                 // Situamos la posición inicial

  24:                 particulas[i].posicion = posicionInicial + new Vector2((float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_POSICIONINICIAL), (float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_POSICIONINICIAL));

  25:  

  26:                 // Establecemos la aceleración

  27:                 particulas[i].aceleracion.X = (float)Aleatorio.ObtenerAleatorio(MAX_ACELERACION_X) ;

  28:                 particulas[i].aceleracion.Y = -Aleatorio.ObtenerAleatorio(MAX_ACELERACION_Y) - MIN_ACELERACIONY;

  29:                 particulas[i].aceleracion.Normalize();

  30:  

  31:                 if (Aleatorio.ObtenerAleatorio(10) <= 5)

  32:                     particulas[i].aceleracion.X *= -1;

  33:  

  34:                 // Establecemos la edad de la "muerte"

  35:                 particulas[i].edadMuerte = EDAD_MUERTE_INICIAL;

  36:                 particulas[i].edadMuerte += Aleatorio.ObtenerAleatorio(MAX_DESVIACION_MUERTE);

  37:  

  38:                 // Establecemos la desviación en la escala (queremos unas partículas más pequeñas que otras)

  39:                 particulas[i].escala = (float)1.0f / (float)Aleatorio.ObtenerAleatorio(MAX_DESVIACION_ESCALA);

  40:  

  41:                 tiempoCreacionUltimaParticula = 0.0f;

  42:             }

  43:         }

  44:         else 

  45:         {

  46:             tiempoCreacionUltimaParticula += (float)gameTime.ElapsedGameTime.TotalMilliseconds;

  47:         }

  48:     }

  49: }

El Draw ya no tiene ningún secreto… excepto el detalle de que estamos aplicando un alpha a la textura, que eso sí es interesante:

   1: public void Draw(GameTime gameTime, SpriteBatch spriteBatch)

   2: {

   3:     for (int i = 0; i < particulas.Length; i++)

   4:     {

   5:         if(particulas[i]!=null)

   6:             spriteBatch.Draw(textura,

   7:                 particulas[i].posicion, null,

   8:                 new Color(255, 255, 255, (byte)MathHelper.Clamp(particulas[i].alfa, 0, 255)), 0.0f, Vector2.Zero, particulas[i].escala,

   9:                 SpriteEffects.None, 0);

  10:     }

  11: }

El resultado del código es el del vídeo que os he mostrado antes, podéis además descargaros el código para probarlo. Algo que os recomiendo es jugar un poco con los valores de las constantes, ampliar el número de partículas hasta 10.000 (lo aguanta perfectamente jejeje) y aumentad el tiempo de vida de las partículas, y observaréis efectos curiosos.

Hasta la próxima!

7 comentarios en “[XNA] Sistemas de partículas en 2D”

  1. Gracias Carlos. La verdad es que entre universidad y trabajo cada día es más difícil… pero comentarios como los tuyos me ayudan a seguir 🙂

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *