Aprende a crear juegos en HTML5 Canvas

domingo, 19 de enero de 2014

Gravedad

Gravedad. Esa fuerza que jala a todos los objetos hacia el centro de un objeto de alta densidad, como es el caso de los inmensos planetas en el universo. Esta fuerza se da por una aceleración que depende de la masa del planeta, y en el caso de la tierra, sabemos que esta aceleración equivale (aproximadamente) a 9.8 metros sobre segundo cuadrado.

Si deseamos crear juegos con una perspectiva lateral, como es el caso de los populares juegos de plataforma, necesitaremos saber como aplicar gravedad sobre los objetos, así que dedicaremos esta entrada a conocer como hacer esto con profundidad.

En realidad no se trata de una ciencia mayor dentro de la programación, y ya hemos visto previamente como tratar con la aceleración, por lo que aplicarle de forma vertical a un objeto no será un problema mayor. En aquella ocasión vimos que para aplicar una aceleración, necesitamos de nuestro objeto, su velocidad (Que ahora será vertical) y la constante de aceleración. Sabemos que en la tierra la aceleración es de 9.8 metros sobre segundo cuadrado, ¿Pero cuanto miden nuestros objetos en el juego?

Supongamos para este caso que nuestro rectángulo de 10 pixeles cuadrados es una caja de un metro cuadrado. Por tanto, nuestra aceleración será de 98 pixeles sobre segundo cuadrado. Dado que actualmente implementamos un regulador de tiempo con actualización cada 50 milisegundos, este valor hay que multiplicarlo por 50/1000 (0.05) para obtener su velocidad en pixeles sobre segundo:
     var K = 98 * 0.05,

        player = null,
        speed = 0;

        player = new Rectangle2D(50, 20, 10, 10);
Dentro de las acciones, habremos de aplicar la aceleración a la velocidad y posteriormente aplicaremos dicha velocidad sobre nuestro personaje. Finalmente, cuando nuestro objeto toque tierra (Que será el borde inferior del lienzo), lo acomodaremos en la posición justo por encima del suelo, y dado que la normal del suelo ahora compensa la aceleración de la gravedad, regresaremos dicho valor a cero:
        // Accelerate
        speed += K;

        // Apply gravity
        player.y += speed;
       
        // Ground
        if (player.bottom > canvas.height) {
            player.bottom = canvas.height;
            speed = 0;
        }
Al final de esta entrada he agregado un ejemplo interactivo que muestra el resultado de este código. Para poder repetir el experimento, he agregado que al hacer clic sobre el lienzo, su posición en el eje Y sea igual al del ratón, para experimentar su caída desde distintas alturas.

Al ejecutar el ejemplo, notaremos como la gravedad actúa sobre la caja. Esta acelera demasiado veloz y cae muy rápido al suelo; quizá efectivo para simulaciones realistas, pero definitivamente es una aceleración muy fuerte para un videojuego. ¿Se imaginan jugar con un personaje que caiga así de rápido de regreso al suelo?

Tomando esto en cuenta, quizá sea necesario trasladar nuestra caja a un planeta con menor gravedad. ¿Podría ser más efectivo hacer nuestro juego en la Luna? Allá la constante gravitacional es de 1.6 metros sobre segundo cuadrado, esto es, 16 pixeles sobre segundo cuadrado en nuestro ejemplo:
     var K = 16 * 0.05,
Probamos el código, y notaremos una caída mucho más agraciada, perfectamente controlable; se nota esa caída suave que tienen los astronautas en los videos. Para nuestro juego, podemos usar un número entero para evitar los decimales:
     var K = 1;
Dado que nuestro juego va a 20 ciclos por segundo, esto quiere decir que esta constante equivale a 20 pixeles sobre segundo cuadrado (2 metros sobre segundo cuadrado de acuerdo a nuestros cálculos). Comprobamos que es aun una velocidad bastante agradable y maniobrable, apenas poco mas rápido que la caída en la Luna.

Usando la delta de tiempo.


En el caso de querer tener física de alta precisión en tiempo real, se puede usar la delta de tiempo como hemos visto anteriormente. Para ello, la constante de gravedad debe permanecer en pixeles por segundo:
     var K = 98;
Y a la velocidad se le debe sumar la constante multiplicada por la delta de tiempo pasada en el lapso del ciclo pasado al actual:
        // Accelerate
        speed += K * deltaTime;
Esta técnica nos permitirá manejar la gravedad con mayor fluidez y precisión para simulaciones realistas.

Sin embargo, dado que la delta del tiempo no es constante entre cada ciclo, esto puede causar conflictos en casos donde los movimientos deban ser precisos y constantes. Este es el caso de los juegos de plataformas, por que ejecutar el mismo salto dos veces, y el poder alcanzar una plataforma alta en ciertas ocasiones y en otras no, puede frustrar al jugador y marcar la diferencia entre poder pasar un nivel difícil o no. Todo por acto de la suerte.

Es por eso que para este ejemplo, no implementaremos la técnica de la delta de tiempo, pues necesitamos movimientos precisos y constantes en los juegos de plataformas, los cuales aprenderemos a crear a partir de la próxima semana.

Código final:

[Canvas not supported by your browser]
/*jslint bitwise: true, es5: true */
(function (window, undefined) {
    'use strict';
    var K1 = 98 * 0.05,
        K2 = 16 * 0.05,
        K3 = 1,
        
        canvas = null,
        ctx = null,
        lastPress = null,
        mousex = 0,
        mousey = 0,
        player1 = null,
        player2 = null,
        player3 = null,
        speed1 = 0,
        speed2 = 0,
        speed3 = 0;
    
    function Rectangle2D(x, y, width, height, createFromTopLeft) {
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
        if (createFromTopLeft) {
            this.left = (x === undefined) ? 0 : x;
            this.top = (y === undefined) ? 0 : y;
        } else {
            this.x = (x === undefined) ? 0 : x;
            this.y = (y === undefined) ? 0 : y;
        }
    }
    
    Rectangle2D.prototype = {
        left: 0,
        top: 0,
        width: 0,
        height: 0,
        
        get x() {
            return this.left + this.width / 2;
        },
        set x(value) {
            this.left = value - this.width / 2;
        },
        
        get y() {
            return this.top + this.height / 2;
        },
        set y(value) {
            this.top = value - this.height / 2;
        },
        
        get right() {
            return this.left + this.width;
        },
        set right(value) {
            this.left = value - this.width;
        },
        
        get bottom() {
            return this.top + this.height;
        },
        set bottom(value) {
            this.top = value - this.height;
        },
        
        intersects: function (rect) {
            if (rect !== undefined) {
                return (this.left < rect.right &&
                    this.right > rect.left &&
                    this.top < rect.bottom &&
                    this.bottom > rect.top);
            }
        },
        
        fill: function (ctx) {
            if (ctx !== undefined) {
                ctx.fillRect(this.left, this.top, this.width, this.height);
            }
        }
    };

    function enableInputs() {
        document.addEventListener('mousemove', function (evt) {
            mousex = evt.pageX - canvas.offsetLeft;
            mousey = evt.pageY - canvas.offsetTop;
        }, false);
        canvas.addEventListener('mousedown', function (evt) {
            lastPress = evt.which;
        }, false);
    }

    function paint(ctx) {
        // Clean canvas
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw players
        ctx.fillStyle = '#0f0';
        player1.fill(ctx);
        player2.fill(ctx);
        player3.fill(ctx);

        // Debug last key pressed
        ctx.fillStyle = '#fff';
        //ctx.fillText('Last Press: ' + lastPress, 0, 20);
        
        // Debug speeds
        ctx.fillText('Speed1: ' + speed1, 0, 10);
        ctx.fillText('Speed2: ' + speed2, 100, 10);
        ctx.fillText('Speed3: ' + speed3, 0, 20);
    }

    function act() {
        // Accelerate
        speed1 += K1;
        speed2 += K2;
        speed3 += K3;

        // Apply gravity
        player1.y += speed1;
        player2.y += speed2;
        player3.y += speed3;

        // Ground
        if (player1.bottom > canvas.height) {
            player1.bottom = canvas.height;
            speed1 = 0;
        }
        if (player2.bottom > canvas.height) {
            player2.bottom = canvas.height;
            speed2 = 0;
        }
        if (player3.bottom > canvas.height) {
            player3.bottom = canvas.height;
            speed3 = 0;
        }

        // Reset position
        if (lastPress === 1) {
            player1.y = mousey;
            player2.y = mousey;
            player3.y = mousey;
        }
    }

    function repaint() {
        window.requestAnimationFrame(repaint);
        paint(ctx);
    }

    function run() {
        setTimeout(run, 50);
        act();
        
        lastPress = null;
    }

    function init() {
        // Get canvas and context
        canvas = document.getElementById('canvas');
        ctx = canvas.getContext('2d');
        canvas.width = 300;
        canvas.height = 200;
        
        // Create players
        player1 = new Rectangle2D(50, 20, 10, 10);
        player2 = new Rectangle2D(150, 20, 10, 10);
        player3 = new Rectangle2D(250, 20, 10, 10);

        // Start game
        enableInputs();
        run();
        repaint();
    }

    window.addEventListener('load', init, false);
}(window));

5 comentarios:

  1. Y SI USARAS DELTA EL RESULTADO NO SERIA MAS REALISTA ?...

    ResponderEliminar
    Respuestas
    1. Técnicamente, speed es la delta de la velocidad pasada sumada a su aceleración constante, así que es el mismo resultado. Quizá renombrar "speed" a "delta" sea más adecuado en términos de física.

      Eliminar
  2. NO ME REFIERO A DIGAMOS EL "GAMELOOP" COMO TU TUTORIAL DE EL AJUSTE AL REQUESTANIMATIONFRAME,,, PASARLE DELTA COMO PARAMETRO A LA FUNCION QUE ACTUALIZA DIGO PARA PLATAFORMAS Y CUALQUIERA Y CLARO RENDIMIENTO ES GENIAL...

    ResponderEliminar
    Respuestas
    1. ¡Ah! ¡El deltaTime! En este momento estamos regulando el tiempo con otra técnica, que nos da un tiempo delta constante de 20 por ciclo, por lo que no debemos preocuparnos de eso. Pero si quisiéramos física más realística y en tiempo real, el tiempo delta es una técnica que debe ser implementada, aunque un poco complicada.

      Sin embargo, en el caso de los juegos de plataformas, el resultado es contraproducente, ya que no es constante (Ya lo he comprobado), causando conflictos como el hecho que un mismo salto a la misma altura, haga que el personaje a veces alcance una plataforma elevada, y a veces no lo logre. ¿¡Te imaginas eso en un juego!? Los jugadores se quejarán "¡Pero si salté bien!", y tendrán razón... Pero la inconstancia del delta de tiempo puede causar esta clase de conflictos en juego de física no realística, por lo que es mejor simularla en lugar de implementarla como realmente corresponde.

      Ahora que escribo esto, me doy cuenta de lo importante que es, así que agregaré esta información a la entrada. Muchas gracias por tu pregunta, y espero esta respuesta aclare tus dudas.

      Eliminar
  3. SI A ESO ME REFERIA, Y SI ES UN POCO MAS COMPLICADO PERO SOBRE LOS SALTOS QUE DICE SOLO ES CUESTION DE HACER COMPROBACION EN INTERVALOS DE TIEMPO PARA HACER SALTOS DOBLE SALTO ETC, CLARO AGREGA MAS COMPLEJIDAD YA LO HE VIVIDO JJ ¬¬. ESPERANDO POR EL SGTE POST :)

    ResponderEliminar