Aprende a crear juegos en HTML5 Canvas

lunes, 16 de abril de 2012

Municiones Dinámicas.

Ya hemos visto anteriormente como crear objetos de forma dinámica mediante arreglos, sin embargo, no como eliminarles de igual forma, lo cual es necesario cuando hacemos uso de municiones en nuestros juegos.

Para empezar, he cambiado el formato del canvas, para aprovecharlo de forma vertical, técnica aprovechada de forma común en juegos de naves espaciales. Esto puede hacerse modificando la etiqueta canvas como se vio al comienzo, o asignando los valores respectivos a la variable canvas en la función "init", justo después de obtener su valor inicial:
        canvas.width=200;
        canvas.height=300;
Recomiendo modificar el tamaño de un canvas lo mínimo necesario desde el código. Esto por que puede causar confusión al jugador, y por que cada vez que se cambia su tamaño, su contenido es limpiado a sus valores predeterminados, lo que puede causar efectos no deseados.

Ahora que ya tenemos listo nuestro lienzo, comencemos entonces creando un arreglo para almacenar nuestros disparos:
    var player=new Rectangle(90,280,10,10);
    var shots=[];
Notemos también que he cambiado la posición del rectángulo player, para que aparezca centrado en la parte inferior del juego. Para que su comportamiento sea el de una nave espacial en esta clase de juegos, he cambiado su movimiento para que solo pueda desplazarse de forma horizontal, comentando las líneas de movimiento vertical:
            // Move Rect
            //if(pressing[KEY_UP])
            //    player.y-=10;
            if(pressing[KEY_RIGHT])
                player.x+=10;
            //if(pressing[KEY_DOWN])
            //    player.y+=10;
            if(pressing[KEY_LEFT])
                player.x-=10;
De igual forma, en lugar de salir por un lado de la pantalla y aparecer por el otro, queremos mantenerlo dentro de la misma. Para ello, usaremos las siguientes líneas:
            // Out Screen
            if(player.x>canvas.width-player.width)
                player.x=canvas.width-player.width;
            if(player.x<0)
                player.x=0;
Podemos ver que lograr esto es sencillo; tan solo se debe regresar al rectángulo a su máxima coordenada permitida, en caso que la exceda en cualquiera de los dos lados (Habría que agregar condicionales similares para el movimiento en Y, en caso de permitir desplazamiento vertical).

¡Ahora sí! Crearemos un nuevo disparo cada vez que presionamos la barra espaciadora:
            // New Shot
            if(lastPress==KEY_SPACE){
                shots.push(new Rectangle(player.x+3,player.y,5,5));
                lastPress=null;
            }
El comportamiento de este código ya es conocido por nosotros. Toca mover cada uno de los disparos en el arreglo, y en caso que el disparo se encuentre ya fuera de pantalla, procederemos a removerlo:
            // Move Shots
            for(var i=0,l=shots.length;i<l;i++){
                shots[i].y-=10;
                if(shots[i].y<0){
                    shots.splice(i--,1);
                    l--;
                }
            }
Para remover el objeto dentro del arreglo, llamamos a la función splice, en este caso, con dos parámetros: El primero de ellos es la posición del arreglo donde efectuaremos la operación, y el segundo indica la cantidad de objetos a eliminar. Splice puede aceptar más parámetros, que sería una lista de objetos a insertar en dicho lugar, pero en este momento, no nos interesa insertar nada aquí.

Notarán también que se la posición del arreglo "i" es continuado por un --. Esto indica que el valor en i se debe restar en 1 después de efectuar la operación, y esto lo hacemos con el fin que, dentro del for, no se vaya a saltar la lectura del siguiente valor en el ciclo una vez removido el presente (Un segundo disparo podría no efectuar la colisión correspondiente si el objecto anterior a este es removido y no se resta el valor de i en 1 tras esta acción). Para que el la longitud "l" corresponda a la verdadera, esta también es restada en uno.

Ahora que hemos comprendido como remover el disparo, procederemos a dibujarle dentro de la función paint:
        ctx.fillStyle='#f00';
        for(var i=0,l=shots.length;i<l;i++)
            shots[i].fill(ctx);
        
        ctx.fillStyle='#fff';
        ctx.fillText('Last Press: '+lastPress,0,20);
        ctx.fillText('Shots: '+shots.length,0,30);
En este caso, he agregado un nuevo texto a la pantalla, que nos mostrará la cantidad de disparos activos en el juego. Con esto podremos confirmar que nuestros disparos son removidos al llegar a la parte superior de la pantalla.

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;
    var player=new Rectangle(90,280,10,10);
    var shots=[];

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

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

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

    function act(){
        if(!pause){
            // Move Rect
            //if(pressing[KEY_UP])
            //    player.y-=10;
            if(pressing[KEY_RIGHT])
                player.x+=10;
            //if(pressing[KEY_DOWN])
            //    player.y+=10;
            if(pressing[KEY_LEFT])
                player.x-=10;

            // Out Screen
            if(player.x>canvas.width-player.width)
                player.x=canvas.width-player.width;
            if(player.x<0)
                player.x=0;
            
            // New Shot
            if(lastPress==KEY_SPACE){
                shots.push(new Rectangle(player.x+3,player.y,5,5));
                lastPress=null;
            }
            
            // Move Shots
            for(var i=0,l=shots.length;i<l;i++){
                shots[i].y-=10;
                if(shots[i].y<0){
                    shots.splice(i--,1);
                    l--;
                }
            }
        }
        // Pause/Unpause
        if(lastPress==KEY_ENTER){
            pause=!pause;
            lastPress=null;
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.fillStyle='#0f0';
        player.fill(ctx);
        ctx.fillStyle='#f00';
        for(var i=0,l=shots.length;i<l;i++)
            shots[i].fill(ctx);
        
        ctx.fillStyle='#fff';
        ctx.fillText('Last Press: '+lastPress,0,20);
        ctx.fillText('Shots: '+shots.length,0,30);
        if(pause){
            ctx.textAlign='center';
            ctx.fillText('PAUSE',100,150);
            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 Rectangle(x,y,width,height){
        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;
    }

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

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

20 comentarios:

  1. Hola, mira tengo una consulta con el metodo splice() porque vos decis que utilizas i-- para que se reste uno a i, pero buscando como funciona ese metodo me tope con una info diferente, dice que el "--" es para que comience a contar desde el ultimo elemento en el array, y la verdad quedé un poco confundido, porque igual con tu explicación no me queda claro por qué no tomaría el siguiente disparo? si lo que supongo que hace es eliminar ese elemento del array y seguir con el siguiente, no logro explicarlo bien, perdona, pero si podrías darme una explicación acerca de eso, te agradecería muchisimo,
    Saludos!

    ResponderEliminar
    Respuestas
    1. De acuerdo, son tres preguntas distintas, intentaré responderlas por separado para que quede claro:

      Para que comience a contar del último, se hace con un número negativo. Por ejemplo, -2, sería el segundo del último para atrás. Este valor nada tiene que ver con i--, aunque -i podría hacer el efecto que indicas.

      i-- es lo opuesto de i++ en el for, esto es, que resta en uno el valor actual de i. Sería lo mismo que hacer i-=1.

      Sobre el por que hacemos esto. Imagina que tienes 8 disparos y vas en el i=3. Al removerlo, todos los siguientes se recorrería en 1. Así, el cuarto sería ahora el tercero, el quinto el cuarto, etc... Cuando pases al for, i sería 4, pero el cuarto original es ahora el tercero, por lo que nos lo estaríamos saltando. Si el tercero estaba también en una colisión, no sería detectada por que nos lo saltamos. Por eso, para evitar este problema, restamos en 1 i despues de remover el disparo, para que al siguiente turno, la siguiente posición (3 que era 4) sea considerada también en el ciclo.

      Disculpa si fue un poco complicado este último párrafo, pero espero que haya quedado más claro para ti este tema.

      Eliminar
    2. No para nada complicado, uff, me confundi con el negativo y el de restar, básicamente me confundí con la posición del "-" jajaj, pero lo otro me quedó bien clarito ahora. Muchisimas gracias, ojala puedas continuar con tus clases magistrales.
      Saludos!

      Eliminar
  2. Muchas gracias por el tutorial.
    Me gustaría saber cómo se puede incorporar munición dinámica para las naves enemigas.
    Saludos

    ResponderEliminar
    Respuestas
    1. Me alegra haya sido de ayuda.

      Incorporar munición dinámica a las naves enemigas, tiene el mismo principio. Solo debes usar un arreglo distinto para almacenarlas, usar las coordenadas de la nave enemiga para crearlas, y un temporizador para saber cada cuanto se creará una nueva.

      ¡Éxito en tus códigos!

      Eliminar
    2. Muchas gracias por la explicación y los capítulos añadidos de munición enemiga :)

      saludos.

      Eliminar
    3. ¡Me alegra saber los ejemplo te ayudaron de forma mejor con elllo!

      ¡Felices códigos!

      Eliminar
  3. Tus ejemplos son geniales, ya me he acabado todo el capitulo 1, y ahora estoy siguiendo adelante con los tutoriales, geniales sin duda, muy bien explicados y ejecutados! Gracias por el tiempo que dedicas a esto, me está sirviendo de mucho.

    ResponderEliminar
    Respuestas
    1. ¡Me alegra saber que te han estado ayudando! Felices códigos :)

      Eliminar
  4. Un muy buen tutorial.
    Hay alguna forma de poder disparar mientras te mueves?

    ResponderEliminar
    Respuestas
    1. ¿Acaso el presente ejemplo no lo permite? ¿O que tienes en mente precisamente?

      Eliminar
    2. Me referia a disparo continuo mientras te mueves, es decir, mantenrr presionado el disparo y moverte a la vez no se si me explico bien

      Eliminar
    3. ¡Ah! Tan sólo cambia lastPress==KEY_SPACE por pressing[KEY_SPACE].

      Eliminar
  5. Hola, en primer lugar excelente tutorial en todos los aspectos, cosas que en otras partes nunca se tratan.

    Tenia una consulta: ¿hay alguna manera de manejar los disparos con el metodo del delta de tiempo? Lo que me pasa al ocupar esto es que si mantengo presionada la barra espacio, los disparos salen juntos y no se distinguen los cuadrados, sino que sale como una sola linea larga, no se si me explico. ¿Habra alguna forma de separarlos o algo asi?

    Saludos

    ResponderEliminar
    Respuestas
    1. Precisamente este tema fue tratado más adelante en el tema Botones en Pantalla (http://juegos.canvas.ninja/2013/09/botones-en-pantalla.html)

      Prácticamente, es con este código como debe manejarse:

      // New Shot
      if(shootTimer>0){
      shootTimer-=deltaTime;
      }
      else if(pressing[KEY_SPACE]){
      shots.push(new Rectangle(player.x+3,player.y,4,4));
      shootTimer=100;
      }

      Espero esto resuelva tu duda. ¡Suerte!

      Eliminar
    2. Con razon, aun no llego a esa seccion. Muchisimas gracias!!

      Eliminar
  6. hola soy yo denuevo el otro dia me ayudaste con las escenas. la segunda linea de este codigo como funciona xq me dice que deltaTime no esta definida

    if(shootTimer>0){
    shootTimer-=deltaTime;
    }else if(pressing[KEY_SPACE]){
    shots.push(new Rectangle(player.x+3,player.y,4,4));
    shootTimer=6;
    }

    ResponderEliminar
    Respuestas
    1. Veo que estás tomando el ejemplo de http://juegos.canvas.ninja/2013/09/botones-en-pantalla.html

      Más adelante se envía muestra como se envía deltaTime a la función "act" para esta clase de acciones. En el enlace se muestra como se usa para tiempos dinámicos, pero dado que este ejemplo está con tiempo regulado, puedes usar directamente el valor 0.05 en su lugar.

      Eliminar
  7. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  8. Vuelvo a publicarlo porque escribí mal algo!
    Para los limites de la pantalla sería mejor detectarlos cuando se presiona una tecla, por ejemplo al apretar la flecha izquierda voy a mover la nave pero podemos añadirle una condicion: si aprieto la flecha izquierda y la posicion de la nave es mayor a 0 entonces muevo la nave.
    codigo de ejemplo:
    if(pressing[KEY_LEFT] && player.x > 0){
    player.x -= 10;
    }
    if(pressing[KEY_RIGHT] && player.x+player.width < canvas.width){
    player.x += 10;
    }

    ResponderEliminar