Aprende a crear juegos en HTML5 Canvas

lunes, 1 de octubre de 2012

¡Huye!

En la anterior entrega vimos como mover los objetos en un desplazamiento angular, y creamos un objeto que sigue a nuestro jugador para todos lados. Con este conocimiento, crearemos un nuevo juego en esta entrega.

¿Un nuevo juego tan pronto? Así es. Con los conocimientos que ya tenemos de juegos previos, y lo recientemente aprendido, podemos crear un nuevo minijuego. Comencemos declarando las variables que necesitaremos, tú ya sabes la función de cada una:
    var pause=true;
    var gameover=true;
    var score=0;
    var eTimer=0;
    var bombs=[];
Agreguemos también a nuestra función init, un escucha del ratón el cual, al presionar sobre nuestro canvas, activará o desactivará la pausa respectivamente:
        canvas.addEventListener('mousedown',function(evt){
            pause=!pause;
        },false);
Tu ya sabes crear el flujo del juego, así que me enfocaré tan solo en los nuevos elementos. Como habrás notado, tenemos un arreglo de bombas, que almacenarán nuestra lista de explosivos. Estos seguirán a nuestro jugador por toda la pantalla, y tras un rato, explotarán. Para calcular el tiempo de explosión, necesitaremos una nueva variable en nuestro círculo:
        this.timer=0;
Agregamos ahora el temporizador. Cada 0.5 a 3 segundos, se agregará una nueva bomba:
            // Generate new bomb
            eTimer-=deltaTime;
            if(eTimer<0){
                var bomb=new Circle(0,0,10);
                bomb.timer=2;
                bombs.push(bomb);
                eTimer=0.5+random(2.5);
            }
Como a nuestra bomba queremos agregarle una variable que no puede ser accedida desde nuestro constructor, primero creamos una nueva variable bomb. A esta posteriormente, le agregamos el temporizador de 2 segundos y finalmente empujamos la nueva bomba al arreglo de bombas. Por último, se asigna el temporizador para la siguiente bomba.

Continuemos programando el comportamiento de las bombas. Primero, nos encargaremos de que cada bomba siga al jugador, y restamos su temporizador en uno:
            // Bombs
            for(var i=0,l=bombs.length;i<l;i++){
                bombs[i].timer-=deltaTime;
                var angle=bombs[i].getAngle(player);
                bombs[i].move(angle,speed*deltaTime);
Si el temporizador es menor a cero, será el momento de la explosión de la bomba. Su radio se duplicará (Para efecto de la explosión), y si la bomba está colisionando con el jugador, el juego terminará:
                if(bombs[i].timer<0){
                    bombs[i].radius*=2;
                    if(bombs[i].distance(player)<0){
                        gameover=true;
                        pause=true;
                    }
                }
            }
Posteriormente eliminamos la bomba y aumentamos el puntuaje en uno. Para que la explosión sea dibujada antes de eliminarle, haremos esto al siguiente turno, poniendo estas instrucciones al comienzo del ciclo for, antes de restar su temporizador. Esto creará el efecto deseado:
                if(bombs[i].timer<0){
                    score++;
                    bombs.splice(i--,1);
                    l--;
                    continue;
                }
Por último, dibujemos la bomba. Si esta está explotando, la rellenaremos de color blanco. Caso contrario, solo dibujaremos su circunferencia; rojo en estado normal, e intercalará entre rojo y blanco cuando esté por explotar (Cuando el temporizador sea menor a 1 segundo):
        for(var i=0,l=bombs.length;i<l;i++){
            if(bombs[i].timer<0){
                ctx.fillStyle='#fff';
                bombs[i].fill(ctx);
            }
            else{
                if(bombs[i].timer<1&&~~(bombs[i].timer*10)%2==0)
                    ctx.strokeStyle='#fff';
                else
                    ctx.strokeStyle='#f00';
                bombs[i].stroke(ctx);
            }
        }
Para hacer el efecto de que titile en esta ocasión, multiplicaremos el temporizador por 10 y lo convertimos a enteros para obtener las décimas de segundo. Ya de este valor, obtenemos su residuo entre dos, y si este es 0, se pintará su contorno de color blanco. El cálculo permite que titile 5 veces antes de explotar.

Probamos ahora el juego. Veremos ahora que bombas son continuamente generadas de la esquina superior derecha, y explotan tras un momento. Sin embargo, este juego no representa un verdadero reto. Agreguemos algunas mejoras a la generación de bombas:
                var bomb=new Circle(random(2)*canvas.width,random(2)*canvas.height,10);
                bomb.timer=1.5+random(2.5);
                bombs.push(bomb);
La primer línea puede ser un poco confusa, pero en realidad es bastante sencillo. Primero obtenemos un valor al azar de 0 o 1, y lo multiplicamos por el ancho de la pantalla, lo que nos dará un 0 o el valor del ancho mismo. Posteriormente hacemos lo mismo con la altura. Esto nos permitirá que las bombas sean generadas en cualquiera de las cuatro esquinas de nuestro juego.

En la segunda línea, agregamos un factor al azar para las explosiones. Esto permite que el tiempo de explosión no dure siempre lo mismo, y de esta forma, las primeras bombas en aparecer no sean obligatoriamente las primeras en explotar.

Aun con estos cambios el juego no es lo suficiente complejo para hacer perder al jugador. ¿Saben que agregaría realmente un reto a este juego? Si las bombas se movieran más rápido conforme pasa el tiempo de juego...

Agreguemos una segunda variable nueva a los círculos:
        this.speed=0;
Ahora, agreguemos velocidad a la bomba al ser creada:
                bomb.speed=100+(random(score))*10;
A la velocidad base, le estoy sumando un valor al azar con la quinta fracción del máximo valor del puntuaje actual. Esto permitirá no solo bombas veloces en niveles más altos, si no además, bombas con diferentes velocidades, lo que hará que evadirlas sea más difícil.

Por último, no olvides mover las bombas de acuerdo a su velocidad:
                bombs[i].move(angle,bombs[i].speed*deltaTime);
¡Ahora sí! Disfrutemos de nuestro nuevo juego. Con tan solo conocer las bases de un nuevo tipo de movimiento, y todo lo aprendido, pudimos crear un juego totalmente nuevo para nuestra colección, prueba del aprendizaje que has adquirido durante este curso. Y con esto también, concluimos el curso básico de uso del ratón en juegos. Como siempre, las dudas que tengas, puedes dejarlas en los comentarios. ¡Felices códigos!

Código final:

[Canvas not supported by your browser]

(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var mousex=0,mousey=0;
    var lastUpdate=0;
    var pause=true;
    var gameover=true;
    var score=0;
    var eTimer=0;
    var player=new Circle(0,0,5);
    var bombs=[];

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

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

    function run(){
        requestAnimationFrame(run);
            
        var now=Date.now();
        var deltaTime=(now-lastUpdate)/1000;
        if(deltaTime>1)deltaTime=0;
        lastUpdate=now;
        
        act(deltaTime);
        paint(ctx);
    }

    function reset(){
        score=0;
        eTimer=0;
        bombs.length=0;
        gameover=false;
    }

    function act(deltaTime){
        if(!pause){
            // GameOver Reset
            if(gameover)
                reset();
            
            // Move player
            player.x=mousex;
            player.y=mousey;
            
            // Keep player in canvas
            if(player.x<0)
                player.x=0;
            if(player.x>canvas.width)
                player.x=canvas.width;
            if(player.y<0)
                player.y=0;
            if(player.y>canvas.height)
                player.y=canvas.height;
            
            // Generate new bomb
            eTimer-=deltaTime;
            if(eTimer<0){
                var bomb=new Circle(random(2)*canvas.width,random(2)*canvas.height,10);
                bomb.timer=1.5+random(2.5);
                bomb.speed=100+(random(score))*10;
                bombs.push(bomb);
                eTimer=0.5+random(2.5);
            }
            
            // Bombs
            for(var i=0,l=bombs.length;i<l;i++){
                if(bombs[i].timer<0){
                    score++;
                    bombs.splice(i--,1);
                    l--;
                    continue;
                }
                
                bombs[i].timer-=deltaTime;
                var angle=bombs[i].getAngle(player);
                bombs[i].move(angle,bombs[i].speed*deltaTime);
                
                if(bombs[i].timer<0){
                    bombs[i].radius*=2;
                    if(bombs[i].distance(player)<0){
                        gameover=true;
                        pause=true;
                    }
                }
            }
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        for(var i=0,l=bombs.length;i<l;i++){
            if(bombs[i].timer<0){
                ctx.fillStyle='#fff';
                bombs[i].fill(ctx);
            }
            else{
                if(bombs[i].timer<1&&~~(bombs[i].timer*10)%2==0)
                    ctx.strokeStyle='#fff';
                else
                    ctx.strokeStyle='#f00';
                bombs[i].stroke(ctx);
            }
        }
        ctx.strokeStyle='#0f0';
        player.stroke(ctx);
        
        ctx.fillStyle='#fff';
        //ctx.fillText('Distance: '+player.distance(bombs[0]).toFixed(1),10,10);
        //ctx.fillText('Angle: '+(player.getAngle(bombs[0])*(180/Math.PI)).toFixed(1),10,20);
        ctx.fillText('Score: '+score,10,10);
        if(pause){
            ctx.textAlign='center';
            if(gameover)
                ctx.fillText('GAME OVER',150,100);
            else
                ctx.fillText('PAUSE',150,100);
            ctx.textAlign='left';
        }
    }

    function enableInputs(){
        document.addEventListener('mousemove',function(evt){
            mousex=evt.pageX-canvas.offsetLeft;
            mousey=evt.pageY-canvas.offsetTop;
        },false);
        canvas.addEventListener('mousedown',function(evt){
            pause=!pause;
        },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.timer=0;
        this.speed=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.getAngle=function(circle){
        if(circle!=null)
            return (Math.atan2(circle.y-this.y,circle.x-this.x));
    }

    Circle.prototype.move=function(angle,speed){
        if(speed!=null){
            this.x+=Math.cos(angle)*speed;
            this.y+=Math.sin(angle)*speed;
        }
    }

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

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

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

19 comentarios:

  1. Me sorprende que siempre que entro a una nueva entrada tuya veo el ejemplo y me parece muuuy complicado, pero en cambio miro el código y cambio de opinión, si lo entiendes no hay problema.

    Jamás pensé que las mates afectasen tanto en la prog, recuerdo cuando era chico y di el movimiento angular en clases y la sorpresa es que de mayor nunca pensé que lo fuese a utilizar, curioso jaja xD

    Gran code compañero! ^^

    ResponderBorrar
    Respuestas
    1. ¡Te comprendo! Solía pensar lo mismo al ver esas materias. Ahora agradezco todo lo que aprendí en esas clases, pues me llevan a experimentos bastante divertidos como este. Lo que ahora me molesta, es no haber puesto mejor atención en matrices, que es la base para 3D :P... Pero nunca es tarde para aprender bien las cosas ;)

      ¡Muchas gracias por tu comentario! ;)

      Borrar
  2. Buenas renegando un poco pude lograr lo que queria que es poder crear bombas que luego exploten. dejo el script por si alguien le interesa

    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var lastKey=null;
    var mousex=0,mousey=0;
    var boom=3;
    var player=new Circle(0,0,5);
    var eTimer=0;
    //var enemies=new Circle(100,100,10);
    var enemies=[];
    var wall=[];
    var map1=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1];
    var target=new Circle(100,100,10);

    function setMap(map,columns,blockSize){
    var col=0;
    var row=0;
    wall.length=0;

    for(var i=0,l=map.length;i=columns){
    row++;
    col=0;
    }
    }
    }

    function init(){
    canvas=document.getElementById('canvas');
    canvas.style.background='#000';
    ctx=canvas.getContext('2d');
    enableInputs();
    setMap(map1,30,20);
    run();
    }

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

    function run(){
    setTimeout(run,50);
    game();
    paint(ctx);
    }

    function game(){
    player.x=mousex;
    player.y=mousey;

    if(player.x<0)
    player.x=0;
    if(player.x>canvas.width)
    player.x=canvas.width;
    if(player.y<0)
    player.y=0;
    if(player.y>canvas.height)
    player.y=canvas.height;

    eTimer--;
    if(lastKey==1){
    canvas.style.background='#333';

    if(player.distance(target)<0){
    boom+=3;
    target.x=random(canvas.width/10-1)*10+target.radius;
    target.y=random(canvas.height/10-1)*10+target.radius;
    }else if(boom>0){
    //enemies.push(new Rectangle(player.x,player.y,30,30));
    //var enemie=new Circle(player.x,player.y,10);
    enemies.push(new Circle(player.x,player.y,10));
    boom--;
    // enemies.push(enemie);
    //enemies.timer=50+random(50);
    eTimer=10+random(50);
    }
    }else{
    canvas.style.background='#000';
    }



    // Move Shots
    /*for(var i=0;icanvas.height-30){
    //enemies.splice(i--,1);
    enemies[i].y-=5;
    }else if(enemies[i].timer==0){
    enemies[i].radius*=2;

    }
    }


    }

    function paint(ctx){
    ctx.clearRect(0,0,canvas.width,canvas.height);

    ctx.fillStyle='#999';
    for(var i=0,l=wall.length;irect.x&&
    this.yrect.y);
    }
    }
    }

    function Circle(x,y,radius){
    this.x=(x==null)?0:x;
    this.y=(y==null)?0:y;
    this.radius=(radius==null)?0:radius;
    this.timer=30;

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

    ResponderBorrar
    Respuestas
    1. ¡Gracias por compartir! Quería ver a detalle lo que habías conseguido, pero el código me da bastantes errores... ¿Crees que podrías revisar de nuevo el código que dejaste?

      Borrar
  3. Las lecciones de este blog me están siendo muy útiles, muchas gracias.

    Y a los demás por vuestros comentarios, ayudan mucho :))

    Cris.

    ResponderBorrar
  4. hermosos tutoriales... pero no c si me perdi de algo o que.. pero tengo una duda el index.html es el mismo para todos los casos?? porque siempre veo que trabajas el javascript.... gracias por tu respuesta de antemano

    ResponderBorrar
    Respuestas
    1. El index.html es el mismo! Tan solo ha sido agregada posteriormente una línea para que trabaje en móviles, tres temas atras. De ahi fuera, es el mismo para todos los ejemplos.

      Borrar
  5. hola denuevo disculpa que sea canson pero tengo una duda:
    si quiero poner una imagen de fondo al juego como hago??
    he intentado creando una imagen
    var image= new Image();
    image.src=''imagen.jpg''
    ctx.drawImage(img,10,10); (este lo pongo dentro del paint)
    pero eso no me funciona
    me podrias ayudar con eso??
    en algunas paginas encontre que lo hacian con css pero la imagen se pierde cuando comienza a ejecutarse el juego.

    ResponderBorrar
    Respuestas
    1. El método que usas es correcto, pero si usas ese mismo código en tu juego, temo que comienzas declarando una variable "image", y luego intentas dibujar una variable "img" que no existe...

      Espero eso resuelva tu problema. ¡Éxito!

      Borrar
  6. hola que tal estoy aprendiendo canvas, no es muy complicado ya habia visto el juego de la serpiente en clase y nos dejo el maestro hacerlo xD pero no fue muy complicado en mis epocas de estudio haahah ahora mi pregunta es que siginifica "~~" en la codificacion JAMAS lo habia visto, excelentes tutoriales, creo que tus tutoriales me serviran para hacer un juego en phonegap :D

    ResponderBorrar
    Respuestas
    1. Este dato se explicó brevemente en una entrega pasada. En resumen, es una técnica llamada BitWise de doble negación, que por sus propiedades en JavaScript, nos permite convertir de forma veloz y eficaz, un número flotante a un número entero. Funciona como un Math.floor para números positivos, y como un Math.ciel para números negativos.

      ¡Mucha suerte con tu proyecto!

      Borrar
  7. hey men dissculpa que sea canson pero hace poco retome con mis praticas de canvas y aun sigue sin funcionarme el codigo..... hasta le he echo copy paste al index el js y no me funciona :(
    podrias ayudarme con eso?? pasarme el codigo bien o algo

    ResponderBorrar
    Respuestas
    1. ¿Es el código presente o el de la imagen que mencionaste el que no te funciona?

      Borrar
    2. el codigo presente.... tu me dijiste que funcionaba con el index del inicio pero creo el index.html y el game.js y cuando lo corro en el google chrome solo me sale el lienzo gris y no hace nada mas :/

      Borrar
    3. Al parecer había un error de dedo en el código de ejemplo, que donde debía ir una "t", se fue una "y" al escribirlo. Prueba de nuevo el código corregido, y hazme saber si esta vez te funciona sin problemas.

      Borrar
    4. hey ahora si corre perfecto..... gracias por ser buena gente :)

      Borrar
    5. ¡Un placer! Y de hecho, es una disculpa de mi parte la que debo por ese pequeño error. ¡Pero muchas gracias por haberlo descubierto para poder corregirle! ¡Éxito en tu aprendizaje!

      Borrar
  8. Una pregunta, ¿hay alguna forma de evitar que el jugador se mueva a su velocidad máxima?
    Esto lo digo porque me he dado cuenta que agitando el ratón en circulos a mucha velocidad les es imposible pillarte a las explosiones, al menos en la versión blog, ¿podrías decirme como evitar que pueda ir tan rápido el jugador?

    ResponderBorrar
    Respuestas
    1. ¿A que nivel llegas? Que después de un rato (quizá un poco largo), simplemente me es imposible seguir evitando las bombas.

      Respondiendo a tu pregunta, la técnica para limitar el movimiento por ratón, es implementar un seguidor también al personaje, pero con un margen de velocidad bastante superior. Quizá esto te ayude en tu versión del proyecto. ¡Suerte!

      Borrar