Aprende a crear juegos en HTML5 Canvas

domingo, 29 de diciembre de 2013

Asteroides fragmentables

Seguramente con la hoja de sprites en la ocasión pasada, el gran asteroide en la imagen te dio una pista del juego que estamos haciendo. Y si aun no estás seguro, te lo confirmo de una vez: Estamos creando un juego de tipo "asteroides".

Una de las características que identifican a este juego, es que al explotar cada uno de los gigantescos asteroides, estos se fragmentan en asteroides más pequeños, hasta que los menores son finalmente destruidos. En esta ocasión, aprenderemos a hacer dicho efecto.

Comenzaremos declarando el arreglo que contendrá los asteroides:
    var enemies=[];
Para asegurar que siempre tengamos asteroides a los cuales disparar durante la prueba, tres nuevos serán agregados cada vez que el arreglo esté vacío, fuera de pantalla, en alguna dirección al azar:
        // 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);
            }
        }
Mover los asteroides no representan reto alguno para nosotros. Tampoco el hacer que al colisionar el asteroide con uno de nuestros disparos, el puntuaje se sume y ambos sean removidos de sus respectivos arreglos:
        // Move Enemies
        for(var i=0,l=enemies.length;i<l;i++){
            enemies[i].move((enemies[i].rotation-90)*Math.PI/180,2);
         
            for(var j=0,ll=shots.length;j<ll;j++){
                if(enemies[i].distance(shots[j])<0){
                    score++;
                    enemies.splice(i--,1);
                    l--;
                    shots.splice(j--,1);
                    ll--;
                }
            }
        }
La novedad en este código, es crear los nuevos fragmentos del asteroide al destruir uno grande. Para ello, antes de sumar el nuevo puntuaje, si el radio del asteroide era mayor a 5, crearemos tres nuevos asteroides de la mitad de radio del asteroide original. Estos nuevos asteroides tendrán la rotación del disparo mas 120 grados al anterior; de esta forma, los nuevos asteroides se expandirán de forma triangular al asteroide original, en dirección opuesta al origen de su explosión:
                    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);
                        }
                    }
Con esto tendremos el efecto deseado al destruir los asteroides grandes. Por último, solo queda dibujarlos:
        ctx.strokeStyle='#00f';
        for(var i=0,l=enemies.length;i<l;i++)
            enemies[i].drawImageArea(ctx,spritesheet, 0,10,40,40);
Dado que drawImageArea dibuja las imágenes de acuerdo al radio del círculo, no es necesario ningún código extra para dibujar los asteroides en los distintos tamaños.

Código final:


[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    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 score=0;
    var aTimer=0;
    var player=new Circle(150,100,5);
    var shots=[];
    var enemies=[];
    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 act(deltaTime){
        // 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){
            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);
            
            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--;
                }
            }
        }
        
        aTimer+=deltaTime;
        if(aTimer>3600)
            aTimer-=3600;
        
        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);
        
        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.fillStyle='#fff';
        //ctx.fillText('Rotation: '+player.rotation,0,20);
        ctx.fillText('Score: '+score,0,20);
    }

    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);};
    })(); 
})();

No hay comentarios.:

Publicar un comentario