Aprende a crear juegos en HTML5 Canvas

domingo, 18 de agosto de 2013

Sistemas de partículas

Uno de nuestros seguidores pregunta: ¿Cómo se hacen los sistemas de partículas? Los tutoriales en ingles son difíciles de comprender.

Un sistema de partículas es, básicamente, un conjunto de pequeños elementos que se mueven en grupo, comúnmente para crear efectos visuales. Crearlo no es una tarea demasiado complicada; solo requiere de algunos conocimientos variados, los cuales afortunadamente ya hemos aprendido anteriormente.

Técnicamente, nosotros hemos hecho anteriormente un sistema de partículas muy básico, cuando vimos como crear un fondo con Estrellas en el juego de naves espaciales. Pero ahora veremos como crearlo en forma con sus propiedades avanzadas, con uno de los ejemplos más comunes para su demostración: Fuegos Artificiales.

Para este ejemplo, usaremos el código base de la entrega anterior Presionando el botón del ratón, removiendo todo lo que tiene que ver con el objeto "target" y el "score". También usaremos la delta de tiempo para mover las partículas de acuerdo al tiempo pasado, similar a la forma usada en la entrega Desplazamiento angular.

Comenzaremos creando una función "Particle", que contendrá todo lo necesario para crear una partícula: Posición, tamaño, tiempo de vida (en milisegundos), velocidad (en pixeles por segundo), ángulo (en grados) y color (¡Sí! ¡Cada partícula tendrá su propio color!):
    function Particle(x,y,radius,life,speed,angle,color){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?1:radius;
        this.life=(life==null)?0:life;
        this.speed=(speed==null)?0:speed;
        this.angle=(angle==null)?0:angle;
        this.color=(color==null)?'#fff':color;
    }
Ahora crearemos otra función que se encargue de manejar todas estas partículas como un conjunto, este será el sistema de partículas. Tendrá tres funciones básicas: Crear nuevas partículas, moverlas hasta que su edad acabe y sean eliminadas, y por supuesto, dibujarlas.
    function ParticleSystem(){}
La solución fácil sería crear un arreglo dentro del sistema de partículas, que almacene todas las partículas, pero existe una forma de que el sistema de partículas funcione como el mismo arreglo necesario, y para eso, conoceremos otro de los secretos importantes de la programación orientada a objetos: la herencia.

En programación se le llama herencia a la propiedad que tiene una clase, de tener las propiedades de otra clase ya existente, además de crear las suyas propias para generar una clase completamente nueva. Por ejemplo, si queremos crear un botón, podemos crearlo a partir de una clase rectángulo, ya que comparte con ella todas sus características, como posición ancho y alto. Pero el botón, tendrá nuevas propiedades que no necesariamente son parte de un rectángulo, como el saber si está siendo presionado. En este caso, haremos que nuestro sistema de partículas herede las propiedades de un arreglo.

Javascript no tiene herencia como tal, pero su funcionamiento por prototipos permite que funciones hereden las propiedades de otras. Para hacer que el sistema de partículas herede las propiedades de un arreglo, tenemos que asignar su prototipo de la siguiente forma:
    ParticleSystem.prototype=[];
Nota que esta asignación debe realizarse antes de la creación de cualquier nuevo sistema de partículas, por lo que he puesto esta línea cerca del comienzo de nuestro código. De esta forma, podemos acceder a los elementos del arreglo directamente del objeto creado por el sistema de partículas. Para acceder a ellos desde las funciones internas del sistema de partículas, se haría con "this[i]", y se obtendría su longitud con "this.length".

Para ver un ejemplo más claro, analicemos la siguiente función en el sistema de partículas, con la que ya debes estar familiarizado, pues se encarga de rellenar cada partícula en la pantalla:
    ParticleSystem.prototype.fill=function(ctx){
        for(var i=0,l=this.length;i<l;i++){
            ctx.fillStyle=this[i].color;
            ctx.beginPath();
            ctx.arc(this[i].x,this[i].y,this[i].radius,0,Math.PI*2,true);
            ctx.fill();
        }
    }
Ahora pasemos a la función que moverá cada partícula. Usaremos la función aprendida en Desplazamiento angular para mover la partícula de acuerdo a una velocidad y ángulo, los cuales ya han sido almacenados en la partícula, por lo que solo necesitaremos obtener la delta de tiempo, para desplazarse de acuerdo al tiempo que ha pasado. También restaremos este valor a la vida de la partícula, y cuando alcance un valor menor a 0, la eliminaremos del sistema de partículas:
    ParticleSystem.prototype.move=function(deltaTime){
        for(var i=0,l=this.length;i<l;i++){
            this[i].life-=deltaTime;
            if(this[i].life<0){
                this.splice(i--,1);
                l--;
            }
            else{
                this[i].x+=Math.cos(this[i].angle)*this[i].speed*deltaTime;
                this[i].y+=Math.sin(this[i].angle)*this[i].speed*deltaTime;
            }
        }
    }
No es necesario agregar ahora la función que crea las nuevas partículas, pues el método "push" que ha sido heredado de los arreglos, se encargará de esa función.

Ahora que nuestro sistema de partículas ha sido creado, haremos el ejemplo para aprender a usarlo. Comencemos creando la variable de nuestro sistema de partículas:
    var ps=new ParticleSystem();
Dibujarlo será bastante sencillo, solo debemos de llamar a la siguiente función:
        ps.fill(ctx);
Mover las partículas en el sistema es igual de fácil, llamando esta función dentro de la función "act":
        ps.move(deltaTime);
Como podrás ver, todo el trabajo de las partículas recae en los parámetros al momento de su creación, por lo que una vez creadas, no necesitas preocuparte más de tener que interactuar con ellas en el código. Vamos ahora entonces a crear fuegos artificiales al hacer clic con el ratón. Los parámetros que le mandaremos son:

  • La posición del ratón.
  • Radio de un pixel.
  • Un tiempo de vida de medio a un segundo.
  • Una velocidad de hasta 100 pixeles por segundo.
  • Una dirección al azar en cualquiera de los 360 grados.
  • Y un color al azar para cada explosión.
        if(lastPress==1){

            var color='rgb('+random(255)+','+random(255)+','+random(255)+')';
            for(var i=0;i<200;i++)
                ps.push(new Particle(player.x,player.y,1,0.5+random(500)/1000,random(100),random(360),color));
        }
Como podrás ver, los fuegos artificiales usan muchos valores al azar. Al probar el código, podremos ver los fuegos artificiales en acción. Como detalle curioso, si pones la variable color dentro del for en lugar de fuera de él, obtendrás explosiones muy coloridas, aunque posiblemente este no sea un efecto realmente deseado al hacer fuegos artificiales.

Los sistemas de partículas tienen bastante aplicaciones, además de explosiones puedes hacer efectos de arena, lluvia, nieve, y muchos más. También puedes asignarles imágenes para complementar los efectos especiales deseados.

Código final:


[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    ParticleSystem.prototype=[];
    var canvas=null,ctx=null;
    var lastPress=null;
    var lastUpdate=0;
    var mousex=0,mousey=0;
    var bgColor='#000';
    var player=new Circle(0,0,5);
    var ps=new ParticleSystem();

    function init(){
        canvas=document.getElementById('canvas');
        ctx=canvas.getContext('2d');
        canvas.width=300;
        canvas.height=200;

        enableInputs();
        run();
    }

    function random(max){
        return ~~(Math.random()*max);
    }

    function run(){
        requestAnimationFrame(run);
            
        var now=Date.now();
        var deltaTime=(now-lastUpdate)/1000;
        if(deltaTime>1)deltaTime=0;
        lastUpdate=now;
        
        act(deltaTime);
        paint(ctx);
    }

    function act(deltaTime){
        player.x=mousex;
        player.y=mousey;

        if(player.x<0)
            player.x=0;
        if(player.x>canvas.width)
            player.x=canvas.width;
        if(player.y<0)
            player.y=0;
        if(player.y>canvas.height)
            player.y=canvas.height;

        if(lastPress==1){
            bgColor='#333';
            var color='rgb('+random(255)+','+random(255)+','+random(255)+')';
            for(var i=0;i<200;i++)
                ps.push(new Particle(player.x,player.y,1,0.5+random(500)/1000,random(100),random(360),color));
        }
        else
            bgColor='#000';
        
        ps.move(deltaTime);
    }

    function paint(ctx){
        ctx.fillStyle=bgColor;
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ps.fill(ctx);
        ctx.strokeStyle='#0f0';
        player.stroke(ctx);

        ctx.fillStyle='#fff';
        ctx.fillText('Particles: '+ps.length,0,20);
        lastPress=null;
    }

    function enableInputs(){
        document.addEventListener('mousemove',function(evt){
            mousex=evt.pageX-canvas.offsetLeft;
            mousey=evt.pageY-canvas.offsetTop;
        },false);
        canvas.addEventListener('mousedown',function(evt){
            lastPress=evt.which;
        },false);
    }

    function Circle(x,y,radius){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?0:radius;
    }

    Circle.prototype.distance=function(circle){
        if(circle!=null){
            var dx=this.x-circle.x;
            var dy=this.y-circle.y;
            return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
        }
    }

    Circle.prototype.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }
    
    function Particle(x,y,radius,life,speed,angle,color){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?1:radius;
        this.life=(life==null)?0:life;
        this.speed=(speed==null)?0:speed;
        this.angle=(angle==null)?0:angle;
        this.color=(color==null)?'#fff':color;
    }
    
    function ParticleSystem(){}

    ParticleSystem.prototype.move=function(deltaTime){
        for(var i=0,l=this.length;i<l;i++){
            this[i].life-=deltaTime;
            if(this[i].life<0){
                this.splice(i--,1);
                l--;
            }
            else{
                this[i].x+=Math.cos(this[i].angle)*this[i].speed*deltaTime;
                this[i].y+=Math.sin(this[i].angle)*this[i].speed*deltaTime;
            }
        }
    }

    ParticleSystem.prototype.fill=function(ctx){
        for(var i=0,l=this.length;i<l;i++){
            ctx.fillStyle=this[i].color;
            ctx.beginPath();
            ctx.arc(this[i].x,this[i].y,this[i].radius,0,Math.PI*2,true);
            ctx.fill();
        }
    }

    window.requestAnimationFrame=(function(){
        return window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame || 
            function(callback){window.setTimeout(callback,17);};
    })();
})();

10 comentarios:

  1. Muy bueno lo estuve esperando bastante, ya había dejado la programación y todo, pero ahora voy a retomarla, esperemos que esta vez sea para quedarme.
    Saludos, muy buen tuto!

    ResponderEliminar
    Respuestas
    1. ¡Esperemos así sea! ¡Mucha suerte! ¡Y muchas gracias!

      Eliminar
  2. sabes si también se puede heredar de la clase image?

    ResponderEliminar
    Respuestas
    1. Nunca lo he intentado. ¿Qué intentas hacer?

      Eliminar
    2. pues que la clase image lleve sus datos de ubicacion y valor alpa, esto sobre todo cuando tengo dos resoluciones que soportar.

      Eliminar
    3. function adPropiedad(cual){
      Object.defineProperty(cual, 'x', {
      value: 0,
      writable: true,
      configurable: true,
      enumerable: false
      });
      Object.defineProperty(cual, 'y', {
      value: 0,
      writable: true,
      configurable: true,
      enumerable: false
      });
      Object.defineProperty(cual, 'alpha', {
      value: 1.0,
      writable: true,
      configurable: true,
      enumerable: false
      });
      }

      ya lo logre, si a alguien le sirve

      var miImagen=new Image();


      miImagen.onload=function(){
      adPropiedad(this);
      console.log(this.x+" , "+this.y+" , "+this.alpha);
      //ahora puede asignar un valor a sus nuevas variables
      this.x=500;
      }

      miImagen.src="cualquiera.png";

      Eliminar
  3. Amigo como crees que puedo llevar los tutos de desarrollo de juegos desde el principio haciendo solo los ejercicios y leyendo o me recomiendas que tambien anote algo en los cuadernos?

    ResponderEliminar
    Respuestas
    1. Eso depende totalmente de tu forma de aprender. Personalmente nunca he tomado notas de programación, pero muchos amigos aprenden mejor haciendo notas de lo que van aprendiendo. Ve cual funciona mejor con tu estilo de aprendizaje. ¡Éxito!

      Eliminar
  4. Si no entendieron miren esto: http://www.domestika.org/es/projects/231743-magnetic-particles

    ResponderEliminar