¿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.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..
Nuestro sistema de partículas tendrá unos pocos parámetros ajustables:
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:
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 = [];
Cada fotograma que necesitamos hacer tres cosas: crear nuevas partículas, mover partículas existentes y dibujar las 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.
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; ;
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]); ;
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).
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:
¡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:
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:
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!
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: