Aprende a crear juegos en HTML5 Canvas

domingo, 22 de septiembre de 2013

Acelerómetro

Otra alternativa para mover nuestros objetos en un juego, es usar el acelerómetro incluido en los dispositivos móviles, que miden la aceleración en cada uno de los tres ejes del dispositivo. Gracias a la aceleración de la gravedad, también podemos conocer la inclinación de nuestro dispositivo.

Para este ejemplo, solo necesitaremos conocer la aceleración en el eje X. Comencemos declarando estas dos variables:
    var motionSupport=false;
    var accelerationX=0;
Para habilitar el acelerómetro, primero debemos de comprobar si este está implementado, y en dado caso, creamos un escucha para el movimiento del dispositivo:
    if(window.DeviceMotionEvent){
        motionSupport=true;
        window.addEventListener('devicemotion',function(evt){
            accelerationX=evt.accelerationIncludingGravity.x;
            //accelerationY=evt.accelerationIncludingGravity.y;
            //accelerationZ=evt.accelerationIncludingGravity.z;
        },false);
    }
He dejado comentados la aceleración en el eje Y y Z ya que no son usadas en este ejemplo, pero podrías bien aprovecharle en futuros proyectos. Ahora, imprimimos en pantalla un mensaje en caso que el movimiento del dispositivo no sea soportado, así como la aceleración en el eje X:
        if(!motionSupport)
            ctx.fillText('MOTION NOT SUPPORTED: ',0,10);
        ctx.fillText('AccelerationX: '+accelerationX,0,20);
Nota: Tengo reportado que las versiones anteriores a octubre 2013 de Chrome y Opera con Blink, no soportan el acelerómetro debido a un bug.

Ahora que tenemos el valor de la aceleración en X, pasaremos a usarlo para mover nuestro personaje. Antes, he removido todos los botones de la lección anterior, exceptuando el de pausa, y dado que no uso botones, he cambiado el modo de disparo para que sea activado al tocar en cualquier lugar de nuestra pantalla; esto lo he hecho, detectando si el primer toque no es nulo:
            else if(touches[0]!=null){
                shots.push(new Rectangle(player.x+3,player.y,4,4));
                shootTimer=100;
            }
La aceleración retorna un valor de -10 a 10 aproximadamente, dependiendo de la inclinación del dispositivo con respecto a ese eje. Para usar el valor de la aceleración, existen varias formas. A continuación se muestran las opciones más populares:

Movimiento básico:

Prácticamente es simular el movimiento que ya hemos venido haciendo antes, dependiendo solo si el móvil está relativamente inclinado o no, tal como se ve en el siguiente ejemplo:
            // Move Rect
            if(accelerationX>2) //RIGHT
                player.x+=120*deltaTime;
            if(accelerationX<-2) //LEFT
                player.x-=120*deltaTime;
De esta forma, se realiza el movimiento básico tomando en cuenta únicamente si el dispositivo está inclinado, o no.

Ver el ejemplo: http://canvas.ninja/mobile4a.html

Movimiento acelerado:

Podemos sumar a nuestro personaje el valor de la aceleración multiplicado a nuestro movimiento, tal como se muestra en este ejemplo:
            // Move Rect
            player.x+=accelerationX*120*deltaTime;
Este tipo de movimiento nos permite realizar movimientos más precisos, que dependen de la inclinación de nuestro dispositivo.

Ver el ejemplo: http://canvas.ninja/mobile4b.html

Movimiento absoluto:

Por último, podemos posicionar a nuestro personaje en un lugar exacto de la pantalla, dependiendo de la inclinación de nuestro dispositivo. Por ejemplo, si queremos delimitar la posición de la aceleración -4 a 4, tenemos que dividir el ancho de la pantalla en 8 (Son 8 partes iguales de -4 a 4), multiplicarlo por la aceleración en X, y sumarla al centro de la pantalla, es decir, a canvas.width/2, tal como se ve en el siguiente ejemplo:
            // Move Rect
            player.x=(canvas.width/2)+(accelerationX*canvas.width/8);
Toma en cuenta que una aceleración de 5 es el dispositivo inclinado a 45 grados, y más allá de este valor, la orientación de la pantalla cambiará en caso de no estar bloqueada, por lo que no es recomendable usar valores mayores a 4, para evitar este problema.

Ver el ejemplo: http://canvas.ninja/mobile4c.html

Con esto, hemos aprendido a usar el acelerómetro en dispositivos móviles. El código final de hoy corresponde al primer ejemplo mostrado; si deseas usar uno distinto, solo cambia la sección de mover el rectángulo por el código correspondiente.

Código Final:

(function(){
    'use strict';
    window.addEventListener('load',init,false);
    window.addEventListener('resize',resize,false);
    var canvas=null,ctx=null;
    var scaleX=1,scaleY=1;
    var touches=[];
    var lastPress=null;
    var pause=false;
    var motionSupport=false;
    var accelerationX=0;
    var time=0;
    var shootTimer=0;
    var shots=[];
    var player=new Rectangle(100,260,10,10);
    var btnPause=new Button(90,0,20,20);

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

    function resize(){
        if(window.innerWidth>window.innerHeight){
            canvas.style.position='';
            canvas.style.top='';
            canvas.style.left='';
            canvas.style.width='';
            canvas.style.height='';
            scaleX=1;
            scaleY=1;
        }
        else{
            canvas.style.position='fixed';
            canvas.style.top='0';
            canvas.style.left='0';
            canvas.style.width='100%';
            canvas.style.height='100%';
            scaleX=canvas.width/window.innerWidth;
            scaleY=canvas.height/window.innerHeight;
        }
    }

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

    function act(deltaTime){
        if(!pause){
            // Move Rect
            if(accelerationX>2) //RIGHT
                player.x+=120*deltaTime;
            if(accelerationX<-2) //LEFT
                player.x-=120*deltaTime;

            // 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(shootTimer>0){
                shootTimer-=deltaTime;
            }
            else if(touches[0]!=null){
                shots.push(new Rectangle(player.x+3,player.y,4,4));
                shootTimer=0.1;
            }
            
            // Move Shots
            for(var i=0,l=shots.length;i<l;i++){
                shots[i].y-=120*deltaTime;
                if(shots[i].y<0){
                    shots.splice(i--,1);
                    l--;
                }
            }
        }
        // Pause/Unpause
        if(lastPress==1&&btnPause.touch()){
            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';
        if(!motionSupport)
            ctx.fillText('MOTION NOT SUPPORTED: ',0,10);
        ctx.fillText('AccelerationX: '+accelerationX,0,20);
        ctx.fillText('Shots: '+shots.length,0,30);
        if(pause){
            ctx.textAlign='center';
            ctx.fillText('PAUSE',95,150);
            ctx.textAlign='left';
        }
        
        ctx.strokeStyle='#fff';
        btnPause.stroke(ctx);
        
        // Touch Debug
        ctx.fillStyle='#999';
        for(var i=0,l=touches.length;i<l;i++){
            if(touches[i]!=null){
                ctx.fillRect(touches[i].x-1,touches[i].y-1,2,2);
            }
        }
    }

    function enableInputs(){
        if(window.DeviceOrientationEvent){
            motionSupport=true;
            window.addEventListener('devicemotion',function(evt){
                accelerationX=evt.accelerationIncludingGravity.x;
                //accelerationY=evt.accelerationIncludingGravity.y;
                //accelerationZ=evt.accelerationIncludingGravity.z;
            },false);
        }
        
        canvas.addEventListener('touchstart',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                var x=~~((t[i].clientX-canvas.offsetLeft)*scaleX);
                var y=~~((t[i].clientY-canvas.offsetTop)*scaleY);
                touches[t[i].identifier%100]=new Vtouch(x,y);
            }
        },false);
        canvas.addEventListener('touchmove',function(evt){
            evt.preventDefault();
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                if(touches[t[i].identifier%100]){
                    touches[t[i].identifier%100].x=~~((t[i].clientX-canvas.offsetLeft)*scaleX);
                    touches[t[i].identifier%100].y=~~((t[i].clientY-canvas.offsetTop)*scaleY);
                }
            }
        },false);
        canvas.addEventListener('touchend',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                touches[t[i].identifier%100]=null;
            }
        },false);
        canvas.addEventListener('touchcancel',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                touches[t[i].identifier%100]=null;
            }
        },false);
        
        canvas.addEventListener('mousedown',function(evt){
            evt.preventDefault();
            var x=~~((evt.clientX-canvas.offsetLeft)*scaleX);
            var y=~~((evt.clientY-canvas.offsetTop)*scaleY);
            touches[0]=new Vtouch(x,y);
        },false);
        document.addEventListener('mousemove',function(evt){
            if(touches[0]){
                touches[0].x=~~((evt.clientX-canvas.offsetLeft)*scaleX);
                touches[0].y=~~((evt.clientY-canvas.offsetTop)*scaleY);
            }
        },false);
        document.addEventListener('mouseup',function(evt){
            touches[0]=null;
        },false);

        function Vtouch(x,y){
            this.x=x||0;
            this.y=y||0;
        }
    }

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

    function Button(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;
    }

    Button.prototype.touch=function(){
        for(var i=0,l=touches.length;i<l;i++){
            if(touches[i]!=null){
                if(this.x<touches[i].x&&
                    this.x+this.width>touches[i].x&&
                    this.y<touches[i].y&&
                    this.y+this.height>touches[i].y){
                    return true;
                }
            }
        }
        return false;
    }

    Button.prototype.stroke=function(ctx){
        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);};
    })();
})();

5 comentarios:

  1. Hola,
    Siguiendo tu curso no he podido hacer que el juego interactue con la tablet. Puede ser que los índices de la array touches[] sean touches[i] en lugar de touches[t[i].identifier]

    Felicidades por el curso,
    Xavi

    ResponderEliminar
    Respuestas
    1. El for encuentra el touch indicado mediante el identificador, asi que ese no es el problema. Yo he probado estos códigos sin problemas en mi tableta. ¿Puedes indicarme que tableta y navegador estas usando?

      Eliminar
  2. se que esto es viejo pero esto no funciona si la auto rotación esta activada puesto que el acelero-metro al parecer no puede ser usado por dos aplicaciones a la vez :D

    ResponderEliminar
    Respuestas
    1. Qué extraño, pues yo tengo ambos activados en mi móvil y los dos funcionan correctamente. Eso sí, es bastante incómodo para un juego.

      Eliminar
  3. se que esto es viejo pero esto no funciona si la auto rotación esta activada puesto que el acelero-metro al parecer no puede ser usado por dos aplicaciones a la vez :D

    ResponderEliminar