Haga su juego Pop con efectos de partículas y Quadtrees

¿Así que quieres explosiones, disparos, balas o hechizos mágicos en tu juego? Los sistemas de partículas hacen grandes efectos gráficos simples para darle un poco de sabor a tu juego. Puedes sorprender al jugador aún más haciendo que las partículas interactúen con tu mundo, rebotando en el entorno y en otros jugadores. En este tutorial implementaremos algunos efectos de partículas simples, y desde aquí pasaremos a hacer que las partículas reboten en el mundo que las rodea..

También optimizaremos las cosas implementando una estructura de datos llamada quadtree. Quadtrees te permite buscar colisiones mucho más rápido de lo que podrías sin una, y son fáciles de implementar y comprender..

Nota: Aunque este tutorial está escrito con HTML5 y JavaScript, debería poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos.

Para ver las demostraciones en el artículo, asegúrese de leer este artículo en Chrome, Firefox, IE 9 o cualquier otro navegador que admita HTML5 y Canvas.
Observe cómo las partículas cambian de color al caer y cómo rebotan en las formas..

Qué es un sistema de partículas?

Un sistema de partículas es una forma simple de generar efectos como fuego, humo y explosiones.

Creas un emisor de partículas, y esto lanza pequeñas "partículas" que puede mostrar como píxeles, cuadros o pequeños mapas de bits. Siguen la física newtoniana simple y cambian de color a medida que se mueven, lo que da como resultado efectos gráficos dinámicos y personalizables..


El inicio de un sistema de partículas

Nuestro sistema de partículas tendrá unos pocos parámetros ajustables:

  • ¿Cuántas partículas escupe cada segundo?.
  • Cuánto tiempo puede "vivir" una partícula.
  • Los colores que cada partícula transitará a través.
  • La posición y el ángulo de las partículas se originarán a partir de.
  • ¿Qué tan rápido irán las partículas cuando desovan?.
  • ¿Cuánta gravedad debería afectar a las partículas?.

Si cada partícula generara exactamente lo mismo, tendríamos un flujo de partículas, no un efecto de partículas. Entonces permitamos también la variabilidad configurable. Esto nos da algunos parámetros más para nuestro sistema:

  • Cuánto puede variar su ángulo de lanzamiento.
  • Cuánto puede variar su velocidad inicial.
  • Cuánto puede variar su vida.

Terminamos con una clase de sistema de partículas que comienza así:

 función ParticleSystem (params) // Parámetros predeterminados this.params = // Donde las partículas se originan de pos: nuevo Punto (0, 0), // Cuántas partículas se generan cada segundo partículasPerSegundo: 100, // Cuánto tiempo vive cada partícula (y cuánto puede variar) partícula Vida: 0,5, vidaVariación: 0,52, // El degradado de los colores por los que la partícula viajará a través de los colores: nuevo Gradiente ([nuevo Color (255, 255, 255, 1), nuevo Color (0, 0, 0, 0)]), // El ángulo con el que la partícula disparará (y cuánto puede variar) ángulo: 0, angleVariation: Math.PI * 2, // El rango de velocidad a la que la partícula disparará minVelocity: 20, maxVelocity: 50, // El vector de gravedad aplicado a cada partícula de gravedad: nuevo Punto (0, 30.8), // Un objeto contra el que realizar colisiones y rebotar el factor de amortiguamiento // para dicho colisionador colisionador: nulo bounceDamper: 0.5; // Anular nuestros parámetros predeterminados con los parámetros proporcionados para (var p en params) this.params [p] = params [p];  this.particles = []; 

Haciendo que el sistema fluya

Cada fotograma que necesitamos hacer tres cosas: crear nuevas partículas, mover partículas existentes y dibujar las partículas.

Creación de partículas

Crear partículas es bastante simple. Si estamos creando 300 partículas por segundo, y han pasado 0.05 segundos desde el último fotograma, creamos 15 partículas para el fotograma (que promedia a 300 por segundo).

Deberíamos tener un bucle simple que se vea así:

 var newParticlesThisFrame = this.params.particlesPerSecond * frameTime; para (var i = 0; i < newParticlesThisFrame; i++)  this.spawnParticle((1.0 + i) / newParticlesThisFrame * frameTime); 

Nuestro spawnParticle () La función crea una nueva partícula basada en los parámetros de nuestro sistema:

 ParticleSystem.prototype.spawnParticle = function (offset) // Queremos disparar la partícula en un ángulo aleatorio y una velocidad aleatoria // dentro de los parámetros dictados para este sistema var angle = randVariation (this.params.angle, this. params.angleVariation); var speed = randRange (this.params.minVelocity, this.params.maxVelocity); var life = randVariation (this.params.particleLife, this.params.particleLife * this.params.lifeVariation); // Nuestra velocidad inicial se moverá a la velocidad que elegimos arriba en la dirección // del ángulo que elegimos var speed = new Point (). FromPolar (angle, speed); // Si creamos cada partícula única en "pos", entonces cada partícula // creada dentro de un marco comenzará en el mismo lugar. // En su lugar, actuamos como si hubiéramos creado la partícula continuamente entre // este cuadro y el cuadro anterior, comenzando con un cierto desplazamiento // a lo largo de su trayectoria. var pos = this.params.pos.clone (). add (velocity.times (offset)); // Construir un nuevo objeto de partícula a partir de los parámetros que elegimos this.particles.push (nueva Particle (this.params, pos, speed, life)); ;

Elegimos nuestra velocidad inicial de un ángulo y velocidad aleatorios. Entonces usamos el dePolar () Método para crear un vector de velocidad cartesiana a partir de la combinación ángulo / velocidad.

La trigonometría básica produce la de Polar método:

 Point.prototype.fromPolar = function (ang, rad) this.x = Math.cos (ang) * rad; this.y = Math.sin (ang) * rad; devuelve esto ;

Si necesita repasar un poco la trigonometría, toda la trigonometría que estamos usando se deriva de la Unidad de Círculo.

Movimiento de partículas

El movimiento de partículas sigue las leyes básicas de Newton. Todas las partículas tienen una velocidad y una posición. Nuestra velocidad es actuada por la fuerza de la gravedad, y nuestra posición cambia proporcionalmente a la gravedad. Finalmente debemos seguir la vida de cada partícula, de lo contrario las partículas nunca morirían, terminaríamos teniendo demasiadas y el sistema se detendría. Todas estas acciones ocurren proporcionalmente al tiempo entre marcos.

 Particle.prototype.step = function (frameTime) this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); this.life - = frameTime; ;

Partículas de dibujo

Finalmente tenemos que dibujar nuestras partículas. La forma en que implementes esto en tu juego variará enormemente de una plataforma a otra, y qué tan avanzado quieres que sea el renderizado. Esto puede ser tan simple como colocar un solo píxel de color, para mover un par de triángulos para cada partícula, dibujados por un complejo sombreador de GPU.

En nuestro caso, aprovecharemos la API de Canvas para dibujar un pequeño rectángulo para la partícula.

 Particle.prototype.draw = function (ctx, frameTime) // No es necesario dibujar la partícula si está fuera de la vida. if (this.isDead ()) return; // Queremos viajar a través de nuestro gradiente de colores a medida que la partícula envejece var lifePercent = 1.0 - this.life / this.maxLife; var color = this.params.colors.getColor (lifePercent); // Configurar los colores ctx.globalAlpha = color.a; ctx.fillStyle = color.toCanvasColor (); // Rellene el rectángulo en la posición de la partícula ctx.fillRect (this.pos.x - 1, this.pos.y - 1, 3, 3); ;

La interpolación de color depende de si la plataforma que está utilizando proporciona una clase de color (o formato de representación), si proporciona un interpolador para usted y cómo desea abordar todo el problema. Escribí una pequeña clase de degradado que permite una fácil interpolación entre múltiples colores y una pequeña clase de colores que proporciona la funcionalidad para interpolar entre dos colores cualquiera.

 Color.prototype.interpolate = function (percent, other) return new Color (this.r + (other.r - this.r) * percent, this.g + (other.g - this.g) * percent, this .b + (other.b - this.b) * percent, this.a + (other.a - this.a) * percent); ; Gradient.prototype.getColor = function (percent) // Ubicación del color de punto flotante dentro de la matriz var colorF = percent * (this.colors.length - 1); //Redondear a la baja; este es el color especificado en la matriz // debajo de nuestro color actual var color1 = parseInt (colorF); //Redondeo; este es el color especificado en la matriz // sobre nuestro color actual var color2 = parseInt (colorF + 1); // Interpolar entre los dos colores más cercanos (usando el método anterior) devuelve this.colors [color1] .interpolate ((colorF - color1) / (color2 - color1), this.colors [color2]); ;

Aquí está nuestro sistema de partículas en acción.!

Partículas que rebotan

Como puede ver en la demostración anterior, ahora tenemos algunos efectos básicos de partículas. Sin embargo, carecen de cualquier interacción con el entorno que los rodea. Para que estos efectos formen parte de nuestro mundo de juego, vamos a hacer que reboten contra las paredes que los rodean..

Para empezar, el sistema de partículas ahora tomará un colisionador como parámetro Será tarea del colisionador decirle a una partícula si se ha estrellado contra algo. los paso() El método de una partícula ahora se ve así:

 Particle.prototype.step = function (frameTime) // Guarde nuestra última posición var lastPos = this.pos.clone (); // Mover this.velocity.add (this.params.gravity.times (frameTime)); this.pos.add (this.velocity.times (frameTime)); // ¿Puede esta partícula rebotar? if (this.params.collider) // Compruebe si encontramos algo var intersect = this.params.collider.getIntersection (nueva línea (lastPos, this.pos)); if (intersect! = null) // Si es así, restablecemos nuestra posición y actualizamos nuestra velocidad // para reflejar la colisión this.pos = lastPos; this.velocity = intersect.seg.reflect (this.velocity) .times (this.params.bounceDamper);  this.life - = frameTime; ;

Ahora, cada vez que la partícula se mueve, le preguntamos al colisionador si su trayectoria de movimiento ha "colisionado" a través del getIntersection () método. Si es así, restablecemos su posición (para que no esté dentro de lo que sea que se cruzó) y reflejamos la velocidad.

Una implementación básica de "colisionador" podría verse así:

 // Toma una colección de segmentos de línea que representan la función del mundo del juego Colisionador (líneas) this.lines = líneas;  // Devuelve cualquier segmento de línea intersectado por "ruta", de lo contrario nulo Collider.prototype.getIntersection = function (ruta) para (var i = 0; i < this.lines.length; i++)  var intersection = this.lines[i].getIntersection(path); if (intersection) return intersection;  return null; ;

¿Notaste un problema? Cada partícula necesita llamar collider.getIntersection () y luego cada getIntersection La llamada necesita verificar cada "muro" en el mundo. Si tiene 300 partículas (una especie de número bajo) y 200 paredes en su mundo (tampoco es irrazonable), está realizando 60,000 pruebas de intersección de líneas. Esto podría paralizar tu juego, especialmente con más partículas (o mundos más complejos).


Detección más rápida de colisiones con Quadtrees

El problema con nuestro colisionador simple es que comprueba cada pared para cada partícula. Si nuestra partícula está en el cuadrante superior derecho de la pantalla, no deberíamos perder el tiempo comprobando si se estrelló contra paredes que están solo en la parte inferior o izquierda de la pantalla. Así que, idealmente, queremos recortar los controles de las intersecciones fuera del cuadrante superior derecho:


Solo comprobamos las colisiones entre el punto azul y las líneas rojas..

¡Eso es sólo una cuarta parte de los cheques! Ahora vamos aún más lejos: si la partícula está en el cuadrante superior izquierdo del cuadrante superior derecho de la pantalla, solo deberíamos comprobar esas paredes en el mismo cuadrante:

Quadtrees te permite hacer exactamente esto! En lugar de probar contra todos En las paredes, se dividen las paredes en los cuadrantes y los sub-cuadrantes que ocupan, por lo que solo es necesario revisar algunos cuadrantes. Puede pasar fácilmente de 200 cheques por partícula a solo 5 o 6.

Los pasos para crear un quadtree son los siguientes:

  1. Comienza con un rectángulo que llena toda la pantalla..
  2. Toma el rectángulo actual, cuenta cuántos "muros" caen dentro de él.
  3. Si tiene más de tres líneas (puede elegir un número diferente), divida el rectángulo en cuatro cuadrantes iguales. Repita el paso 2 con cada cuadrante.
  4. Después de repetir los pasos 2 y 3, terminará con un "árbol" de rectángulos, sin que ninguno de los rectángulos más pequeños contenga más de tres líneas (o lo que elija).

Construyendo un quadtree. Los números representan el número de líneas dentro del cuadrante, el rojo es demasiado alto y la necesidad de subdividir.

Para construir nuestro quadtree tomamos un conjunto de "paredes" (segmentos de línea) como un parámetro, y si hay demasiados dentro de nuestro rectángulo, subdividimos en rectángulos más pequeños, y el proceso se repite.

 QuadTree.prototype.addSegments = function (segs) for (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > 3) this.subdivide (); ; QuadTree.prototype.subdivide = function () var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push (nuevo QuadTree (x, y, w2, h2)); this.quads.push (nuevo QuadTree (x + w2, y, w2, h2)); this.quads.push (nuevo QuadTree (x + w2, y + h2, w2, h2)); this.quads.push (nuevo QuadTree (x, y + h2, w2, h2)); para (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ;

Puedes ver la clase completa de QuadTree aquí:

 / ** * @constructor * / function QuadTree (x, y, w, h) this.thresh = 4; this.segs = []; this.quads = []; this.rect = new Rect2D (x, y, w, h);  QuadTree.prototype.addSegments = function (segs) for (var i = 0; i < segs.length; i++)  if (this.rect.overlapsWithLine(segs[i]))  this.segs.push(segs[i]);   if (this.segs.length > this.thresh) this.subdivide (); ; QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) devuelve null; para (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ; QuadTree.prototype.subdivide = function()  var w2 = this.rect.w / 2, h2 = this.rect.h / 2, x = this.rect.x, y = this.rect.y; this.quads.push(new QuadTree(x, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y, w2, h2)); this.quads.push(new QuadTree(x + w2, y + h2, w2, h2)); this.quads.push(new QuadTree(x, y + h2, w2, h2)); for (var i = 0; i < this.quads.length; i++)  this.quads[i].addSegments(this.segs);  this.segs = []; ; QuadTree.prototype.display = function(ctx, mx, my, ibOnly)  var inBox = this.rect.containsPoint(new Point(mx, my)); ctx.strokeStyle = inBox ? '#FF44CC' : '#000000'; if (inBox || !ibOnly)  ctx.strokeRect(this.rect.x, this.rect.y, this.rect.w, this.rect.h); for (var i = 0; i < this.quads.length; i++)  this.quads[i].display(ctx, mx, my, ibOnly);   if (inBox)  ctx.strokeStyle = '#FF0000'; for (var i = 0 ; i < this.segs.length; i++)  var s = this.segs[i]; ctx.beginPath(); ctx.moveTo(s.a.x, s.a.y); ctx.lineTo(s.b.x, s.b.y); ctx.stroke();   ;

La prueba de intersección con un segmento de línea se realiza de una manera similar. Para cada rectángulo hacemos lo siguiente:

  1. Comience con el rectángulo más grande en el cuadrángulo.
  2. Compruebe si el segmento de línea se interseca o está dentro del rectángulo actual. Si no es así, no te molestes en hacer más pruebas por este camino.
  3. Si el segmento de línea cae dentro del rectángulo actual o lo intersecta, verifique si el rectángulo actual tiene algún rectángulo secundario. Si lo hace, vuelva al Paso 2, pero use cada uno de los rectángulos secundarios.
  4. Si el rectángulo actual no tiene rectángulos hijos pero es un nodo de la hoja (es decir, solo tiene segmentos de línea como hijos), pruebe el segmento de línea objetivo contra esos segmentos de línea. Si uno es una intersección, devuelva la intersección. Hemos terminado!

Buscando un Quadtree. Comenzamos en el rectángulo más grande y buscamos cada vez más pequeños, hasta que finalmente probamos segmentos de línea individuales. Con el quadtree, solo realizamos cuatro pruebas de rectángulo y dos pruebas de línea, en lugar de pruebas con los 21 segmentos de línea. La diferencia solo se hace más dramática con conjuntos de datos más grandes..
 QuadTree.prototype.getIntersection = function (seg) if (! This.rect.overlapsWithLine (seg)) devuelve null; para (var i = 0; i < this.segs.length; i++)  var s = this.segs[i]; var inter = s.getIntersection(seg); if (inter)  var o = ; return s;   for (var i = 0; i < this.quads.length; i++)  var inter = this.quads[i].getIntersection(seg); if (inter) return inter;  return null; ;

Una vez que pasamos un QuadTree si nos oponemos a nuestro sistema de partículas, como el "colisionador" obtendremos búsquedas increíblemente rápidas. Vea la demostración interactiva a continuación: use su mouse para ver en qué segmentos de línea tendría que probar el quadtree!


Pase el cursor sobre un (sub) cuadrante para ver qué segmentos de línea contiene.

Comida para el pensamiento

El sistema de partículas y el quadtree presentados en este artículo son sistemas de enseñanza rudimentarios. Algunas otras ideas que quizás desee considerar al implementarlas usted mismo:

  • Es posible que desee mantener objetos además de segmentos de línea en el quadtree. ¿Cómo lo expandirías para incluir círculos? Cuadrícula?
  • Es posible que desee una forma de recuperar objetos individuales (para notificarles que han sido alcanzados por una partícula), al mismo tiempo que recupera los segmentos reflectantes..
  • Las ecuaciones físicas sufren las discrepancias que las ecuaciones de Euler acumulan con el tiempo con tasas de trama inestables. Si bien esto generalmente no importa para un sistema de partículas, ¿por qué no leer sobre ecuaciones de movimiento más avanzadas? (Eche un vistazo a este tutorial, por ejemplo).
  • Hay muchas maneras de almacenar la lista de partículas en la memoria. Una matriz es la más simple pero puede que no sea la mejor opción ya que las partículas a menudo se eliminan del sistema y las nuevas a menudo se insertan. Una lista enlazada puede encajar mejor pero tiene una mala localidad de caché. La mejor representación para partículas puede depender del marco o lenguaje que esté utilizando.
Artículos Relacionados
  • Utilice Quadtrees para detectar posibles colisiones en el espacio 2D