Aprende a crear juegos en HTML5 Canvas

domingo, 3 de mayo de 2015

Getters & Setters

Aprendiendo como interactuar rectángulos con el ratón, nos dimos cuenta que la interacción entre estos dos no es muy natural, pues a diferencia de los círculos, el origen de los rectángulos no es en el centro, si no en la esquina superior izquierda. Conforme se van desarrollando nuevos proyectos o proyectos más complejos, veremos que tener el origen en el centro facilitaría mucho la interacción con estos objetos en algunos de estos proyectos, y existe un truco que permite esta configuración.

Los getters y setters son un híbrido mágico entre función y variable, pues se manejan como si fueran variables dentro de un objeto, pero reaccionan como funciones, afectando varias propiedades del objeto que pueden interactuar entre sí, y los rectángulos son el mejor caso para ejemplificar esto.

Para comenzar, inicializaremos las variables necesarias en el prototipo de nuestro rectángulo. Para evitar confusiones con nuestra antigua clase "Rectangle", a esta clase la llamaremos "Rectangle2D":
    Rectangle2D.prototype = {
        left: 0,
        top: 0,
        width: 0,
        height: 0
    };
En este momento te has de preguntar "¿Por qué estamos usando en esta ocasión los nombres 'top'y left'? ¿Dónde quedaron 'x'y 'y'?", y es aquí donde aprendemos la magia de los getters y los setters. Comencemos por un ejemplo práctico, si quisieramos manipular el centro de un rectángulo teniendo las posiciones superior izquierda del mismo, la mejor idea que podríamos tener sería crear funciones para esto, y ejecutarlas de esta forma:
var x = myRect.getX();
myRect.setX(120);
var y = myRect.getY();
myRect.setY(180);
Dentro de esas funciones manipularíamos los valores correspondientes con la variable "left". Esto funciona muy bien, pero son funciones comunes de nombres largos que repetiremos continuamente a lo largo de nuestro código. Sería más práctico si pudieramos asignar y obtener su valor como si fueran variables, tal como lo hemos hecho en nuestra antigua función "Rectangle", de esta forma:
var x = myRect.x;
myRect.x = 120;
var y = myRect.y;
myRect.y = 180;
Por supuesto, tener múltiples variables para un mismo efecto sería poco práctico, porque posteriormente tendríamos que manipular todas sus variantes de forma manual, y actualizar "left" y "y" de esta forma cada vez que uno de estos valores cambie no sería nada práctico. Si tan solo hubiera una forma de que, al cambiar el valor de una variable, todas las variables relacionadas cambiaran su valor también...

Es aquí donde aplicamos los getters y los setters. Si en lugar de declarar las funciones como "getX()" y "setX(value)", las declaramos como "get x()" "set x(value)", podremos manipularlas de la misma forma como vimos en el ejemplo pasado. Por tanto, declararemos el getter y setter para "x" y "y" de la siguiente forma:
        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;
        },
De esta forma, podremos usar "myRect.x" y "myRect.y" como los hemos usado hasta el día de hoy, pero esta vez manipulando el centro del rectángulo a través de estas aparentes "variables". ¿Pero por qué detenernos aquí? Ahora que conocemos esta técnica podemos aplicarla también para manipular los valores para el lado derecho e inferior del rectángulo, de la siguiente forma:
        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;
        },
¿En que nos beneficia esto? Para empezar algunas funciones que hemos realizado antes serán mucho más simples de programar y comprender, tal como la función "intersects":
        intersects: function (rect) {
            if (rect !== undefined) {
                return (this.left < rect.right &&
                    this.right > rect.left &&
                    this.top < rect.bottom &&
                    this.bottom > rect.top);
            }
        },
De igual forma se simplifica la función "contains". Observa también como ahora es más simple comparar independiente de si el otro objeto es un rectángulo o un punto de coordenadas:
        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));
            }
        },
Aprovechando que estamos revisando nuestras antiguas funciones, no olvides actualizar también la función "fill", de lo contrario, si aun usas las coordenadas "x" y "y", el cuadro se dibujará desde su centro con un área incorrecta:
        fill: function (ctx) {
            if (ctx !== undefined) {
                ctx.fillRect(this.left, this.top, this.width, this.height);
            }
        }
Finalmente, solo queda asignar los valores en el constructor de nuestro rectángulo:
    function Rectangle2D(x, y, width, height) {
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
        this.x = (x === undefined) ? 0 : x;
        this.y = (y === undefined) ? 0 : y;
    }
Habrás notado que en esta ocasión, he asignado los valores del alto y el ancho antes de su posición. Esto es sumamente importante, porque el centro del rectángulo depende por completo de su alto y su ancho. Si intentamos asignar el centro del rectángulo cuando su ancho y alto son aun cero, terminarán resultado de igual forma que si se les hubiera asignado a la parte superior izquiera del mismo.

Esto conlleva finalmente a una importante pregunta. Al crear un rectángulo ¿Es mejor asignar sus valores al centro, o a la esquina superior izquierda? En realidad depende mucho del juego que estás creando. En juegos de movimiento libre como rompecabezas y naves espaciales, puede resultar mucho más cómodo manejar las coordenadas desde su centro, mientras que en otros genéros que requieran de un universo estructurado, como son laberintos y plataformas, crearlos desde su esquina puede resultar más conveniente.

Para mantener abiertas ambas posibilidades, agregaremos una quinta variable en el constructor de nuestro rectángulo, un valor booleano llamado "createFromTopLeft", el cual al ser verdadero, utilizará las coordenadas de origen como su esquina superior izquierda, y de lo contrario, si el valor es falso o se omite, lo creará desde su centro por defecto:
    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;
        }
    }
Así, al crear los arrastrables en nuestro código de la ocasión pasada con un "Rectangle2D", lo haríamos de la siguiente forma:
            draggables.push(new Rectangle2D(random(canvas.width), random(canvas.height), 20, 20, false));
He especificado al final el valor "false" por no dejarlo vacío, pero no especificarlo tal como era anteriormente haría el mismo efecto. Con esto hemos aprendido a usar getters y setters, y su aplicación práctica en los rectángulos.

En el futuro veremos como esta clase de rectángulos nos serán bastante prácticos, para aplicar imagenes rotadas, volteadas y escaladas, para ubicar y reubicar la posición de un rectángulo al interactuar con objetos sólidos, y muchos otros casos más. [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 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);
            }
        }
    };
    
    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 rectangles
        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 rectangle
        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 rectangle
            for (i = 0, l = draggables.length; i < l; i += 1) {
                if (draggables[i].contains(pointer)) {
                    dragging = i;
                    break;
                }
            }
        } else if (lastRelease === 1) {
            // Release current dragging rectangle
            dragging = null;
        }
        
        // Move current dragging rectangle
        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 Rectangle2D(random(canvas.width), random(canvas.height), 20, 20, false));
        }
        
        // Start game
        enableInputs();
        run();
    }
    
    window.addEventListener('load', init, false);
}(window));

No hay comentarios.:

Publicar un comentario