Aprende a crear juegos en HTML5 Canvas

domingo, 5 de enero de 2014

Explosión y regeneración

La semana pasada dejamos listos los asteroides, y solo quedó pendiente el impacto entre estos y nuestro jugador. He querido dejarlo para un tema aparte, pues deseaba probar algo diferente para esta ocasión.

A diferencia de nuestro anterior juego de naves donde al ser impactado el jugador, este pierde parte de su vitalidad y obtiene inmunidad por unos instantes, los juegos de asteroides se caracterizan por hacer explotar al jugador al impactar contra él, antes de regenerarlo en el centro con su respectiva breve inmunidad. Este mismo efecto es el que aprenderemos a hacer ahora.

Comenzamos declarando el arreglo de las explosiones. Para dibujarles, usaremos los círculos dorados en la hoja de sprites que hasta ahora habíamos dejado sin usar, de la siguiente forma:
        ctx.strokeStyle='#ff0';
        for(var i=0,l=explosion.length;i<l;i++)
            explosion[i].drawImageArea(ctx,spritesheet, 35,(aTimer%2)*5,5,5);
Al colisionar un asteroide con el jugador restaremos una de sus vidas y le damos inmunidad de 60 ciclos, esto es, tres segundos. Posteriormente creamos la explosión con 8 círculos, uno cada 45 grados y un temporizador de 40 ciclos (2 segundos). Esto nos da un segundo extra para posicionar de nuevo el jugador en el centro y prepararlo para empezar de nuevo:
                // Collision Enemy-Player
                if(player.timer<1&&enemies[i].distance(player)<0){
                    lives--;
                    player.timer=60;
                    for(var j=0;j<8;j++){
                        var e=new Circle(player.x,player.y,2.5);
                        e.rotation=45*j;
                        e.timer=40;
                        explosion.push(e);
                    }
                }
Mover las explosiones ya no deben representar reto alguno a estos niveles:
            // Move Explosion
            for(var i=0,l=explosion.length;i<l;i++){
                explosion[i].move((explosion[i].rotation-90)*Math.PI/180,1);
                explosion[i].timer--;
                if(explosion[i].timer<1){
                    explosion.splice(i--,1);
                    l--;
                }
            }
Como mencioné antes, una vez terminada la explosión, 20 ciclos antes de terminar la inmunidad del jugador, es tiempo de regresarlo a la posición de inicio. No solo es cambiar su posición, si no también regresar su rotación y velocidad a 0. Esto lo haremos de forma más fácil en una función que llamaremos "playerReset":
            // Damaged
            if(player.timer>0){
                player.timer--;
                if(player.timer==20){
                    playerReset();
                }
            }
Y la función ha de quedar de esta forma:
    function playerReset(){
        player.x=canvas.width/2;
        player.y=canvas.height/2;
        player.rotation=0;
        player.speed=0;
    }
Podemos usar esta misma función dentro de la función "reset" para no tener que repetir los mismos comandos dos veces. No olvides también agregar en esta función la limpieza del arreglo de la explosión.

Con esto la explosión ha quedado lista. Tan solo queda hacer desaparecer la nave mientras la explosión se efectúa. Este truco visual puede ser fácilmente simulado junto al dibujado intermitente de la nave durante la inmunidad; solo agregamos también que se dibuje si el temporizador es menor que los ciclos donde no debería aparecer (más de 20):
        if(player.timer<21&&player.timer%2==0){
            ctx.strokeStyle='#0f0';
            if(pressing[KEY_UP])
                player.drawImageArea(ctx,spritesheet, (aTimer%3)*10,0,10,10);
            else
                player.drawImageArea(ctx,spritesheet, 0,0,10,10);
        }
¡Con esto el juego ha quedado terminado! Podemos comprobar como las explosiones y regeneración le dan un toque mas propio a este juego.

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var KEY_ENTER=13;
    var KEY_SPACE=32;
    var KEY_LEFT=37;
    var KEY_UP=38;
    var KEY_RIGHT=39;
    var KEY_DOWN=40;
    
    var canvas=null,ctx=null;
    var lastPress=null;
    var pressing=[];
    var pause=true;
    var score=0;
    var lives=0;
    var aTimer=0;
    var player=new Circle(150,75,5);
    var shots=[];
    var enemies=[];
    var explosion=[];
    var spritesheet=new Image();
    var background=new Image();
    spritesheet.src='assets/asteroids.png';
    background.src='assets/nebula2.jpg';

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

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

    function run(){
        setTimeout(run,50);
        act(0.05);
    }

    function repaint(){
        requestAnimationFrame(repaint);
        paint(ctx);
    }

    function playerReset(){
        player.x=canvas.width/2;
        player.y=canvas.height/2;
        player.rotation=0;
        player.speed=0;
    }

    function reset(){
        playerReset();
        player.timer=0;
        shots.length=0;
        enemies.length=0;
        explosion.length=0;
        score=0;
        lives=3;
    }

    function act(deltaTime){
        if(!pause){
            // GameOver Reset
            if(lives<1)
                reset();
            
            // Set Rotation
            if(pressing[KEY_RIGHT]){
                player.rotation+=10;
            }
            if(pressing[KEY_LEFT]){
                player.rotation-=10;
            }
            // Set Acceleration
            if(pressing[KEY_UP]){
                if(player.speed<5)
                    player.speed++;
            }
            if(pressing[KEY_DOWN]){
                if(player.speed>-5)
                    player.speed--;
            }
            
            // Move Player
            player.move((player.rotation-90)*Math.PI/180,player.speed);
            
            // New Shot
            if(lastPress==KEY_SPACE&&player.timer<21){
                var s=new Circle(player.x,player.y,2.5);
                s.rotation=player.rotation;
                s.speed=player.speed+10;
                s.timer=15;
                shots.push(s);
            }
            
            // Move Shots
            for(var i=0,l=shots.length;i<l;i++){
                shots[i].timer--;
                if(shots[i].timer<0){
                    shots.splice(i--,1);
                    l--;
                    continue;
                }
                
                shots[i].move((shots[i].rotation-90)*Math.PI/180,shots[i].speed);
            }
            
            // New Enemies
            if(enemies.length<1){
                for(var i=0;i<3;i++){
                    var e=new Circle(-20,-20,20);
                    e.rotation=random(360);
                    enemies.push(e);
                }
            }
            
            // Move Enemies
            for(var i=0,l=enemies.length;i<l;i++){
                enemies[i].move((enemies[i].rotation-90)*Math.PI/180,2);
                
                // Collision Enemy-Player
                if(player.timer<1&&enemies[i].distance(player)<0){
                    lives--;
                    player.timer=60;
                    for(var j=0;j<8;j++){
                        var e=new Circle(player.x,player.y,2.5);
                        e.rotation=45*j;
                        e.timer=40;
                        explosion.push(e);
                    }
                }
                
                // Collision Enemy-Shot
                for(var j=0,ll=shots.length;j<ll;j++){
                    if(enemies[i].distance(shots[j])<0){
                        if(enemies[i].radius>5){
                            for(var k=0;k<3;k++){
                                var e=new Circle(enemies[i].x,enemies[i].y,enemies[i].radius/2);
                                e.rotation=shots[j].rotation+120*k;
                                enemies.push(e);
                            }
                        }
                        score++;
                        enemies.splice(i--,1);
                        l--;
                        shots.splice(j--,1);
                        ll--;
                    }
                }
            }
            
            // Move Explosion
            for(var i=0,l=explosion.length;i<l;i++){
                explosion[i].move((explosion[i].rotation-90)*Math.PI/180,1);
                explosion[i].timer--;
                if(explosion[i].timer<1){
                    explosion.splice(i--,1);
                    l--;
                }
            }
            
            // Damaged
            if(player.timer>0){
                player.timer--;
                if(player.timer==20){
                    playerReset();
                }
            }
            
            // GameOver
            if(lives<1){
                pause=true;
            }
            
            // Animation Cicle
            aTimer+=deltaTime;
            if(aTimer>3600)
                aTimer-=3600;
        }
        if(lastPress==KEY_ENTER)
            pause=!pause;
        
        lastPress=null;
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        if(background.width)
            ctx.drawImage(background,0,0);
        else
            ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#00f';
        for(var i=0,l=enemies.length;i<l;i++)
            enemies[i].drawImageArea(ctx,spritesheet, 0,10,40,40);
        
        ctx.strokeStyle='#f00';
        for(var i=0,l=shots.length;i<l;i++)
            shots[i].drawImageArea(ctx,spritesheet, 30,(~~(aTimer*10)%2)*5,5,5);
        
        if(player.timer<21&&player.timer%2==0){
            ctx.strokeStyle='#0f0';
            if(pressing[KEY_UP])
                player.drawImageArea(ctx,spritesheet, (~~(aTimer*10)%3)*10,0,10,10);
            else
                player.drawImageArea(ctx,spritesheet, 0,0,10,10);
        }
        
        ctx.strokeStyle='#ff0';
        for(var i=0,l=explosion.length;i<l;i++)
            explosion[i].drawImageArea(ctx,spritesheet, 35,(~~(aTimer*10)%2)*5,5,5);
        
        ctx.fillStyle='#fff';
        if(spritesheet.width)
            for(var i=0;i<lives;i++)
                ctx.drawImage(spritesheet, 0,0,10,10, canvas.width-20-20*i,10,10,10);
        else
            ctx.fillText('Lives: '+lives,canvas.width-50,20);
        
        //ctx.fillText('Rotation: '+player.rotation,0,20);
        ctx.fillText('Score: '+score,0,20);
        
        if(pause){
            ctx.textAlign='center';
            if(lives<1)
                ctx.fillText('GAME OVER',canvas.width/2,canvas.height/2);
            else
                ctx.fillText('PAUSE',canvas.width/2,canvas.height/2);
            ctx.textAlign='left';
        }
    }

    document.addEventListener('keydown',function(evt){
        lastPress=evt.keyCode;
        pressing[evt.keyCode]=true;
    },false);

    document.addEventListener('keyup',function(evt){
        pressing[evt.keyCode]=false;
    },false);

    function Circle(x,y,radius){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.radius=(radius==null)?0:radius;
        //this.scale=1;
        this.rotation=0;
        this.speed=0;
        this.timer=0;
    }
        
    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.move=function(angle,speed){
        if(speed!=null){
            this.x+=Math.cos(angle)*speed;
            this.y+=Math.sin(angle)*speed;

            // Out Screen
            if(this.x>canvas.width)
                this.x=0;
            if(this.x<0)
                this.x=canvas.width;
            if(this.y>canvas.height)
                this.y=0;
            if(this.y<0)
                this.y=canvas.height;
        }
    }

    Circle.prototype.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }

    Circle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
        if(img.width){
            ctx.save();
            ctx.translate(this.x,this.y);
            //ctx.scale(this.scale,this.scale);
            ctx.rotate(this.rotation*Math.PI/180);
            ctx.drawImage(img,sx,sy,sw,sh,-this.radius,-this.radius,this.radius*2,this.radius*2);
            ctx.restore();
        }
        else
            this.stroke(ctx);
    }

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

13 comentarios:

  1. MUY BUENOS TUS TUTORIALES, PERO NO SERIA MEJOR USAR EN LA FUNCION CIRCLE , CIRCLE.PROTOTYPE.DISTANCE , CIRCLE.PROTOTYPE.MOVE, ETC.... Y TRATAR DE NO USAR TANTAS TRANFORMACIONES DEL CANVAS DIGO CTX,TRANSLATE,, Y ASI...

    ResponderBorrar
    Respuestas
    1. Usar prototype es una forma alternativa para na creación de funciones, pero realmente no tienen impacto mayor sobre el script final. Con respecto a las transformaciones, son usadas sólo las necesarias para rotar las imágenes. ¿Conoces alguna forma alterna que use menos transformaciones?

      Borrar
    2. EL CREAR UN NUEVO OBJETO EN JS SIN EL PROTOTYPE ES HACER UN COPIA EN MEMORIA Y CLARO QUE EN UN PROJECTO MAS GRANDE ESO PASA FACTURA.. PARA ROTAR SE PUEDE HACE UNA FUNCION ROTAR
      function rotar(punto, centro, angulo){
      angulo = (angulo ) * (Math.PI/180); // A RADIANES
      var rotarX = Math.cos(angulo) * (punto.x - centro.x) - Math.sin(angulo) * (punto.y-centro.y) + centro.x;
      var rotarY = Math.sin(angulo) * (punto.x - centro.x) + Math.cos(angulo) * (punto.y - centro.y) + centro.y;
      return {x:rotarX,y:rotarY}
      }
      esta seria para rotar respecto a un eje pero modificadonla sirve para hacer girar libremente.

      Borrar
    3. La información sobre prototype parece un poco escondida, pero he logrado constatar la razón en tus palabras, y como mencionas, en proyectos más grandes sí puede marcar la diferencia, y en ejemplos como el presente que no usan variables privadas, no presenta conflictos, aunque podría ser confuso para programadores con mayor experiencia en otros lenguajes. Veré la forma de compensar este conocimiento para su mejor enseñanza. Muchas gracias por la información.

      Ahora, con respecto a la rotación ¿Tienes un ejemplo práctico de esta función de rotación aplicada a Canvas? Que no he podido encontrar información al respecto. Muchas gracias.

      Borrar
  2. JA NO PASA NADA YO AUN ESTOY APRENDIENDO Y PERO CREO QUE YA LEIDO BASTANTE JEJEJ ESTARE MUY SEGUIDO POR EL BLOG PARA SEGUIR APERNDIENO :) , Y SI INVESTIGA PROTOTIPOS ES UNO DE LOS VERDADEROS POTENCIALES DE JS, EH ESA FUNCION SERIA ASI
    CREAR DOS CIRCULO NORMALMENTE Y LOS DEMAS ASI

    function ROTAR (punto, centro, angulo){
    var rotarX = Math.cos(angulo) * (punto.x - centro.x) - Math.sin(angulo) * (punto.y-centro.y) + centro.x;
    var rotarY = Math.sin(angulo) * (punto.x - centro.x) + Math.cos(angulo) * (punto.y - centro.y) + centro.y;
    return {x:rotarX,y:rotarY}
    }

    FUNCION UPDATE(DELTA) {
    VAR ROTACION = 90 * (MATH.PI/ 180) + DELTA;//USAS DELTA COMO QUIERAS
    VAR LOCAL;
    LOCAL = ROTAR(CIRCULO1,CIRCULO2,ROTACION)
    CIRCULO1.X= LOCAL.X
    CIRCULO1.Y=LOCAL.Y
    }
    SI NO TE SIRVE ME MUESTRAS EL CODIGO QUE TIENES

    ResponderBorrar
    Respuestas
    1. El código para calcular la rotación no parece tener conflicto alguno, el detalle es dibujar con esa información en el lienzo. ¿Tú sabes como hacerlo?

      Borrar
  3. COMO DIBUJARLO? SI YA LO TENGO HECHO ES UN PROTOTIPO DE UN JUEGO QUE PIENSO HACER... PERO NO TIENES NADA HECHO?

    ResponderBorrar
    Respuestas
    1. ¿Dibujar una imagen rotada con la formula que me das en lugar de transformaciones? Me temo que no he encontrado la forma de hacerlo. ¿Como es que lo haces tú?

      Borrar
  4. Hola no te des mala vida segun lo que leo lo que te quiere decir es que esa funcion es para rotar pero no precisamente imagenes, eh y si hay que procurar no hacer tantas transformacion en el canvas en lo que sea posible pero tu ejemplo va bien. Y si no has leido sobre prototipos creo que te falta la mitad de js. muy buena pagina.

    ResponderBorrar
    Respuestas
    1. Noto que calcula información de rotación, pero no veo que se pueda dibujar con ello. Esperaba a ver si quizá el tuviera informacion de una forma alterna, ya que yo solo conozco usar transformaciones para ello.

      Aun cuando llevo mucho tiempo en desarrollo de juegos, aun me falta descubrir muchos de los secretos de JavaScript, que poco a poco recibo y transmito a los demas. Gracias por todo :)

      Borrar
  5. Con esa funciones para transformaciones creo que son suficiente el problema de ctx.rotate es la dependencia con el ctx.translate, eh una pregunta no tienes twitter personal, ya que desarrolladores html5 hispanos pocos, la mayoria que sigo y conozco ya sabes otro idioma.
    sobre js podrias empezar por aqui si aun no lo has visto
    http://blog.amatiasq.com/2012/01/javascript-conceptos-basicos-herencia-por-prototipos

    ResponderBorrar
    Respuestas
    1. Asi es. Se requieren muchas transformaciones, pero parece ser la única forma de hacer dicho dibujado. Con respecto al Twitter, solo tengo este creado especialmente para el blog.

      De prototype ya tengo experiencia, aunque veo que este ejemplo usa una forma distinta para crear sus funciones. Parece que tendré que averiguar aun mas sobre el mejor metodo para hacer uso de ello...

      Borrar
  6. pero si estas pendiente a esa cuenta la voy a seguir...
    eh si las tranformaciones son un problema pero ahi es donde entra la optimizacion y eso.. la manera en que estoy programando ahora es de esta forma espero te sirva http://codeincomplete.com/posts/2013/10/27/rotating_tower_platformer/
    un ejemplo brutal... pero para desarrollo final estoy usando el framework Phaser js.

    ResponderBorrar