Aprende a crear juegos en HTML5 Canvas

lunes, 5 de marzo de 2012

Programación orientada a objetos

En programación se llama "objeto" a un conjunto de propiedades y métodos que definen su comportamiento. Al conjunto de características definidas en base al cual se crea un objeto, se llama "clase".

Los lenguajes de programación permiten crear clases personalizadas además de aquellas que vienen predefinidas. Para comprender mejor lo aquí explicado, crearemos de ejemplo una clase para objetos de tipo rectángulo.

JavaScript, a diferencia de otros lenguajes, no tiene clases como tal. Pero se pueden definir funciones que actúan como clases, tal como mostramos en el siguiente código:
function Rectangle(x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
}
Mediante la función anterior, podemos crear ahora objetos de tipo rectángulo, como se muestra en el ejemplo siguiente:
var rect1 = new Rectangle(50, 50, 100, 60);
De esta forma, hemos creado un objeto de tipo rectángulo, al cual especificamos sus propiedades x (50), y (50), ancho (100) y alto (60).

Sin embargo, si por error se creara un objeto sin especificar todas sus propiedades, las propiedades faltantes obtendrían un valor nulo o indefinido, lo cual podría causar problemas posteriormente en nuestro código. Para prevenir ello, es recomendado que se asigne un valor predefinido en caso que no se especifique el valor indicado:
this.x = (x == null) ? 0 : x;
En esta línea, asignamos a this.x uno de dos valores. Se comprueba mediante (x == null) ? si su valor es nulo o indefinido. Si es así, se asigna el valor antes de los dos puntos (0), y en caso contrario, se asigna el valor posterior a los dos puntos (x).

Esta asignación predefinida se repite para los demás valores. Personalmente, me gusta hacer algo distinto con la altura; en caso de que su valor sea nulo o indefinido, en lugar de asignarle 0, le asigno el mismo valor que el ancho. De esta forma, si envío solo tres valores al rectángulo en lugar de 4, me creará un cuadrado perfecto cuyo ancho y alto será el tercer y último valor asignado:
this.height = (height == null) ? this.width : height;
Nota que se asigna this.width y no width directamente, pues de esta forma, se obtiene ya su valor después de comprobarse si era nulo o no. De lo contrario, podríamos asignar un valor nulo por error a la altura, en caso que el ancho enviado haya sido nulo también.

Antes de la programación orientada a objetos, cada propiedad de un objeto debía almacenarse en una variable independiente. Si bien esto no parece gran cosa para uno o dos objetos en el código, cuando se maneja decenas o cientos de estos, se vuelve una tarea complicada el no hacer estos programas sin la ayuda de objetos.

Otra propiedad importante de los objetos, es que tienen métodos propios. Por ejemplo, agregaremos este método a la clase Rectángulo, la cual nos permitirá saber cuando dicho rectángulo esté en intersección con otro:
    this.intersects = function (rect) {
        if (rect == null) {
            window.console.warn('Missing parameters on function intersects');
        } else {
            return (this.x < rect.x + rect.width &&
                this.x + this.width > rect.x &&
                this.y < rect.y + rect.height &&
                this.y + this.height > rect.y);
        }
    };
Para llamar a este método, lo haríamos de la siguiente forma:
    if (rect1.intersects(rect2)) {
     
    }
Analicemos ahora la función intersects paso a paso. Para empezar, se recibe una variable rect, que será un objeto de tipo rectángulo. En la siguiente línea, comprobamos si el rectángulo es nulo, y si se da el caso, advertimos sobre el parámetro faltante, pues en caso de no enviar nada, si esta validación no se efectuara, provocaría un error. Por el contrario, si el rectángulo existe, se ejecuta una comparación de los cuatro puntos de ambos rectángulos, para comprobar si el contenido ambos rectángulos se intersecta en algún momento, y se retorna el valor de si esta comparación es cierta o falsa.

Otro método útil para nuestro rectángulo, es que este se dibuje automáticamente en nuestro lienzo. Mediante esta función, solo debemos indicarle el contexto donde se dibujará el rectángulo, y este se dibujará de forma automática en donde sus valores indiquen, haciéndolo con una instrucción más sencilla y con menor probabilidad de errar en los valores dados:
    this.fill = function (ctx) {
        if (ctx == null) {
            window.console.warn('Missing parameters on function fill');
        } else {
            ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    };
Usar rectángulos y otros objetos en los códigos de tus juegos, facilitarán muchas de las tareas que necesitas para llevarles a cabo. Con esto, concluimos la explicación básica sobre programación orientada a objetos. Para conocer más a profundidad sobre objetos en JavaScript, puedes hacerlo en el siguiente enlace: http://developer.mozilla.org/es/docs/Introducción_a_JavaScript_orientado_a_objetos.

Código final:

function Rectangle(x, y, width, height) {
    this.x = (x == null) ? 0 : x;
    this.y = (y == null) ? 0 : y;
    this.width = (width == null) ? 0 : width;
    this.height = (height == null) ? this.width : height;

    this.intersects = function (rect) {
        if (rect == null) {
            window.console.warn('Missing parameters on function intersects');
        } else {
            return (this.x < rect.x + rect.width &&
                this.x + this.width > rect.x &&
                this.y < rect.y + rect.height &&
                this.y + this.height > rect.y);
        }
    };

    this.fill = function (ctx) {
        if (ctx == null) {
            window.console.warn('Missing parameters on function fill');
        } else {
            ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    };
}
Regresa al índice.

23 comentarios:

  1. Interesante curso :), gracias por compartir. Sólo como un comentario, también se puede asignar un valor por default, con:
    this.x = x || 0;
    Saludos.

    ResponderEliminar
    Respuestas
    1. Es muy cierto, esta es una forma mas sencilla de hacer esta asignación. Pero este método no sería efectivo si por ejemplo, el valor prefeterminado es 40, e intentas asignarle un 0. Es por ello, que enseño este método en su lugar.

      Gracias por tu comentario :)

      Eliminar
  2. Quizás es por que yo soy muy cabezón o estricto pero prefiero que al ejecutar la app me de error si me falta pasarle un parámetro a la función. Tengo por costumbre de si pone que recibe 3 parámetros por ejemplo, pues pasarle esos 3 parámetros y no menos de lo que indica.

    Sé que el operador ternario en el caso de que no le pases nada es útil pero prefiero prevenir que curar.

    A lo mejor es porque estoy muy acostumbrado a Java y no me gusta cambiar xD

    ResponderEliminar
    Respuestas
    1. Bueno, esta forma tiene sus ventajas y desventajas, como verás con el tiempo. Es por esto mismo, que comprobamos que un valor no sea nulo si deseamos que sea obligatorio (Como se muestra en las funciones "intersects" y "fill". Si quieres que te lance un error para advertirte cuando omites un parámetro esencial en una función, puedes hacerlo dentro de un else:

      else console.error("Faltan parámetros en la función Rectangle.intersect(rect)");

      Eliminar
  3. Genial! está super bien explicado y es una forma precisa de calcular el impacto.

    ResponderEliminar
  4. Hola! perdona pero soy muy nuevo en esto. Estoy intentado entender bien el codigo y queria saber bien como crear una "clase". Dejando un lado la parte de intersects. ¿Como puedo dibujar el rectangulo? te dejo el codigo que tengo para que me explique en que fallo. Un saludo y gracias de antemano.

    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;

    var rect1=new Rectangle(50,50,100,60);

    function init(){
    canvas=document.getElementById('canvas');
    ctx=canvas.getContext('2d');
    paint(ctx);
    }


    function paint(ctx){
    ctx.fillStyle='#000';
    ctx.fillRect(0,0,canvas.width,canvas.height);
    ctx.fillStyle='#0f0';
    rect1.fill(ctx);

    }


    function Rectangle(x,y,width,height){
    this.x=x;
    this.y=y;
    this.width=width;
    this.height=height;
    }

    ResponderEliminar
    Respuestas
    1. Dentro de la función Rectangle hay dos funciones: intersects y fill. En tu caso, estás excluyendo la primera, pero olvidaste agregar la segunda, por lo que al llamar a rect1.fill, no encuentra como hacer dicha función.

      Solo eso: agrega la función fill a rect. ¡Felices códigos!

      Eliminar
    2. Ya lo entendí. Muchas Gracias!!!

      Eliminar
  5. function Rectangle(x,y,width,height){
    this.x=(x==null)?0:x;
    this.y=(y==null)?0:y;
    this.width=(width==null)?0:width;
    this.height=(height==null)?this.width:height;

    this.intersects=function(rect){
    if(rect!=null){
    return(this.xrect.x&&
    this.yrect.y);
    }
    }

    this.fill=function(ctx){
    if(ctx!=null){
    ctx.fillRect(this.x,this.y,this.width,this.height);
    }
    }
    }

    En este ejemplo, cada vez que creamos un nuevo objeto Rectangle,
    estaríamos duplicando las funciones (intersects y fill), y creando copias distintas de las funciones para cada objeto nuevo.
    No sería más recomendable usar la propiedad "prototype" de un objeto?
    De esta manera las funciones solo se crearian una sola vez.

    Por ejemplo fuera de la función constructora Rectangle poner:


    Rectangle.prototype.intersects = function(){

    if(rect!=null){
    return(this.xrect.x&&
    this.yrect.y);
    }
    }

    De la misma forma con los demas metodos del objeto.

    Saludos! ;)

    ResponderEliminar
  6. Muy buen sitio, gracias por tanta útil información!

    ResponderEliminar
  7. En el tema http://juegoscanvas.blogspot.mx/2012/02/optimizacion-para-javascript.html , en la sección "Usar prototipos en las pseudo-clases", este tema ya es tratado, y los ejemplos posteriores ya lo contemplan.

    De igual forma, muchas gracias por notar este detalle.

    ResponderEliminar
  8. En la función intersects() no sera mejor en lugar de usar condicionales && (AND) para saber si se tocan dos rectángulos usar condicionales || (OR) Pues según la prueba que realice la función solo va a regresar true cuando se toque por los cuatro lados a la vez lo cual es imposible a no ser que haya una anomalía cuántica XD (No se que significa lo de la anomalía pero se escucha muy técnico jajjaja), hasta pronto.

    ResponderEliminar
    Respuestas
    1. Pues no se que tengas en mente, pero creo que ya has comprobado en todos los ejemplos que la función es correcta. Por el contrario, si fueran ORs, la función retornaría true en todo tiempo...

      Creo que hay más bien un error en tu análisis... ¿Será acaso que lo interpretaste al revés de como funciona en realidad?

      Eliminar
    2. Volvi a realizar una prueba y ya vi lo que pasa si pongo OR's, el problema era que habia realizado una prueba pero con una idea distinta, ya capte la idea y ahora entiendo mucho mejor el code, gracias de nuevo. Ya he hecho mi primer juego seguire leyendo tu blog, por cierto ¿publicaras otras entradas?

      Eliminar
    3. ¡Seguro que sí! Puedes ver que actualmente estoy trabajando todavía sobre la sección de platformers, y recientemente han surgido dudas sobre interacción de mouse y rectángulos que planeo cubrir.

      ¡Mucha suerte en tu juego! Y si tienes más dudas, con gusto buscaré poder ayudarte.

      Eliminar
  9. Hola, buen dia, necesita una ayudita con el tema de acceder a las propiedades de un padre, se que es algo "_super"
    te muestro este codigo a ver si ilustro mi duda

    [code]
    function hijo(nombre, edad){
    this.nombre=nombre;
    this.edad=edad;
    }


    function padre(nombre, edad){
    this.nombre=nombre;
    this.edad=edad;
    this.hijos=[];
    }

    padre.prototype.tenerHijo=function(nombre, edad){
    this.hijos[0]=new hijo(nombre, edad);
    }

    //aqui va la duda
    hijo.prototype.saberNombreDeMiPadre=function(){
    //como accedo desde aqui al nombre de mi padre

    //deberia retornar o escribir "juan"

    }

    var juan=new padre("juan",35);
    juan.tenerHijo("juanito",12);

    juan.hijos[0].saberNombreDeMiPadre();
    [/code]

    Gracias de antemano por tu ayuda

    ResponderEliminar
    Respuestas
    1. El problema que tienes no es de herencia. Herencia sería si tuvieras una función "humano" con variables "nombre" y "edad", y partir de esa función, crear la función padre que tuviera las mismas variables y funciones que humano, mas aparte agregarle la variable arreglo de "hijos", y las funciones exclusivas de padre.

      Ahora, para lo que tú deseas, no hay forma por defecto de saber a que arreglo pertenece una clase, ya que la misma clase podría pertenecer a varios arreglos. Pero si tu sabes que dicha clase sólo pertenecerá a una clase padre, puedes agregarle una variable "parent", y asignársela al agregar un hijo a un padre de esta forma:

      padre.prototype.tenerHijo=function(nombre, edad){
      var hijo=new hijo(nombre, edad);
      hijo.parent=this;
      this.hijos.push(hijo);
      }

      Finalmente, la función que buscas, quedaría de esta forma:

      hijo.prototype.saberNombreDeMiPadre=function(){
      return this.parent.nombre;
      }

      Con esto, podrás acceder a todas las propiedades de la clase "parent" de cualquier clase que crees.

      Eliminar
    2. ok, te entiendo, pero eso no duplicaria al la clase "padre" cada vez que cree un hijo ?,

      Por otro lado me suena lo de la herencia pero aun asi como seria, creo la clase humano y creo ajuan y juanito, como se que juan es el padre de juanito, como lo harias ?

      Muchas gracias

      Eliminar
    3. La variable es solo una referencia, no se está duplicando ningún objeto.

      Y con herencia o sin ella, la forma de hacerlo sería con una referencia, tal como te he explicado.

      Eliminar
  10. [code]
    // clase padre
    function Mamifero(medio, num_extremidades, sonido){
    this.medio = medio;
    this.extremidades = num_extremidades;
    this.sonido=sonido;
    this.saludo = function(){console.log(this.sonido)};
    }

    // clase hija
    function Perro(raza){
    this.raza = raza;
    }
    // especificamos cual es la clase padre
    Perro.prototype = new Mamifero("terrestre", 4, "Guauu, Guauu...");

    fluffy = new Perro("Doberman");
    azabache = new Perro("terrier");

    fluffy.saludo();

    console.log("raza de fluffy "+fluffy.raza);
    console.log("medio de fluffy "+fluffy.medio);
    console.log("--------");
    console.log("raza de azabache "+azabache.raza);
    console.log("medio de azabache "+azabache.medio);

    [/code]

    ResponderEliminar
    Respuestas
    1. ¡Lograste dominar herencia en JS después de todo! ¡¡Muchas felicidades!! ;)

      Eliminar