Aprende a crear juegos en HTML5 Canvas

domingo, 8 de septiembre de 2013

Pantalla completa en dispositivos móviles

Para aprovechar mejor los juegos en dispositivos móviles, es necesario ponerlos a pantalla completa del mismo. Para ello, hay que estirar el lienzo al tamaño de la pantalla; Pero hay un problema a considerar, que son las distintas relaciones de aspecto.

En la siguiente imagen, se muestran las relaciones de aspecto usadas en dispositivos móviles:



La mayoría de los dispositivos en Android usan las relaciones 3:2, 5:3 y 16:9. En iOS, los iPhone clásicos usan la relación 3:2, el iPhone5 la relación 16:9, y los iPad la relación 4:3. Así es; demasiados tipos de pantalla diferentes a los cuales dar soporte.

Para que un juego de soporte a tantas pantallas tan diversas, existen varias soluciones, tales como agregar barras negras a los lados (No muy común), estirar el lienzo a la pantalla completa (Distorsiona la imagen), o ajustar dinámicamente la relación. Este último es muy efectivo en juegos que usan cámaras, ya que esta se encarga automáticamente de unos dolores de cabeza extra que hay que tomar en cuenta cuando se usa esta técnica.

Por ahora, les enseñaré a usar el método de estirar el lienzo a la pantalla completa, el cual es el más sencillo de implementar y mantener. "Pero espera, ¿No acabas de decir que esto distorsiona la imagen?", seguramente muchos han de preguntar. Y sí, acabo de decir eso, pero me he dado cuenta que, si se usan los aspectos 3:2 o 5:3 con esta técnica, la distorsión es poco perceptible a menos que compares dos dispositivos lado a lado. De hecho, puedo asegurarte que muchos de los juegos que tienes en tus dispositivos móviles usan esta técnica, ¡Y tú ni te habías dado cuenta!

(Y en este momento, dejaste de leer la entrada para revisar los juegos en tu móvil).

¿Ya regresaste? ¡Bien! Apliquemos entonces esta técnica al ejemplo de la semana anterior. Para ello, solo debemos agregar estas líneas en la función init:
        canvas.style.position='fixed';
        canvas.style.top='0';
        canvas.style.left='0';
        canvas.style.width='100%';
        canvas.style.height='100%';
¡Y listo! Prueba el ejemplo desde tu móvil y podrás disfrutarlo en pantalla completa.

¡Pero espera! Cuando tocas en la pantalla, descubres que los toques no están siendo dibujados donde pones tu dedo. Este es un problema muy frecuente relacionado con escalar el lienzo, que afecta al ratón y a los toques, ya que estos son leídos respecto a su tamaño original, por lo que al escalarlo, el resultado no es el esperado.

Para resolver este problema, necesitamos conseguir la relación de la escala entre su tamaño original y el nuevo tamaño. Dado que estamos escalándolo a pantalla completa, esto será sencillo de conseguir, mediante las siguientes fórmulas:
        scaleX=canvas.width/window.innerWidth;
        scaleY=canvas.height/window.innerHeight;
Posteriormente, este valor debe ser multiplicado a cada asignación del toque en X y Y dentro de los escuchas a touchstart, touchmove, mousedown y mousemove. Por ejemplo, en el caso de touchstart, los valores en X y Y serán asignados ahora de esta forma:
                var x=~~((t[i].pageX-canvas.offsetLeft)*scaleX);
                var y=~~((t[i].pageY-canvas.offsetTop)*scaleY);
Dado que las escalas suelen no ser valores enteros, es buena idea convertir estos valores al final en sus respectivos enteros. No olvides declaras las variables scaleX y scaleY al comienzo del código.

Ahora sí, al probar los toques en el lienzo escalado, comprobaremos que estos corresponden al área donde estamos tocando.

Orientación del dispositivo


El lienzo actualmente está orientado a modo retrato (vertical). Si giramos el lienzo a modo paisaje (horizontal), descubriremos que la imagen es estirada de forma nada agradable. Actualmente se está trabajando en un estándar para permitir a las páginas web, bloquear la orientación de su contenido, el cual se implementará de esta forma:
<style type="text/css">
    @viewport{orientation:portrait}
</style>
Sin embargo, la implementación de este parece no ver luz en próximos meses, quizá no para este año. Por ahora, la mejor opción que tenemos, es determinar la orientación del dispositivo, y actuar de acuerdo a esta.

La forma más sencilla de esto, es asignar una función a un escucha que determine cuando el tamaño de la página ha cambiado:
    window.addEventListener('resize',resize,false);
En la función, analizamos el ancho y alto de la página. Si el ancho es mayor, estamos en modo paisaje, de lo contrario, estamos en modo retrato:
    function resize(){
        if(window.innerWidth>window.innerHeight){
            // Landscape
        }
        else{
            // Portrait
        }
    }
No olvides llamar a la función resize en el init, antes de la función run, para determinar la orientación inicial del dispositivo, y actuar de acuerdo a esta.

Para el presente ejemplo cambiaré la orientación del lienzo de acuerdo a la orientación del dispositivo, para que cuando sea escalado, lo haga de forma más agradable:
    function resize(){
        if(window.innerWidth>window.innerHeight){
            canvas.width=300;
            canvas.height=200;
        }
        else{
            canvas.width=200;
            canvas.height=300;
        }
        scaleX=canvas.width/window.innerWidth;
        scaleY=canvas.height/window.innerHeight;
    }
Nota que después de ajustar la orientación del lienzo, se vuelven a obtener los valores de la escala, dado que el tamaño del lienzo y la página han cambiado.

Quizá esta no sea la mejor opción para los juegos, dado que la mayoría no están diseñados para aceptar un cambio de orientación, pero para el ejemplo actual funciona. Puedes probar diferentes métodos de acuerdo a las necesidades de tu juego.

Ahora sí, el ejemplo funcionará bien en cualquier escala, y orientación. Dejo el ejemplo de hoy en un enlace externo, ya que incluirlo en el blog sería algo intrusivo.

Código Final:


Ver el ejemplo: http://blog.jugaa.me/mobile2.html

(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 COLORS=['#f00','#0f0','#00f','#fff'];

    function init(){
        canvas=document.getElementById('canvas');
        ctx = canvas.getContext('2d');
        canvas.width=200;
        canvas.height=300;
        
        canvas.style.position='fixed';
        canvas.style.top='0';
        canvas.style.left='0';
        canvas.style.width='100%';
        canvas.style.height='100%';
        
        enableInputs();
        resize();
        run();
    }

    function resize(){
        if(window.innerWidth>window.innerHeight){
            canvas.width=300;
            canvas.height=200;
        }
        else{
            canvas.width=200;
            canvas.height=300;
        }
        scaleX=canvas.width/window.innerWidth;
        scaleY=canvas.height/window.innerHeight;
    }

    function run(){
        requestAnimationFrame(run);
        act();
        paint(ctx);
    }

    function act(){

    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.fillStyle='#999';
        ctx.fillText('Touch to test',10,10);
        for(var i=0,l=touches.length;i<l;i++){
            if(touches[i]!=null){
                ctx.fillStyle=COLORS[i%COLORS.length];
                ctx.fillRect(touches[i].x-10,touches[i].y-10,20,20);
                ctx.fillText('ID: '+i+' X: '+touches[i].x+' Y: '+touches[i].y,10,10*i+20);
            }
        }
    }

    function enableInputs(){
        canvas.addEventListener('touchstart',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                var x=~~((t[i].pageX-canvas.offsetLeft)*scaleX);
                var y=~~((t[i].pageY-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].pageX-canvas.offsetLeft)*scaleX);
                    touches[t[i].identifier%100].y=~~((t[i].pageY-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.pageX-canvas.offsetLeft)*scaleX);
            var y=~~((evt.pageY-canvas.offsetTop)*scaleY);
            touches[0]=new Vtouch(x,y);
        },false);
        document.addEventListener('mousemove',function(evt){
            if(touches[0]){
                touches[0].x=~~((evt.pageX-canvas.offsetLeft)*scaleX);
                touches[0].y=~~((evt.pageY-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;
        }
    }

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

19 comentarios:

  1. Hola. He estado siguiendo este tutorial de pantalla completa y me funciona muy bien el escalado para la pantalla del navegador, pero para móviles (celular Android, celular Firefox-OS) a veces funciona bien a veces no, y el que me da problemas es el -scaleX- y -scaleY- ya que a veces si da la posición precisa y a veces no (me pone el puntero muy abajo y muy a la derecha, osea la escala no es correcta). ¿A qué se deberá esto? Te anexo mi función para cambiar el tamaño de la pantalla y la de inicialización para ver en que podría estar mal:

    function init() {
    canvas=document.getElementById("canvas");
    canvas.style.background="#000";
    contexto=canvas.getContext("2d");
    resize(2);
    enableInputs();
    run();
    repaint();
    }//fin del init

    function resize(parametro){
    if (parametro==1){
    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;
    }
    }

    Intuyo el problema puede ir en obtener la escala en -X- y en -Y-, que no lo hace bien al momento de leer los valores, pues a veces si funciona a veces no, y cuando no funciona es porque mueve demasiado el puntero del ratón hacia la derecha y hacia abajo, pero no logro darle por donde está el problema.

    Ojalá puedas ayudarme y muchas gracias. Me encantó tu curso.

    ResponderBorrar
    Respuestas
    1. Recuerdo de un problema similar antes, causado por que la pagina era mas grande que la pantalla, y este se movía en el fondo. Necesitaría ver un ejemplo que tengas para ver si es este u otro el problema que tienes, y darte una solución. ¿Tienes tu juego en línea en algún lado para ver esta posibilidad?

      Borrar
    2. Hola. Una disculpa por el retraso, la URL:

      http://proyectoseducativosandroid.hol.es/apps-html-5/01-viborita-puro-js/003-pruebaViborita.html

      Como mencioné antes, el problema es que a veces si carga bien la posición del ratón y a veces se desfasa mucho la escala (-scaleX- -scaleY-) y no se por que. Gracias.

      Borrar
    3. Una disculpa, por si no se puede ver el código fuente, te dejo el mismo pero no en pantalla completa:

      http://proyectoseducativosandroid.hol.es/apps-html-5/01-viborita-puro-js/004-pruebaViborita.html

      Gracias de antemano.

      Borrar
    4. He notado que no has incluido la etiqueta que inicia la pantalla en tamaño real en los móviles:

      <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />

      Si ves ahora, al hacer doble toque sobre la pantalla, esta se expande a su tamaño real, y el enfoque relativo del toque se pierde de nuevo. Ve si esta etiqueta resuelve el problema de forma definitiva y avísame del resultado. ¡Éxito!

      Borrar
    5. Mi estimado, eres grande. He hecho ya varias pruebas, con varios dispositivos y en todos ya funciona bien. Seguiré probando, pero hasta el momento ha solucionado (al menos con mis pruebas) lo solicitado. Muchísimas gracias, muy buen curso y excelentes comentarios y respuestas.

      Borrar
  2. Ah por cierto, una posible solución que medio he encontrado (y que a veces hace que si funcione más veces) es meter un -alert- antes de cambiar el tamaño (invocar el -resize-) y pues esto hace que se ponga correctamente la escala la mayoría de las veces.

    Igual lo que hice fue instalar Firefox en Android y ocupar el mismo truco del -alert- y la mayoría de las veces ya funciona mejor, ¿por qué pasará esto?. Gracias nuevamente.

    ResponderBorrar
  3. oye y para cuando el canvas es mas grande que la pantalla, no dara problemas, veras en mi pc funciona correcto y en mi tablet (note 8) sigue la imprecision.

    ResponderBorrar
    Respuestas
    1. Precisamente esta función permite que lienzos muy grandes se escalen a pantalla completa de dispositivos pequeños como tablets y smartphones. ¿Que problema de imprecisión estás teniendo?

      Borrar
  4. mmm, parece q ya lo solucione, sin embago hay un asunto serio con los toues, no se xq me pasa pero tengo 3 botones en pantalla, cuando uno de ellos esta siendo tocado y quiero tocar otro boton no se obtiene el toque sobre ese boton, te ha sucedido, has probado con mas de un boton a la vez siendo tocado ?

    ResponderBorrar
    Respuestas
    1. ¿Has implementado el multi-toque correctamente? ¿Puedes ver más de un toque en el ejemplo de esta página?

      Si todo eso funciona, no debería haber conflicto con usar más de un botón a la vez.

      Borrar
  5. A mi me esta sucediendo el mismo problema que al del primer comentario y a mi esa solución no me funciona, la url de mi juego es: http://keiapps.esy.es/Juegos/SamaelQ/Index.html

    ResponderBorrar
    Respuestas
    1. Estoy analizando tu código y me surgen varias dudas...

      Para empezar, ¿Por qué divides la el ancho y alto entre 500 para asignarlo a la escala? Presiento que el problema podría tener por aquí sus orígenes...

      Borrar
    2. lo de 500 fue un intento para solucionar el herror ya que tanto el ancho como el alto del canvas original es de 500 asi que para comprovar si el herror venia de ahi lo cambie.

      Borrar
    3. Creo que ya se cual puede ser el problema... No estoy seguro, pero es posible que el tamaño móvil no se ejecute hasta después de haber cargado el sitio. Prueba poner el código que obtiene la escala dentro de la función resize, y no olvides además agregar este escucha:

      window.addEventListener('resize',resize,false);

      Avísame si eso corrige el problema.

      Borrar
  6. Bueno, esa solucion no me ha funcionado, pero provando he decubierto que al dividir las cordenadas del toque por la escala no hay error. Pero gracias por la ayuda.

    ResponderBorrar
    Respuestas
    1. Me extraña que no te haya funcionado, por que yo puedo jugar ahora perfectamente tu juego desde mi celular... ¿Será entonces que el tuyo requiere algo diferente? ¿Qué modelo estás usando?

      Borrar
    2. bueno he aclarado que he encontrado que no se por que en los eventos touch en vez de multiplicar por la escala si lo divido no me sale el error, ademas este error se producia igualmente en pc asi que el modelo no creo que intervenga. pero gracias por tu tiempo

      Borrar
    3. ¡Ah! Temo que no había comprendido tu respuesta.

      Y sobre el error, es que tu obtienes la escala dividiendo la ventana entre el lienzo, mientras en el ejemplo presente se hace de forma inversa. Por lo mismo, el touch actúa de forma inversa también.

      Borrar