Aprende a crear juegos en HTML5 Canvas

domingo, 21 de julio de 2013

Soporte básico a dispositivos multi-toque

Una de las más grandes ventajas de HTML5 Canvas sobre otros programas para juegos web como Flash, es que los dispositivos multi-toque como móviles y tabletas soportan su uso. Sin embargo, si para este momento has intentado jugar uno de los ejemplos que hace uso de ratón en un dispositivo multi-toque, notarás que su comportamiento no es el esperado.

Debido a que los dispositivos multi-toque simulan el comportamiento del ratón para las tareas mas básicas (Comunmente ejecutan todos los comandos en conjunto al dejar de presionar el toque), es necesario hacer nuestro propio soporte para simular el comportamiento en el juego tal como es deseado. No te preocupes de ello, en realidad es muy sencillo.

Comenzaremos agregando esta línea a nuestro html, justo después de la línea <meta charset="UTF-8" />:
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
¿Has notado que normalmente las páginas inician con un alejamiento en los móviles para que podamos apreciar la página completa? Con esta línea, indicamos al navegador móvil que nuestra página está diseñada para móviles, y que deseamos que su escala inicial sea el original. Esto se hace con los tres parámetros enviados mediante content:

width=device-width indica que queremos que la página tenga el ancho completo de nuestro dispositivo móvil, y no el ancho simulado para dar el aspecto de ver una página completa.

initial-scale=1.0 indica quequeremos que la página tenga como zoom inicial el original, y no el zoom-out que suele hacer a los sitios comunes.

Por último, user-scalable=no indica que el usuario no podrá hacer acercamiento o alejamiento a la página. Esto es para soportar el multi-toque, ya que de forma predeterminada, el muti-toque se usar para hacer dicho acercamiento y alejamiento. Por ahora podemos dejar este último parámetro como opcional, ya que solo soportaremos un toque para simular el ratón, pero si deseas soportar multi-toque, es importante que agregues este parámetro también.

Ahora que nuestro sitio ya está listo para ser visto en móviles, pasemos a agregar el código para simular los eventos de ratón. Tomaré el reciente ejemplo de Arrastra y Suelta para este código.

Comenzamos agregado la simulación de movimiento en la función enableInputs, justo después del agregar el escucha al canvas de presionar el ratón:
        canvas.addEventListener('touchmove', function (evt) {
            evt.preventDefault();
            var t = evt.targetTouches;
            mouse.x = t[0].pageX - canvas.offsetLeft;
            mouse.y = t[0].pageY - canvas.offsetTop;
        }, false);
Como podrás ver, en esencia, touchmove es lo mísmo que mousemove. La única diferencia es que primero debemos de obtener todos los toques en pantalla con var t = evt.targetTouches;. Posteriormente, usamos la posición del primer toque para asignar la posición a la variable del ratón de igual forma que en mousemove.

Continuamos analizando la simulación de presión del botón de ratón cuando el toque inicia:
        canvas.addEventListener('touchstart', function (evt) {
            evt.preventDefault();
            lastPress = 1;
            var t = evt.targetTouches;
            mouse.x = t[0].pageX - canvas.offsetLeft;
            mouse.y = t[0].pageY - canvas.offsetTop;
        }, false);
Con touchstart, se hace exactamente la misma acción que con mousedown, asignando a lastPress el valor de 1, pues solo nos interesa simular si el botón derecho ha sido presionado. Pero además se agregan las acciónes de touchmove. ¿La razón? A diferencia del ratón, los toques no están en constante movimiento; solo ocurren cuando los accionamos. Por eso debemos averiguar la posición inicial del primer toque desde el momento que se inicia, pues de lo contrario su coordenada inicial sería (null, null), y podría realizar acciones no deseadas (Como no detectar que nuestro toque ha sido iniciado sobre un objeto específico).

Por último, agregaremos la simulación de liberación del botón de ratón cuando el toque termina:
        canvas.addEventListener('touchend', function (evt) {
            lastRelease = 1;
        }, false);
        
        canvas.addEventListener('touchcancel', function (evt) {
            lastRelease = 1;
        }, false);
Notarás que también agregamos la acción a touchcancel. En caso de que el dispositivo regrese un error con el toque, touchcancel será llamado, y para evitar que la acción de toque se quede pegada en un caso así, es importante agregar las acciónes de touchend a este escucha como mínimo, en caso que no se especifique alguna acción especial para un caso así.

Prueba ahora este ejemplo en dispositivos multi-toque. ¡Verás que el ejemplo funciona ahora de maravilla en ellos! Con este conocimiento, ahora podrás adaptar todos tus juegos que hagan uso del ratón para dispositivos multi-toque también.

Apéndice: Eventos de toque.

A continuación dejo una lista completa de los eventos de toque oficiales, para que conserves como referencia:
touchmove
Se ejecuta cuando un toque se mueve.
touchstart
Se ejecuta cuando un toque es iniciado.
touchend
Se ejecuta cuando un toque es liberado.
touchcancel
Se ejecuta cuando un toque es cancelado. Si no tienes un comportamiento especial para esta acción, es recomendado que se asigne el mismo que al liberar un toque, para evitar posibles errores en caso de que un toque sea cancelado.

Código final:

[Canvas not supported by your browser]
/*jslint bitwise: true, es5: true */
(function (window, undefined) {
    'use strict';
    var canvas = null,
        ctx = null,
        lastPress = null,
        lastRelease = null,
        mouse = {x: 0, y: 0},
        pointer = {x: 0, y: 0},
        dragging = null,
        draggables = [],
        i = 0,
        l = 0;
    
    function Circle(x, y, radius) {
        this.x = (x === undefined) ? 0 : x;
        this.y = (y === undefined) ? 0 : y;
        this.radius = (radius === undefined) ? 0 : radius;
    }
    
    Circle.prototype.distance = function (circle) {
        if (circle !== undefined) {
            var dx = this.x - circle.x,
                dy = this.y - circle.y,
                circleRadius = circle.radius || 0;
            return (Math.sqrt(dx * dx + dy * dy) - (this.radius + circleRadius));
        }
    };
    
    Circle.prototype.fill = function (ctx) {
        if (ctx !== undefined) {
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
            ctx.fill();
        }
    };
    
    function enableInputs() {
        document.addEventListener('mousemove', function (evt) {
            mouse.x = evt.pageX - canvas.offsetLeft;
            mouse.y = evt.pageY - canvas.offsetTop;
        }, false);
        
        document.addEventListener('mouseup', function (evt) {
            lastRelease = evt.which;
        }, false);
        
        canvas.addEventListener('mousedown', function (evt) {
            evt.preventDefault();
            lastPress = evt.which;
        }, false);
        
        canvas.addEventListener('touchmove', function (evt) {
            evt.preventDefault();
            var t = evt.targetTouches;
            mouse.x = t[0].pageX - canvas.offsetLeft;
            mouse.y = t[0].pageY - canvas.offsetTop;
        }, false);
        
        canvas.addEventListener('touchstart', function (evt) {
            evt.preventDefault();
            lastPress = 1;
            var t = evt.targetTouches;
            mouse.x = t[0].pageX - canvas.offsetLeft;
            mouse.y = t[0].pageY - canvas.offsetTop;
        }, false);
        
        canvas.addEventListener('touchend', function (evt) {
            lastRelease = 1;
        }, false);
        
        canvas.addEventListener('touchcancel', function (evt) {
            lastRelease = 1;
        }, false);
    }
    
    function random(max) {
        return ~~(Math.random() * max);
    }

    function paint(ctx) {
        // Clean canvas
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Draw circles
        ctx.fillStyle = '#00f';
        for (i = 0, l = draggables.length; i < l; i += 1) {
            draggables[i].fill(ctx);
        }
        
        // Debug pointer position
        ctx.fillStyle = '#0f0';
        ctx.fillRect(pointer.x - 1, pointer.y - 1, 2, 2);
        
        // Debug dragging circle
        ctx.fillStyle = '#fff';
        ctx.fillText('Dragging: ' + dragging, 0, 10);
    }
        
    function act() {
        // Set pointer to mouse
        pointer.x = mouse.x;
        pointer.y = mouse.y;
        
        // Limit pointer into canvas
        if (pointer.x < 0) {
            pointer.x = 0;
        }
        if (pointer.x > canvas.width) {
            pointer.x = canvas.width;
        }
        if (pointer.y < 0) {
            pointer.y = 0;
        }
        if (pointer.y > canvas.height) {
            pointer.y = canvas.height;
        }
        
        if (lastPress === 1) {
            // Check for current dragging circle
            for (i = 0, l = draggables.length; i < l; i += 1) {
                if (draggables[i].distance(pointer) < 0) {
                    dragging = i;
                    break;
                }
            }
        } else if (lastRelease === 1) {
            // Release current dragging circle
            dragging = null;
        }
        
        // Move current dragging circle
        if (dragging !== null) {
            draggables[dragging].x = pointer.x;
            draggables[dragging].y = pointer.y;
        }
    }

    function run() {
        window.requestAnimationFrame(run);
        act();
        paint(ctx);
        
        lastPress = null;
        lastRelease = null;
    }
    
    function init() {
        // Get canvas and context
        canvas = document.getElementById('canvas');
        ctx = canvas.getContext('2d');
        canvas.width = 200;
        canvas.height = 300;
        
        // Create draggables
        for (i = 0; i < 5; i += 1) {
            draggables.push(new Circle(random(canvas.width), random(canvas.height), 10));
        }
        
        // Start game
        enableInputs();
        run();
    }
    
    window.addEventListener('load', init, false);
}(window));

3 comentarios:

  1. pero como le haces para mas de un toque

    entiendo que tratas el primero toque con la posicion [0] del array de toques

    mousex=t[0].pageX-canvas.offsetLeft;
    mousey=t[0].pageY-canvas.offsetTop;

    y para el segundo digamos un juego con palanca a la izquierda y disparo a la derecha

    como se haria para que funcione al mismo tiempo?

    ya loqgre hacerlos funcionar por separado con un code similary dependiendo de la posicin en x mueve la palanca a la izquierda; pausa el juego en el area central o dispara en la derecha, pero NO he podido con el bendito multitoque simultaneo

    ResponderBorrar
    Respuestas
    1. Curioso que lo preguntes, justo esta semana empezaba con este tema, y precisamente la primer entrega trata ese tema. Te dejo el enlace para que puedas ver como hacerlo: http://juegoscanvas.blogspot.mx/2013/09/multi-toque.html

      Borrar
  2. muchas gracias, voy a mirarlo detalladamente y te cuento si pude

    ResponderBorrar