Aprende a crear juegos en HTML5 Canvas

lunes, 18 de marzo de 2013

Objetos sólidos

Una pregunta que hacen con frecuencia, es la de como crear objetos sólidos, es decir, que al chocar contra ellos, no puedas atravesarlos.

Esta es una técnica realmente sencilla. Para probarla, tomaremos el código de Mover mientras se presiona la tecla, así como las paredes de Interactuando con varios elementos iguales, y los rectángulos 2D de Getters y Setters.

Comenzamos creando nuestras variables para el jugador y las paredes:
        player = null,
        wall = [],
Y los inicializamos en la función "init" con los siguientes valores:
        // Create player
        player = new Rectangle2D(80, 80, 10, 10, true);

        // Create walls
        wall.push(new Rectangle2D(100, 50, 10, 10, true));
        wall.push(new Rectangle2D(100, 150, 10, 10, true));
        wall.push(new Rectangle2D(200, 50, 10, 10, true));
        wall.push(new Rectangle2D(200, 150, 10, 10, true));
No olvides dibujarles en la función paint:
        // Draw player
        ctx.fillStyle = '#0f0';
        player.fill(ctx);
        
        // Draw walls
        ctx.fillStyle = '#999';
        for (i = 0, l = wall.length; i < l; i += 1) {
            wall[i].fill(ctx);
        }
Ahora, para hacer que estas paredes actúen como objetos sólidos, la forma sencilla, es al mover el jugador, si este hace intersección con una pared, se devuelva a su posición anterior:
            // Move Rect
            if (pressing[KEY_UP]) {
                player.y -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.y += 5;
                    }
                }
            }
Sin embargo, si el movimiento no es exacto a la distancia faltante a la pared, quedará un pequeño hueco entre el jugador y la pared. Para solucionar este problema, deberemos posicionar al jugador justo en el punto donde termina un objeto y comienza el otro. La forma más sencilla de solucionar esto, aprovechando las capacidades de nuestra función "Rectangle2D", en el caso actual de avanzar hacia arriba, esto sería posicionando el valor "top" del jugador a la posición "bottom" de la pared:
                    if (player.intersects(wall[i])) {
                        player.top = wall[i].bottom;
                    }
No olvides agregar la evaluación de intersección con los otros tres movimientos posibles:
            if (pressing[KEY_RIGHT]) {
                player.x += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.right = wall[i].left;
                    }
                }
            }
            if (pressing[KEY_DOWN]) {
                player.y += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.bottom = wall[i].top;
                    }
                }
            }
            if (pressing[KEY_LEFT]) {
                player.x -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.left = wall[i].right;
                    }
                }
            }
Con esto, podrás crear objetos sólidos en tus juegos, que son la base para juegos de laberintos y (Con algo más de complejidad) juegos de plataformas.

Codigo final:

[Canvas not supported by your browser]
/*jslint bitwise: true, es5: true */
(function (window, undefined) {
    'use strict';
    var KEY_ENTER = 13,
        KEY_LEFT = 37,
        KEY_UP = 38,
        KEY_RIGHT = 39,
        KEY_DOWN = 40,
        
        canvas = null,
        ctx = null,
        lastPress = null,
        pressing = [],
        pause = false,
        player = null,
        wall = [],
        i = 0,
        l = 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);
            }
        }
    };

    document.addEventListener('keydown', function (evt) {
        lastPress = evt.keyCode;
        pressing[evt.keyCode] = true;

        if (lastPress >= 37 && lastPress <= 40) {
            evt.preventDefault();
        }
    }, false);

    document.addEventListener('keyup', function (evt) {
        pressing[evt.keyCode] = false;
    }, false);

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

        // Draw player
        ctx.fillStyle = '#0f0';
        player.fill(ctx);
        
        // Draw walls
        ctx.fillStyle = '#999';
        for (i = 0, l = wall.length; i < l; i += 1) {
            wall[i].fill(ctx);
        }

        // Debug last press
        ctx.fillStyle = '#fff';
        ctx.fillText('Last Press: ' + lastPress, 0, 20);
        
        // Pause
        if (pause) {
            ctx.textAlign = 'center';
            ctx.fillText('PAUSE', 200, 75);
            ctx.textAlign = 'left';
        }
    }

    function act(deltaTime) {
        if (!pause) {
            // Move Rect
            if (pressing[KEY_UP]) {
                player.y -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.top = wall[i].bottom;
                    }
                }
            }
            if (pressing[KEY_RIGHT]) {
                player.x += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.right = wall[i].left;
                    }
                }
            }
            if (pressing[KEY_DOWN]) {
                player.y += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.bottom = wall[i].top;
                    }
                }
            }
            if (pressing[KEY_LEFT]) {
                player.x -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.left = wall[i].right;
                    }
                }
            }

            // Out Screen
            if (player.x > canvas.width) {
                player.x = 0;
            }
            if (player.y > canvas.height) {
                player.y = 0;
            }
            if (player.x < 0) {
                player.x = canvas.width;
            }
            if (player.y < 0) {
                player.y = canvas.height;
            }
        }
        // Pause/Unpause
        if (lastPress === KEY_ENTER) {
            pause = !pause;
            lastPress = null;
        }
    }

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

    function run() {
        setTimeout(run, 50);
        act(0.05);
    }

    function init() {
        // Get canvas and context
        canvas = document.getElementById('canvas');
        ctx = canvas.getContext('2d');
        canvas.width = 300;
        canvas.height = 200;
        
        // Create player
        player = new Rectangle2D(80, 80, 10, 10, true);

        // Create walls
        wall.push(new Rectangle2D(100, 50, 10, 10, true));
        wall.push(new Rectangle2D(100, 150, 10, 10, true));
        wall.push(new Rectangle2D(200, 50, 10, 10, true));
        wall.push(new Rectangle2D(200, 150, 10, 10, true));

        // Start game
        run();
        repaint();
    }
    
    window.addEventListener('load', init, false);
}(window));

9 comentarios:

  1. Muy simple pero a la vez muy útil!!

    Más de una hora sin dar como hacer para no atravesar los bloques y llego en un momento a tu blog y voilá, problema resuelto. Vaya maravilla de blog que tienes compañero, da gusto entrar.

    Como siempre breve y conciso. Gracias ^^

    ResponderEliminar
    Respuestas
    1. Parece ser uno de los secretos más guardados de desarrollobde videojuegos. No se si por lo maravilloso que es esta técnica, o simplemente por que es demasiado sencillo y no vale la pena su mención. ¡Pero es una de las dudas que más se preguntan! (Incluido yo en su momento, aunque tuve que averiguarlo por la mala).

      ¡Me alegra haya sido de ayuda para ti! Muchas gracias por tu comentario.

      Eliminar
  2. Muy bueno tu blog, me esta sirviendo de gran ayuda para empezar a programar mis juegos. Muchas gracias!!

    ResponderEliminar
  3. Una duda, ¿para que se usa lo siguiente, no lo entiendo, lo de ( ? ) y lo de ( : ) ?
    this.x=(x==null)?0:x;

    ResponderEliminar
    Respuestas
    1. Es una asignación condicional. El signo de interrogación pregunta por el contenido del paréntesis. Si es cierto, devuelve el valor antes del ":", de lo contrario, devuelve el valor despues de este.

      Eliminar
    2. Así es, esta es la forma corta. Sí quieres usar la forma larga, sería así:

      if (x==null) {
      this.x = 0;
      } else {
      this.x = x;
      }

      El else sería redundante en este caso, por lo que podrías dejarlo fuera.

      Eliminar