Aprende a crear juegos en HTML5 Canvas

domingo, 10 de mayo de 2015

Ajusta a la rejilla

Con el conocimiento que hemos adquirido hasta ahora, crearemos un juego de rompecabezas. Para empezar, necesitaremos una rejilla donde acomodar las piezas del rompecabezas. Comencemos declarando el arreglo que contendrá dicha rejilla:
        grid = [],
Vamos a hacer una rejilla de seis piezas de ancho por cuatro de alto. Para ello declararemos dos variables temporales dentro de la función "init" que indicarán la posición en los ejes "x" y "y" de nuestra rejilla. Posteriormente haremos un ciclo "for" para las 6 piezas en el eje "x" dentro de otro ciclo "for" para las 4 piezas en el eje "y", dentro del cual crearemos cada uno de los rectángulos de la rejilla.

Al crear el rectángulo, para obtener su posición correcta, se han de multiplicar estas variables "x" y "y" por el ancho y el alto del rectángulo respectivamente, para que de esta forma, queden alineados correctamente dentro de la cuadrícula. Para darle un márgen a la cuadrícula, se le suman posteriormente la cantidad de pixeles que se le desea dar de cada lado. Así, en este caso, le daremos un margen de 25 pixeles en el eje "x" y de 100 pixeles en el eje "y":
        // Create grid and draggables
        var x = 0,
            y = 0;
        for (y = 0; y < 4; y += 1) {
            for (x = 0; x < 6; x += 1) {
                grid.push(new Rectangle2D(x * 25 + 25, y * 25 + 100, 25, 25, true));
                draggables.push(new Rectangle2D(random(canvas.width), random(canvas.height), 25, 25, false));
            }
        }
Notarás que en este caso, activamos la bandera booleana "createFromTopLeft" como verdadera, para facilitarnos la posición inicial donde queremos dejar el margen de la rejilla. También aprovechamos la creación de la rejilla para agregar una pieza del rompecabezas por cada cuadrícula dentro de ella, asegurándonos de esta forma que la cantidad de piezas y de cuadros en la rejilla sean siempre las mismas.

Pasaremos ahora a dibujar la rejilla. En este caso, dibujaremos su contorno en lugar del relleno para dar la sensación de espacio vacío en cada espacio de la rejilla. También dibujaremos el valor numérico de cada espacio para asegurarnos que estos hayan sido creados en su lugar correspondiente:
        // Draw grid
        ctx.fillStyle = '#999';
        ctx.strokeStyle = '#999';
        for (i = 0, l = grid.length; i < l; i += 1) {
            grid[i].stroke(ctx);
            ctx.fillText(i, grid[i].x, grid[i].y);
        }
De igual forma, modificaremos levemente el dibujado de los rectángulos que representan las piezas de nuestro rompecabezas, para poder visualizar también el calor numérico de cada una de ellos:
        // Draw rectangles
        for (i = draggables.length - 1; i > -1; i -= 1) {
            ctx.fillStyle = '#00f';
            draggables[i].fill(ctx);
            ctx.fillStyle = '#fff';
            ctx.fillText(i, draggables[i].x, draggables[i].y);
        }
Finalmente, para hacer que nuestro juego funcione correctamente, agregaremos una condicional al liberar el botón del ratón, que evaluará si la pieza que estamos arrastando corresponde a la cuadrícula de la rejilla correspondiente a dicha pieza. De ser así, la posición de la pieza se acomodará a aquella de la cuadrícula, ajustando de forma automática la pieza a la rejilla de esta forma:
                // Snap draggable intro grid
                if (grid[dragging].contains(pointer)) {
                    draggables[dragging].x = grid[dragging].x;
                    draggables[dragging].y = grid[dragging].y;
                }
Con esto, tendremos la base necesaria para crear nuestro rompecabezas. Podemos probar el juego en este momento, y ver como las piezas se acomodan automáticamente en el lugar que les corresponde.

Ahora que tenemos lista esta base, seguro te gustará agregar alguna forma de saber cuando todas las piezas han sido posicionadas en el lugar que les corresponde. Para ello, empezaremos agregando una nueva variable booleana que nos indicará si el juego ya ha concluido o no:
        isFinished = false,
Cada vez que se suelte una pieza, buscaremos identificar si el rompecabezas ha sido resuelto:
                isFinished = getPuzzleSolved();
La función para identificar si el rompecabezas ha sido resuelto, buscará cuadrícula por cuadrícula si la pieza del rompecabezas correspondiente está en su misma posición. En cuanto identifique una que no lo está, regresará un valor falso. Si llega al final de la rejilla sin encontrar una pieza fuera de lugar, significa que el rompecabezas ha quedado resuelto, y por tanto regresará un valor verdadero:
    function getPuzzleSolved() {
        for (i = 0, l = grid.length; i < l; i += 1) {
            if (grid[i].x !== draggables[i].x || grid[i].y !== draggables[i].y) {
                return false;
            }
        }
        return true;
    }
Usualmente comparamos con una intersección en lugar de comparar contra la coordenada exacta, ya que en la mayoría de los juegos es poco probable que dos objetos se encuentren exactamente en la misma coordenada, pero dado al ajuste a la rejilla, podemos tener seguridad que en este caso, esta clase de comparación siempre será efectiva.

Finalmente, dibujamos un mensaje de felicitación en caso de que el juego haya concluido:
        // Is the game finished?
        ctx.fillStyle = '#fff';
        if (isFinished) {
            ctx.fillText('Well done!', canvas.width / 2, canvas.height / 2);
        }
Con esto tendremos completa la base de nuestro rompecabezas. Dado que no ocurre aún ningún evento al terminar nuestro juego, he agregado una línea la cual, cada que se selecciona una pieza, el mensaje de felicitación regresa a falso. De esta forma aun después de terminar se pueden remover las piezas, apareciendo de nuevo el mensaje hasta que todas las piezas se vuelvan a acomodar en su lugar.

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 = [],
        grid = [],
        isFinished = false,
        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;
        },
        
        contains: function (rect) {
            if (rect !== undefined) {
                return (this.left < (rect.left || rect.x) &&
                    this.right > (rect.right || rect.x) &&
                    this.top < (rect.top || rect.y) &&
                    this.bottom > (rect.bottom || rect.y));
            }
        },
        
        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);
            }
        },
        
        stroke: function (ctx) {
            if (ctx !== undefined) {
                ctx.strokeRect(this.left, this.top, this.width, this.height);
            }
        }
    };
    
    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 getPuzzleSolved() {
        for (i = 0, l = grid.length; i < l; i += 1) {
            if (grid[i].x !== draggables[i].x || grid[i].y !== draggables[i].y) {
                return false;
            }
        }
        return true;
    }

    function paint(ctx) {
        // Clean canvas
        ctx.fillStyle = '#ccf';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Set default text properties
        ctx.textAlign = 'center';
        
        // Draw grid
        ctx.fillStyle = '#999';
        ctx.strokeStyle = '#999';
        for (i = 0, l = grid.length; i < l; i += 1) {
            grid[i].stroke(ctx);
            ctx.fillText(i, grid[i].x, grid[i].y);
        }
        
        // Draw rectangles
        for (i = draggables.length - 1; i > -1; i -= 1) {
            ctx.fillStyle = '#00f';
            draggables[i].fill(ctx);
            ctx.fillStyle = '#fff';
            ctx.fillText(i, draggables[i].x, draggables[i].y);
        }
        
        // Debug pointer position
        ctx.fillStyle = '#0f0';
        ctx.fillRect(pointer.x - 1, pointer.y - 1, 2, 2);
        
        // Is the game finished?
        ctx.fillStyle = '#fff';
        if (isFinished) {
            ctx.fillText('Well done!', canvas.width / 2, canvas.height / 2);
        }
        
        // Debug dragging rectangle
        //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 rectangle
            for (i = 0, l = draggables.length; i < l; i += 1) {
                if (draggables[i].contains(pointer)) {
                    dragging = i;
                    isFinished = false;
                    break;
                }
            }
        }
        
        if (dragging !== null) {
            // Move current dragging rectangle
            draggables[dragging].x = pointer.x;
            draggables[dragging].y = pointer.y;
            
            if (lastRelease === 1) {
                // Snap draggable intro grid
                if (grid[dragging].contains(pointer)) {
                    draggables[dragging].x = grid[dragging].x;
                    draggables[dragging].y = grid[dragging].y;
                    isFinished = getPuzzleSolved();
                }

                // Release current dragging rectangle
                dragging = null;
            }
        }
    }

    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 grid and draggables
        var x = 0,
            y = 0;
        for (y = 0; y < 4; y += 1) {
            for (x = 0; x < 6; x += 1) {
                grid.push(new Rectangle2D(x * 25 + 25, y * 25 + 100, 25, 25, true));
                draggables.push(new Rectangle2D(random(canvas.width), random(canvas.height), 25, 25, false));
            }
        }
        
        // Start game
        enableInputs();
        run();
    }
    
    window.addEventListener('load', init, false);
}(window));

No hay comentarios.:

Publicar un comentario