Aprende a crear juegos en HTML5 Canvas

domingo, 1 de septiembre de 2013

Multi-toque

Previamente habíamos aprendido como dar soporte básico a los dispositivos multi-toque, esto es, simular el ratón dentro de un dispositivo de toque. En esta ocasión, aprenderemos a usar su potencial completo soportando varios toques a la vez en la pantalla, y para pruebas de escritorio, como simularlo con el ratón.

Utilizaremos una plantilla de juego en blanco con las funciones que ya conocemos. Comenzaremos declarando un arreglo donde guardaremos nuestros toques:
    var touches=[];
Usaremos de nuevo la función enableInputs en la función init. El primer input que veremos, es el touchstart:
        canvas.addEventListener('touchstart',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                var x=t[i].pageX-canvas.offsetLeft;
                var y=t[i].pageY-canvas.offsetTop;
                touches[t[i].identifier%100]=new Point(x,y);            }
        },false);
En la primer línea, asignamos a una variable t todos los toques nuevos. Normalmente suele ser uno por llamada al escucha, pero en el caso que ocurra más de uno, no querremos perder ningún toque, así que leeremos todos a través de un for.

De cada toque, obtendremos su posición en X y en Y como lo hemos hecho desde antes, y almacenaremos en la posición de identificador, los valores de sus coordenadas mediante un objeto de tipo Point, el cual está declarado de la siguiente forma:
    function Point(x,y){
        this.x=x||0;
        this.y=y||0;
    }
Para eliminar el punto cuando el toque termine, simplemente asignamos nulo a su posición:
        canvas.addEventListener('touchend',function(evt){
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                touches[t[i].identifier%100]=null;
            }
        },false);
No olvides hacer lo mismo para el escucha de touchcancel.

Finalmente, para el movimiento del toque, asignamos su nueva posición en la posición de su identificador:
        canvas.addEventListener('touchmove',function(evt){
            evt.preventDefault();
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                touches[t[i].identifier%100].x=t[i].pageX-canvas.offsetLeft;
                touches[t[i].identifier%100].y=t[i].pageY-canvas.offsetTop;
            }
        },false);
Con esto, tendremos un arreglo que nos almacenará todos los toques en pantalla en dispositivos multi-toque.

Quizá hayas notado con curiosidad que el identificador ha sido obtenido de su módulo de 100. Esto se debe a un comportamiento peculiar de Safari Móvil, pues mientras los demás navegadores suelen reciclar los identificadores, Safari crea uno nuevo por cada nuevo toque hasta llegar a números desorbitantes, que causan un error al intentar almacenarlo en el arreglo. La solución que hemos implementado, previene ese error, aunque podría causar un efecto curioso si se dan más de 100 toques sin separar un toque anterior de la pantalla... Aunque esto es poco probable en la mayoría de los juegos.

Pasa simular los toques con el ratón, tendremos que hacer los mismos pasos, saltando la parte del for, y asignando todos estos valores a la posición 0 del arreglo, porque solo tenemos un puntero con el mouse.

Hay que verificar además en al mover el ratón, si el toque existe en el arreglo, ya que a diferencia de los toques, este registra movimiento aun cuando no esté siendo presionado uno de sus botones:
        canvas.addEventListener('mousedown',function(evt){
            evt.preventDefault();
            var x=evt.pageX-canvas.offsetLeft;
            var y=evt.pageY-canvas.offsetTop;
            touches[0]=new Point(x,y);
        },false);
        document.addEventListener('mousemove',function(evt){
            if(touches[0]){
                touches[0].x=evt.pageX-canvas.offsetLeft;
                touches[0].y=evt.pageY-canvas.offsetTop;
            }
        },false);
        document.addEventListener('mouseup',function(evt){
            touches[0]=null;
        },false);
Ahora que ya tenemos nuestros toques registrados, pasaremos a dibujarlos para ver que efectivamente están ahí. Para diferenciar los distintos toques, he creado un arreglo con los cuatro colores básicos:
    var COLORS=['#f00','#0f0','#00f','#fff'];
Para dibujarlos, lo hacemos mediante un for, verificando si existe cada toque antes de intentar pintarlo:
        for(var i=0,l=touches.length;i<l;i++){
            if(touches[i]){
                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);
            }
        }
El la primer línea, asignamos el color correspondiente; lo hacemos con un módulo de COLORS.length, así, si hay más de cuatro toques, se asignará al siguiente el primero de nuevo, y de esta forma, se podrán dibujar infinitos toques (al menos, tantos como soporte el navegador).

En la segunda, dibujamos el toque como un rectángulo de 20x20 pixeles. Y por último, dibujamos una línea de texto que nos reporte su id, y su posición en X y en Y.

Probamos el código en un dispositivo multitoque, y podremos ver como los distintos toques son dibujados en nuestra pantalla.

Codigo Final

[Canvas not supported by your browser]

(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var touches=[];
    var COLORS=['#f00','#0f0','#00f','#fff'];

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

    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]){
                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;
                var y=t[i].pageY-canvas.offsetTop;
                touches[t[i].identifier%100]=new Point(x,y);
            }
        },false);
        canvas.addEventListener('touchmove',function(evt){
            evt.preventDefault();
            var t=evt.changedTouches;
            for(var i=0;i<t.length;i++){
                touches[t[i].identifier%100].x=t[i].pageX-canvas.offsetLeft;
                touches[t[i].identifier%100].y=t[i].pageY-canvas.offsetTop;
            }
        },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;
            var y=evt.pageY-canvas.offsetTop;
            touches[0]=new Point(x,y);
        },false);
        document.addEventListener('mousemove',function(evt){
            if(touches[0]){
                touches[0].x=evt.pageX-canvas.offsetLeft;
                touches[0].y=evt.pageY-canvas.offsetTop;
            }
        },false);
        document.addEventListener('mouseup',function(evt){
            touches[0]=null;
        },false);
    }

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

17 comentarios:

  1. muchas gracias , me sirvio bastante aunq hice unas mod

    ResponderEliminar
    Respuestas
    1. Como siempre, yo solo entrego las bases para que ustedes puedan seguir adelante. Claro, si encuentras algo que consideres importante para el conocimiento de los demás, siempre agradecemos la colaboración de nuestros seguidores.

      Me alegra esta entrega haya sido de ayuda para ti.

      Eliminar
  2. y para ejecutar una funcion con respecto a la posicion del toque como se haria, ya vi e implemente el tuto "botones en pantalla" pero yo deseo es que los botones aparezcan solo cuando el videojugador toque la pantalla, para eso necesito filtrar los toques por su ubicacion, ?? q pena si te pido algo molesto, en todo caso te agreedceria si me puedes ayudar

    ResponderEliminar
    Respuestas
    1. En el for, el valor de "i" es su ID, asi que cuando se hace un "lastPress==1", pones el botón en la posición que deseas, y haces todo relativo al "i" adecuado. Cuando el botón "i" sea nulo, entonces sacas el botón fuera de pantalla, desapareciendolo asi de esta forma.

      Espero eso te ayude. ¡Suerte y felices códigos!

      Eliminar
  3. Hola, sabes como compilar un juego html5 en una app para android?

    Saludos

    ResponderEliminar
    Respuestas
    1. La solución mas simple es empaquetar el juego en un WebView para que corra en una app. Hay programas que crean hibridos como Phonegap, o que optimizan el código (Dicen a velocidades nativas) como CocoonJS, pero hasta donde sé, no hay al día de hoy una aplicación capaz de compilar un Javascript a código nativo de Android.

      Espero esto te de una idea clara para que puedas portar tu juego a móviles. ¡Exito!

      Eliminar
  4. Mub buenos tutoriales, gracias por todo el trabajazo que te estas currando, me están sirviendo mucho.
    Una pregunta tengo, como hacer para proteger el código de un juego?, si esta en JavaScript, no es accesible a través del código fuente?
    Muchas gracias por todo

    ResponderEliminar
    Respuestas
    1. Lo mejor que puedes hacer al respecto es usar un compilador js, que hace complejo el código a ser leído y manipulado con facilidad. Es solo una capa de protección, que en realidad es comparable a lo que hacen los demás compiladores, pero lo hará menos propenso a una clonación, además de hacerlo más ligero al cargar.

      Eliminar
  5. Muchas gracias por todo. Solo una cosa, la gente que publica juegos utilizando el canvas, lo hace como tu dices con un compilador, o hay alguna otra manera mas segura?, muchas gracias

    ResponderEliminar
    Respuestas
    1. Esta es la medida de seguridad mas popular. En Google puedes encontrar mas técnicas, pero lo que he aprendido es que al final, ningun código es 100% seguro.

      Eliminar
  6. Muchas gracias Karl, eres muy grande

    ResponderEliminar
  7. Me has enseñado más en unas semanas que mis profesores en un módulo superior de 2 años. muchas gracias

    ResponderEliminar
  8. Buenas...
    Escribi en otro post, pero creo que es mas acertado por aqui!..
    He estado probando algunas cosas y quisiera saber como se podría hacer para dibujar una linea haciendo uso del toque(por ejemplo un trazado, al estilo de fruit ninja). He visto con tu ejemplo que hace un fill de rectangulo, pero este desaparece, tengo dudas en como se puede dejar ese recuadro o linea, como si fuera un trazado...

    Espero haber expresado bien mi duda...

    Gracias...

    ResponderEliminar
    Respuestas
    1. He revisado y respondido antes tu pregunta en la otra entrada, espero la respuesta te ayude a conseguir lo que buscas. ¡Suerte!

      Eliminar
    2. Gracias por la ayuda..
      Si en parte de lo que queria me sirve con tu consejo.. ya lo puse en practica y me ha funcionado.. no se me habia ocurrido lo del arreglo!!..

      Han sido muy buenos tus tutoriales y valen la pena!

      Eliminar
  9. Hola... escribo de nuevo..

    Con respecto a lo que me mencionaste de un arreglo, se me ha complicado para dibujar linea, por el hecho que toca realizar primero moveTo, en cambio si dibujo un rectangulo es mas facil ya que solo se haria en la posición x e y del arreglo[i]...

    Claro el detalle está que no quiero que se pierda el trazo anterior... para otro caso que hice en el que si queria que se borraran posiciones anteriores si me funcionó(pero usando imagen, drawImage)

    Saludos!

    ResponderEliminar
    Respuestas
    1. No, AshMola Naoe Li Pushlab, Naoe Masloka Enlova Naoe Pushlabia Hersmatoka Poshka Islaova Hersmellen Naomalao Min, Minao Shakamolae.

      Shakamolae Nios á Hérmástélén Nio Mao.

      Shao!!!

      Eliminar