Aprende a crear juegos en HTML5 Canvas

lunes, 30 de septiembre de 2013

Enemigos comunes: El disparador.

Uno de los seguidores del blog ha preguntado como crear un enemigo que dispare. Aprovechando esta duda, he decidido abrir esta nueva sección que cubra algunos de los enemigos más populares en los juegos de naves. Para mantenerlo simple, no los agregaré directo sobre el código anterior, si no que los pondré en un código individual, y tendrás tú que encargarte de integrarle con el resto de tu juego si deseas usar alguno de estos.

Hasta el momento hemos trabajado con el tipo de enemigo más básico, el kamikaze, cuya única función es estrellarse contra el jugador. Ahora, para empezar esta serie de enemigos populares, hablaremos del segundo tipo de enemigo popular: El disparador. Esa molesta nave que no es solo peligrosa por su presencia, si no que además lanza disparos en contra de nuestro personaje.

En realidad, veremos aquí no solo un tipo de enemigo, si no dos. ¿Por qué dos? Si lo analizas un momento, comprenderás que los disparos enemigos, son en realidad, un tercer tipo de enemigo. Comencemos agregando estos dibujos a nuestra hoja de sprites:


Si sigues usando la hoja de estilos de la lección pasada, los disparos enemigos han de comenzar en el pixel 75, y la nueva nave en el pixel 80.

Comencemos por decidir la forma en que actuará nuestro enemigo. Para hacerlo distinto a nuestro kamikaze, haremos que este aparezca únicamente en la parte superior de nuestra pantalla en intervalos al azar, se mueva de forma horizontal, y lance algunos disparos antes de desaparecer por el otro lado de la pantalla.

Para empezar, declararemos un nuevo contador, que nos indicará el tiempo antes de crear un nuevo enemigo:
    var eTimer=0;
Disminuiremos el contador en uno en el ciclo de tiempo, y cuando llegue a cero, crearemos una nueva disparadora en la parte superior izquierda, y pondremos un nuevo valor al azar a nuestro contador:
            // Generate Enemy
            eTimer--;
            if(eTimer<0){
                enemies.push(new Rectangle(0,40,10,10,1));
                eTimer=20+random(40);
            }
El nuevo valor indica que se generará una nueva nave dentro de 20 a 60 ciclos después, esto equivale de 1 a 3 segundos. En este momento, para muestra, está bien que se generen las naves en tan corto lapso de tiempo, pero cuando las incluyas en tu juego, es posible que quieras que esto ocurra en un lapso de tiempo más disperso.

Pasemos a programar la disparadora (enemigo al que asignamos el tipo 1). Tu ya sabes como hacer que se mueva, solo asigna esta vez los valores para que se desplace hacia la derecha:
            if(enemies[i].type==1){
                enemies[i].x+=5;
                // Shooter Outside Screen
                if(enemies[i].x>canvas.width){
                    enemies.splice(i--,1);
                    l--;
                    continue;
                }
Como la disparadora se encuentra en la parte superior siempre, y el jugador en la parte inferior, no tiene caso que comparemos si ambas naves entran en colisión. Pasemos a crear los disparos de la nave, el concepto es el mismo que la creación de la disparadora, y el intervalo de tiempo ha sido pensado para que dispare de 2 a 4 veces más o menos por aparición, sin que haga dos disparos muy juntos:
                    // Shooter Shots
                    enemies[i].timer--;
                    if(enemies[i].timer<0){
                        enemies.push(new Rectangle(enemies[i].x+3,enemies[i].y+5,5,5,2));
                        enemies[i].timer=10+random(30);
                    }
Por último, agrega la intersección entre las disparadoras y nuestros disparos. Querremos eliminar a la nave enemiga si da la casualidad que uno de nuestros disparos logra pegar en contra de esta:
                    // Shot Intersects Shooter
                    for(var j=0,ll=shots.length;j<ll;j++){
                        if(shots[j].intersects(enemies[i])){
                            score++;
                            shots.splice(j--,1);
                            ll--;
                            enemies.splice(i--,1);
                            l--;
                        }
                    }
Continuemos por programar los disparos enemigos (enemigo al que asignamos el tipo 2). Su función es bastante simple: se mueve hacia abajo, y si pega con el jugador, lo daña; nada más que eso. Es lógico que los disparos enemigos se mueva más rápido que las naves enemigas:
                // EnemyShot
                else if(enemies[i].type==2){
                    enemies[i].y+=10;
                    // EnemyShot Outside Screen
                    if(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;
                    }
                }
Por último, vamos a dibujarle. Incluyo el dibujado de la nave kamikaze para dejar claro como se dibujarían todos los elementos juntos:
        for(var i=0,l=enemies.length;i<l;i++){
            ctx.strokeStyle='#00f';
            if(enemies[i].type==0){
                if(enemies[i].timer%2==0)
                    enemies[i].drawImageArea(ctx,spritesheet,30,0,10,10);
                else{
                    ctx.strokeStyle='#fff';
                    enemies[i].drawImageArea(ctx,spritesheet,40,0,10,10);
                }
            }
            else if(enemies[i].type==1)
                enemies[i].drawImageArea(ctx,spritesheet,80+(aTimer%2)*10,0,10,10);
            else if(enemies[i].type==2)
                enemies[i].drawImageArea(ctx,spritesheet,75,(aTimer%2)*5,5,5);
        }
Probamos el código, y veremos la nave disparadora apareciendo continuamente y lanzando sus disparos en contra nuestra. Si lo has agregado directo en tu juego, podrás sentir la interacción de todos sus elementos. Con esto, tenemos ya nuestra enemiga nave disparadora.

¿Qué otros enemigos comunes les gustaría conocer en esta sección? Espero sus comentarios.

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

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        for(var i=0,l=enemies.length;i<l;i++){
            ctx.strokeStyle='#00f';
            if(enemies[i].type==0){
                if(enemies[i].timer%2==0)
                    enemies[i].drawImageArea(ctx,spritesheet,30,0,10,10);
                else{
                    ctx.strokeStyle='#fff';
                    enemies[i].drawImageArea(ctx,spritesheet,40,0,10,10);
                }
            }
            else if(enemies[i].type==1)
                enemies[i].drawImageArea(ctx,spritesheet,80+(~~(aTimer*10)%2)*10,0,10,10);
            else if(enemies[i].type==2)
                enemies[i].drawImageArea(ctx,spritesheet,75,(~~(aTimer*10)%2)*5,5,5);
        }
        
        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){
        this.x=(x==null)?0:x;
        this.y=(y==null)?0:y;
        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);};
    })();
})();

29 comentarios:

  1. Diferenciar el tipo de objetos en nuestro mundo mediante numeros puede llegar a ser un lío, en su lugar se podría usar un string que es algo un poco mas expresivo, por ejemplo..

    function Entity ( type, options ) {
    type = type ? type : "generic_type"
    this.x = options.x;
    // more properties goes here
    }
    Ejemplo de uso
    var player = new Entity( "player", { x: 10, y: 10, w:15, h: 15 } );
    var kamikase = new Entity( "kamikase", { x: 10, y: 10, w:15, h: 15 } );

    if ( kamikase.type === "kamikase" && player.isCollidedBy( kamikase ) ) {
    // magics goes here
    }

    ResponderBorrar
    Respuestas
    1. Usar nombres identificables para los humanos es sin duda una forma de mantener mejor control sobre el código. Sin embargo, es bien sabido que comparar contra strings es más costoso que comparar contra enteros, ya que el evaluador compara caracter por caracter detrás del código.

      En realidad esto rara vez afecta el rendimiento en juegos pequeños, pero si se desea optimizar más, una solución popular es utilizar constantes con valores numéricos para esta tarea:

      var KAMIKAZE = 0;
      var SHOOTER = 1;
      ...

      Borrar
    2. Que Gran idea y contundente respuesta, tus argumentos no tienen discucion.. ahora querido Karl quisiera preguntarte como haces para que tus videogames sean fluidos en moviles, resulta que estoy usando una tablet razonablemente buena y aun asi no se ve muy natural el ejercicio de esta pagina...(movimiento)

      Borrar
    3. ¿Es el mismo ejemplo directo de este sitio? ¿O un ejemplo que has adaptado aparte? Si es directo de este sitio, he de comentar que hay varios scripts extra que están corriendo en el sitio, que podrían quizá alentar el desempeño del juego.

      Aun así, me resulta algo extraño, ya que reviso el ejemplo desde un celular relativamente bueno, y el rendimiento se siente bastante natural...

      Borrar
    4. Es el ejemplo directo de este sitio, tampoco es que se vea como el gtaV corriendo en un radio pero bueno hay un pequeño lag, tal vez tengas razon con lo de los otros scripts, estare testeando, muchas gracias por tus respuestas...

      Borrar
    5. Para estar seguros, puedes hacer una versión aparte del código y comprobar si eso mejora su rendimiento.

      Puedes probar también el ejemplo de RequestAnimationFrame para revisar los cuadros por segundo que soporta tu tableta en una simulación simple. Eso podría darnos un mejor perfil de sus capacidades.

      Borrar
  2. Hola Karl, gracias por todos estos tutoriales, te quería hacer una consulta, he implementado este código al juego de naves, y la nave disparadora y los disparos no me aparecen, pero si están ahí porque me hacen daño, donde puede estar el fallo?

    ResponderBorrar
    Respuestas
    1. ¿De que tamaño son los gráficos de tus balas? Me ha ocurrido ver que a veces ponen un gráfico tan grande que este se dibuja fuera del área del juego y no tienen forma de detectar que este es el problema.

      Si este no es tu caso, necesitaría ver tu juego para analizar que otro problema puede ser. Quizá algún nombre incorrecto o una instrucción faltante por accidente.

      Borrar
    2. Muchas gracias por responder tan rápido, tengo el código como el último de tema2 naves, he implementado el código tal y como lo pones aqui.
      //Dibuja enemigos
      for(var i=0,l=enemigos.length;i<l;i++){
      ctx.strokeStyle='#00f';
      if(enemigos[i].tipo==0){
      if(enemigos[i].tiempo%2==0){
      //Imagen de nave enemiga.
      enemigos[i].drawImageArea(ctx,naves,30,0,10,10);
      }
      else{
      ctx.strokeStyle='#fff'; //Imagen de nave en blanco al recibir daño.
      enemigos[i].drawImageArea(ctx,naves,40,0,10,10);
      }
      }
      else if(enemigos[i].tipo==2) //Dos imágenes a 10 cuadros por segundo,con 10 pixeles.
      enemigos[i].drawImageArea(ctx,navee,80+(~~(anitemp*10)%2)*10,0,10,10);
      else if(enemigos[i].tipo==3)
      enemigos[i].drawImageArea(ctx,navee,75,(~~(anitemp*10)%2)*5,5,5);
      }

      Borrar
    3. // Generamos enemigo tipo disparador.
      etemp--;
      if(etemp<0){
      enemigos.push(new Rectangle(0,40,10,10,1));
      etemp=20+aleatorio(40);//Equivale a de 1 a 3 segundos.
      }
      //Movemos nave disparadora.
      for(var i=0,l=enemigos.length;i0)
      enemigos[i].tiempo--;
      //Disparo
      if(enemigos[i].tipo==2){ //Tipo 2 es la nave.
      enemigos[i].x+=5;
      //Disparo fuera de la pantalla.
      if(enemigos[i].x>canvas.width){
      enemigos.splice(i--,1);
      l--;
      continue;
      }
      //Disparando
      enemigos[i].tiempo--;
      if(enemigos[i].tiempo<0){
      enemigos.push(new Rectangle(enemigos[i].x+3,enemigos[i].y+5,5,5,2));
      enemigos[i].tiempo=10+aleatorio(30);
      }
      //Intersección entre nuestro disparos y nave disparadora.
      for(var j=0,ll=disparos.length;jcanvas.height){
      enemigos.splice(i--,1);
      l--;
      continue;
      }
      //Intersección jugador con disparos enemigos.
      if(jugador.intersects(enemigos[i])&&jugador.tiempo<1){
      jugador.vida--;
      jugador.tiempo=20;
      }
      }
      }

      Borrar
    4. ¿Cual es el tipo de tu nave disparadora y cual el tipo de sus disparos? Por el código de dibujo y los comentarios, parecen ser 2 y 3 respectivamente, pero al momento de crear un nuevo disparo, los creas con el tipo 2.

      Revisa si este es el problema, y si no, ¿Podría ver la imagen que estás usando?

      Borrar
    5. Gracias por responder¡¡ Utilizo la misma imagen que proporcionas por aquí.

      Borrar
    6. //Disparos enemigos.
      else if(enemigos[i].tipo==3){//Tipo 3 disparo.
      enemigos[i].y+=10;
      //Disparo enemigo fuera de la pantalla
      if(enemigos[i].y>canvas.height){
      enemigos.splice(i--,1);
      l--;
      continue;
      }
      //Intersección jugador con disparos enemigos.
      if(jugador.intersects(enemigos[i])&&jugador.tiempo<1){
      jugador.vida--;
      jugador.tiempo=20;
      }
      }
      }
      Esto también lo tengo puesto en el código

      Borrar
    7. El tipo 2 es el disparador, estaba mal el comentario, y el tipo 3 el disparo.

      Borrar
    8. En esta parte:

      //Disparando
      enemigos[i].tiempo--;
      if(enemigos[i].tiempo<0){
      enemigos.push(new Rectangle(enemigos[i].x+3,enemigos[i].y+5,5,5,**2**));
      enemigos[i].tiempo=10+aleatorio(30);
      }

      ¿El número entre asteriscos no debería ser 3?

      Borrar
    9. // Generamos enemigo tipo disparador.
      etemp--;
      if(etemp<0){
      enemigos.push(new Rectangle(0,40,10,10,1));
      etemp=20+aleatorio(40);//Equivale a de 1 a 3 segundos.
      }
      //Movemos nave disparadora.
      for(var i=0,l=enemigos.length;i0)
      enemigos[i].tiempo--;
      //Disparador
      if(enemigos[i].tipo==2){ //Tipo 2 es la nave.
      enemigos[i].x+=5;
      //Disparador fuera de la pantalla.
      if(enemigos[i].x>canvas.width){
      enemigos.splice(i--,1);
      l--;
      continue;
      }
      //Disparador nuevo
      enemigos[i].tiempo--;
      if(enemigos[i].tiempo<0){
      enemigos.push(new Rectangle(enemigos[i].x+3,enemigos[i].y+5,5,5,2));
      enemigos[i].tiempo=10+aleatorio(30);
      }
      //Intersección entre nuestro disparos y nave disparadora.
      for(var j=0,ll=disparos.length;jcanvas.height){
      enemigos.splice(i--,1);
      l--;
      continue;
      }
      //Intersección jugador con disparos enemigos.
      if(jugador.intersects(enemigos[i])&&jugador.tiempo<1){
      jugador.vida--;
      jugador.tiempo=20;
      }
      }
      }
      He corregido comentarios que estaban mal, he cambiado eso y sigue igual, me hace daño pero no se ve la imagen...

      Borrar
    10. Me refiero que he cambiado el dos por el tres y sigue igual

      Borrar
    11. Todo parece estar bien. No creo que pueda hacer más sin depurar directamente tu juego con todos sus componentes.

      Borrar
    12. Si puedes hospedar las imágenes en algún sitio, podrías usar JSFiddle, aunque seguro sería más fácil algún enlace a un ZIP con tu proyecto, como Dropbox.

      Borrar
    13. https://www.dropbox.com/sh/8ci0bph2qzfoxco/AADO764tDnaW0w_FCqfcGVzKa?dl=0

      Perdón por las molestias y gracias por mirar mi código, aquí te dejo el enlace¡¡

      Borrar
    14. Ya vi en donde se dio la confusión.

      En la lección, indiqué que las imágenes debían ser anexadas al final de la imagen original, sin embargo, tú creaste una nueva variable para incluir estas imágenes nuevas por aparte. Al dibujarlo, estás buscando las imágenes en la posición 75 y 80 para disparos y el nuevo enemigo, pero la imagen solo mide 25px de ancho. Si deseas mantener las imágenes aparte, únicamente debes cambiar la posición a 0 y 5 respectivamente, y deberán verse sin problemas.

      Borrar
    15. Encontré un error más. Los tipos de tus enemigos son 0 (kamikazes), 1 (Disparadores) y 2 (Munición enemiga), pero estás intentando dibujar 0, 2 y 3. Cambia el 2 por 1 y el 3 por 2, eso corregirá las naves fantasmas actuales.

      Borrar
    16. Este comentario ha sido eliminado por el autor.

      Borrar
    17. Muchas gracias,ya sale¡¡ Lo cambié para hacer pruebas, a ver donde estaba el error..y la lié,no sé xq la nave disparadora me sale en paralelo..

      Borrar