Aprende a crear juegos en HTML5 Canvas

lunes, 7 de octubre de 2013

Enemigos comunes: El disparador de 8 lados.

Continuamos con nuestra serie de enemigos comunes en los juegos de naves. Hoy traigo uno de los más horribles, y odiosos enemigos comunes, pues no hay sitio donde puedas esconderte de sus ataques... Ya que ataca hacia todos lados.

El disparador de 8 lados, lanza un disparo hacia cada lado y hacia cada diagonal, cubriendo un rango de 360 grados. Si planeas incluir uno de estos en tu juego, recomiendo que permitas al jugador moverse libre por la pantalla para evitar esta clase de ataques, o en dado caso, eliminar los disparos laterales del disparador de 8 lados.

Usaré las imágenes a continuación para este enemigo:


Para permitir crear un solo tipo de disparo que se mueva en distintas direcciones debemos agregar dos nuevas variables a nuestro rectángulo:
    function Rectangle(x,y,width,height,type,health,vx,vy){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.vx=(vx==null)?0:vx;
        this.vy=(vy==null)?0:vy;
        this.width=(width==null)?0:width;
        this.height=(height==null)?this.width:height;
        this.type=(type==null)?1:type;
        this.health=(health==null)?1:health;
        this.timer=0;
Estas variables marcan las velocidades en las coordenadas X y Y del objeto. Con ello, podemos indicar distintos tipos de movimientos a objetos del mismo tipo, en lugar de forzar a todos a realizar el mismo movimiento.

Para que esto sea efectivo, cambiaremos el movimiento de los disparos enemigos al siguiente:
                // EnemyShot
                else if(enemies[i].type==2){
                    enemies[i].x+=enemies[i].vx;
                    enemies[i].y+=enemies[i].vy;
                    // EnemyShot Outside Screen
                    if(enemies[i].x<0||enemies[i].x>canvas.width||enemies[i].y<0||enemies[i].y>canvas.height){
                        enemies.splice(i--,1);
                        l--;
                        continue;
                    }
Nota que además, debemos comprobar para su eliminación los cuatro lados y no solo el inferior, ya que no sabemos por que lugar saldrá nuestro disparo enemigo. Prosigamos ahora a programar nuestro disparador de 8 lados. Usaré el mismo código de generación que en el ejemplo pasado, modificando ligeramente sus valores para ajustarle a esta clase de enemigo:
            // Generate Enemy
            eTimer--;
            if(eTimer<0){
                enemies.push(new Rectangle(random(15)*10,0,10,10,3));
                eTimer=40+random(40);
            }
Posteriormente, creamos el código para mover el disparador de 8 lados (enemigo al que asignamos el tipo 3). Su movimiento será el mismo de las nave tipo kamikaze:
                // 8Shooter
                if(enemies[i].type==3){
                    enemies[i].y+=5;
                    // 8Shooter Outside Screen
                    if(enemies[i].y>canvas.height){
                        enemies.splice(i--,1);
                        l--;
                        continue;
                    }
Lo que hará especial a este enemigo, será la forma en que genera los disparos, creando uno para cada lado y cada diagonal cada vez que dispare:
                    // 8Shooter Shots
                    enemies[i].timer--;
                    if(enemies[i].timer<0){
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,0,10));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-7,7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-10,0));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-7,-7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,0,-10));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,7,-7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,10,0));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,7,7));
                        enemies[i].timer=30+random(30);
                    }
Nota que la velocidad en las diagonales es del 70% que de los laterales. Esto es por que el movimiento diagonal se compone de dos ejes en lugar de uno, por tanto, su desplazamiento final es mayor. Aunque este 70% no garantiza un desplazamiento exacto de 10 pixeles a la diagonal, la aproximación es similar y crea el efecto de desplazarse a una velocidad similar a la de los lados.

Por último, agregamos sus colisiones con los disparos y con el jugador, como hemos hecho con los demás enemigos:
                    // Player Intersects 8Shooter
                    if(player.intersects(enemies[i])&&player.timer<1){
                        player.health--;
                        player.timer=20;
                    }
                    
                    // Shot Intersects 8Shooter
                    for(var j=0,ll=shots.length;j<ll;j++){
                        if(shots[j].intersects(enemies[i])){
                            score++;
                            enemies[i].health--;
                            if(enemies[i].health<1){
                                enemies.splice(i--,1);
                                l--;
                            }
                            else
                                enemies[i].timer=1;
                            shots.splice(j--,1);
                            ll--;
                        }
                    }
Con esto, nuestro disparador de ocho lados, está preparado. No olvides dibujarle de la siguiente forma:
            else if(enemies[i].type==3){
                if(enemies[i].timer==1)
                    enemies[i].drawImageArea(ctx,spritesheet,120,0,10,10);
                else
                    enemies[i].drawImageArea(ctx,spritesheet,100+(aTimer%2)*10,0,10,10);
            }
Probemos el código, y veremos nuestro disparador de 8 lados, haciendo su trabajo... Como lo odiaremos todos cuando lo implementen en su juego...

Si ya lo has implementado, notarás que cuando le disparas, este rápidamente contra-ataca al recibir daño. Esto es por que estamos usando la misma variable para el temporizador de disparos, y el temporizador que demuestra que ha recibido daño. Lo he dejado así por que me parece una agradable y desafiante mecánica natural de esta malvada nave enemiga, pero si deseas que su comportamiento sea distinto, tendrás que encargarte de ello.

Ahora que hemos agregado movimiento independiente a los objetos de un mismo tipo, podemos usar este para cualquier elemento que se halla creado. Por ejemplo, al obtener la mejora de triple disparo, podemos hacer que los disparos sean dispersos en lugar de frontales, como lo hemos hecho hasta ahora. Eso lo haríamos modificando la línea de tripe disparo a la siguiente forma:
                if(multishot==3){
                    shots.push(new Rectangle(player.x-3,player.y+2,5,5,0,0,-3,-10));
                    shots.push(new Rectangle(player.x+3,player.y,5,5,0,0,0,-10));
                    shots.push(new Rectangle(player.x+9,player.y+2,5,5,0,0,3,-10));
                }
Notarás que el disparo central se mueve solo en el eje Y, pero los disparos laterales tienen una pequeña desviación en el eje X de 3 y -3 respectivamente; esto es lo que permite que esta mejora lance disparos dispersos. Este tipo de ataque será también muy efectivo para poder eliminar los disparadores de 8 lados.

Para que estos disparos tengan efecto, modifica el movimiento de los disparos para que se muevan también con respecto a sus valores en vx y vy. No olvides agregar el movimiento necesario cuando se lanzan solo 1 y 2 disparos también, o estos quedarán congelados en su posición.

Con esto concluimos esta entrega, teniendo un nuevo tipo de enemigo, y una nueva forma de aprovechar la máxima mejora de nuestro disparos.

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var pause;
    var aTimer=0;
    var eTimer=0;
    var player=new Rectangle(90,280,10,10,0,3);
    var shots=[];
    var enemies=[];
    var spritesheet=new Image();
    spritesheet.src='assets/spritesheet-nbg.png';

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

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

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

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

    function act(deltaTime){
        if(!pause){
            // Generate Enemy
            eTimer--;
            if(eTimer<0){
                enemies.push(new Rectangle(random(15)*10,0,10,10,3));
                eTimer=40+random(40);
            }
            
            // Move Enemies
            for(var i=0,l=enemies.length;i<l;i++){
                if(enemies[i].timer>0)
                    enemies[i].timer--;
                
                // EnemyShot
                else if(enemies[i].type==2){
                    enemies[i].x+=enemies[i].vx;
                    enemies[i].y+=enemies[i].vy;
                    // EnemyShot Outside Screen
                    if(enemies[i].x<0||enemies[i].x>canvas.width||enemies[i].y<0||enemies[i].y>canvas.height){
                        enemies.splice(i--,1);
                        l--;
                        continue;
                    }
                    
                    // Player Intersects EnemyShot
                    if(player.intersects(enemies[i])&&player.timer<1){
                        player.health--;
                        player.timer=20;
                    }
                }
                // 8Shooter
                if(enemies[i].type==3){
                    enemies[i].y+=5;
                    // 8Shooter Outside Screen
                    if(enemies[i].y>canvas.height){
                        enemies.splice(i--,1);
                        l--;
                        continue;
                    }
                    
                    // 8Shooter Shots
                    enemies[i].timer--;
                    if(enemies[i].timer<0){
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,0,10));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-7,7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-10,0));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,-7,-7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,0,-10));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,7,-7));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,10,0));
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2,0,7,7));
                        enemies[i].timer=30+random(30);
                    }
                    
                    // Player Intersects 8Shooter
                    if(player.intersects(enemies[i])&&player.timer<1){
                        player.health--;
                        player.timer=20;
                    }
                    
                    // Shot Intersects 8Shooter
                    for(var j=0,ll=shots.length;j<ll;j++){
                        if(shots[j].intersects(enemies[i])){
                            score++;
                            enemies[i].health--;
                            if(enemies[i].health<1){
                                enemies.splice(i--,1);
                                l--;
                            }
                            else
                                enemies[i].timer=1;
                            shots.splice(j--,1);
                            ll--;
                        }
                    }
                }
            }
            
            // Timer
            aTimer+=deltaTime;
            if(aTimer>3600)
                aTimer-=3600;
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#00f';
        for(var i=0,l=enemies.length;i<l;i++){
            if(enemies[i].type==2)
                enemies[i].drawImageArea(ctx,spritesheet,75,(~~(aTimer*10)%2)*5,5,5);
            else if(enemies[i].type==3){
                if(enemies[i].timer==1)
                    enemies[i].drawImageArea(ctx,spritesheet,120,0,10,10);
                else
                    enemies[i].drawImageArea(ctx,spritesheet,100+(~~(aTimer*10)%2)*10,0,10,10);
            }
        }
        
        ctx.fillStyle='#fff';
        ctx.fillText('Score: 0',0,20);
        ctx.fillText('Health: ?',150,20);
        if(pause){
            ctx.textAlign='center';
            ctx.fillText('PAUSE',100,150);
            ctx.textAlign='left';
        }
    }

    function Rectangle(x,y,width,height,type,health,vx,vy){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        this.vx=(vx==null)?0:vx;
        this.vy=(vy==null)?0:vy;
        this.width=(width==null)?0:width;
        this.height=(height==null)?this.width:height;
        this.type=(type==null)?1:type;
        this.health=(health==null)?1:health;
        this.timer=0;
    }

    Rectangle.prototype.intersects=function(rect){
        if(rect!=null){
            return(this.x<rect.x+rect.width&&
                this.x+this.width>rect.x&&
                this.y<rect.y+rect.height&&
                this.y+this.height>rect.y);
        }
    }
    
    Rectangle.prototype.fill=function(ctx){
        ctx.fillRect(this.x,this.y,this.width,this.height);
    }

    Rectangle.prototype.drawImageArea=function(ctx,img,sx,sy,sw,sh){
        if(img.width)
            ctx.drawImage(img,sx,sy,sw,sh,this.x,this.y,this.width,this.height);
        else
            ctx.strokeRect(this.x,this.y,this.width,this.height);
    }

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

3 comentarios:

  1. Qué pasada!! Muchas gracias por este Blog!!

    ResponderBorrar
  2. hola me han gustado mucho tus tutoriales y he creado un juego con mis propios sprites he creado mas enemigos a uno de ellos le llame dragon y por su puesto dibuje un dragon en paint todo lo hice con base en tus tutoriales no hay problema verdad? explicame por favor

    ResponderBorrar
    Respuestas
    1. Seguro, estos son sólo códigos base y claro que tu juego compartirá mucho e mi código que te he enseñado con el tuyo, pero también necesitarás ajustes para definir tu propio juego con tus reglas,y es ahí donde cada juego se vuelve único.

      ¡Mucha suerte en tu juego!

      Borrar