martes, 22 de mayo de 2012

Utilizando JSON para serializar y des-serializar objetos de javascript incluyendo sus métodos (Parte 1).

Ver Parte 2 de este tema.
En el desarrollo real del editor que estoy realizando me he visto en la necesidad de serializar mis objetos javascript a disco para lo que pensaba que seria suficiente con convertir mis objetos al formato JSON utilizando json2.js de https://github.com/douglascrockford/JSON-js) :

localStorage.setItem(nombredelapantalla, JSON.stringify(pantalla));

¿Cual ha sido mi sorpresa cuando he visto que este formato no admite que los objetos javascript tengan funciones? Es decir, si tenemos un objeto (función) del tipo:

function Persona(nombre, trabajo, nacimiento) {
     this.nombre = nombre;
     this.trabajo = trabajo;
     this.nacimiento = nacimiento;
     this.getNombre = function() {
          return this.nombre;
     }
}

Su JSON será:
{"nombre":"Pedro Picapriedra","trabajo":"Picapedrero","nacimiento":-1000}. 
Aquí no existe ninguna referencia a la clase, objeto o tipo “Persona”. Si des-serializamos este objeto tendremos un nuevo objeto con los atributos nombre, trabajo, y nacimiento pero sin el método getNombre() (Sabremos que su nombre es Pedro Picapiedra, que es Picapedrero pero no sabremos si es una Persona y no le podremos preguntar su nombre).
Tras varias vueltas por internet y revisar arriba y abajo JQuery sólo he visto el método toSource que convierte un objeto a un formato similar al de JSON con referencias al método y al tipo del objeto. Lástima que, una vez más, este método no esté disponible en todos los navegadores y sólo sea válido para Mozilla.
En definitiva, toca hacer en javascript algo que muchos otros lenguajes hacen de serie.

1ª Prueba: Introducimos un atributo para identificar el tipo del objeto en nuestra “clase” y lo utilizamos para regenerar el objeto copiando los valores de los atributos del objeto des-serializado por json2:

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var member in objetoSinMetodos)
          nuevoPedro[member] = objetoSinMetodos[member];
     alert(nuevoPedro.getNombre() + ", " + nuevoPedro.trabajo + "," + nuevoPedro.nacimiento);
}

El ejemplo en:

2ª Prueba: Introducimos el objeto "Direccion" con sus atributos y métodos como atributo del objeto Persona. Al des-serializar queremos poder llamar a objPersona.getDireccion().getCodigoPostal(). 
El código siguiente se explica por sí mismo, si un atributo es un objeto le metemos la propiedad “tipo” y así sabemos de qué tipo es (aunque en el código lo creemos a capón en esta versión):
var valorAtributo;

if (objetoSinMetodos.tipo == "persona") {
     var nuevoPedro = new Persona();
     // Copiamos los valores de los atributos
     for(var atributo in objetoSinMetodos) {
          valorAtributo = objetoSinMetodos[atributo];
    if (typeof valorAtributo == "object") {
         var nuevaDireccion = new Direccion();
         for (var atributo2 in valorAtributo)
              nuevaDireccion[atributo2] = valorAtributo[atributo2];
         nuevoPedro[atributo] = nuevaDireccion;
    } 
    else
         nuevoPedro[atributo] = valorAtributo;
     }
     alert(nuevoPedro.getDireccion().getCodigoPostal());
}

El ejemplo completo en:

3.- Quitando el “a capón”: Algo muy bueno que tiene Javascript es que a partir de una variable, un string, un nombre, podemos hacer casi todo. Podemos, en este caso, crear un nuevo objeto del tipo persona o del tipo dirección a partir de los valores, string, de los atributos tipo:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
     var nuevoObjeto = new this[objetoDesdeJSON.tipo];
     var valorAtributo;
     for(var atributo in objetoDesdeJSON) {
          valorAtributo = objetoDesdeJSON[atributo];
    if (typeof valorAtributo == "object")
         nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
    else
         nuevoObjeto[atributo] = valorAtributo;
     }
     return nuevoObjeto;
}

El ejemplo completo en:

4. Incluyendo arrays: Al ir a introducir el código anterior en mi estructura de objetos he visto que algunos de ellos tenían atributos que eran arrays por lo que la función dejaba de funcionar. Para resolver esto, debemos saber si un objeto de Javascript es un array o no. En 
nos explican la solución:

function isArray(a) {
     return Object.prototype.toString.apply(a) === '[object Array]';
}

Quedando, finalmente:

function recrearObjetoDesdeJSON(objetoDesdeJSON) {
  if (!objetoDesdeJSON.tipo)
return objetoDesdeJSON;
var nuevoObjeto = new this[objetoDesdeJSON.tipo];
var valorAtributo;
for(var atributo in objetoDesdeJSON) {
valorAtributo = objetoDesdeJSON[atributo];
if (typeof valorAtributo == "object") {
if (isArray(valorAtributo)) {
var nuevoArray = new Array();
for (var i=0; i<valorAtributo.length; i++) {
nuevoArray[i] = recrearObjetoDesdeJSON(valorAtributo[i]);
}
nuevoObjeto[atributo] = nuevoArray;
}
else
nuevoObjeto[atributo] = recrearObjetoDesdeJSON(valorAtributo);
}
else
nuevoObjeto[atributo] = valorAtributo;
}
return nuevoObjeto;
}

Recordar que para que esto funcione cada uno de nuestros objetos deberá disponer de un atributo “tipo” con el nombre de la clase.
Es muy probable que en el caso de un atributo que sea un array de arrays el código no funcione correctamente. No es mi caso ;D. 

¡CUIDADO! En mi caso tenia varios atributos que luego volvia a referencias desde otro que era un array:
this.atr1
this.atr2
this.atr3 = new Array(this.atr1, this.atr2);
Al des-serializarse el objeto con mi función los atributos del array no son los mismos objetos (son objetos del mismo tipo pero son instancias distintas).  En mi caso la solución ha sido poner una función:
this.getAtributos = function() {
return new Array(this.atr1, this.atr2);
}

Estoy leyendo un buen libro de javascript "Professional javascript for web developers", tercera edición, en el que comentan el problema de codificar los objetos javascript tal y como lo estoy haciendo yo aquí, definiendo las funciones dentro del objeto con "this.funcion". Parece ser que en este caso cada vez que se crea un objeto se crea tambien uno por cada una de las funciones que aparecen en ese objeto de modo tal que si creamos dos instancias de uno de mis objetos se crearan también instancias nuevas de cada una de las funciones. En definitiva debería escribir mis "clases" de otro modo y cuando lo haga deberé buscar el modo de serializar mis objetos de nuevo. En ese momento, espero que pronto, pondré aquí la parte 2.
Por cierto, con ECMASCRIPT 6 parece ser que Javascript resolverá muchos de sus problemas. Se prodrán definir clases, constantes, etc.

Ver Parte 2 de este tema.

jueves, 29 de marzo de 2012

Un editor html WISIWING con JQuery.

Aquí dejo el código de una pequeña prueba para hacer un editor de html (ver código fuente del enlace). Aunque para mis necesidades no es necesario que fuese WISIWING (lo que ves es lo que tienes) si quería ver cómo podría hacer algo que diese una imagen real del resultado final.
Ahora mismo con JQuery (simplifica el trabajo de javascript), con los nuevos navegadores (esto no funcionara en cualquiera si no estamos actualizados), lo tenemos más fácil aunque tenemos que pasar por el aro de trabajar con javascript. Manipulando el dom de la página podemos agregar y eliminar elementos, podemos introducir contenedores (div, párrafos, tablas, …), podemos modificar las propiedades css de los elementos y podemos jugar con sus atributos mientras vemos el resultado directamente en pantalla. Lo mejor, es que el código apenas llega a las 100 líneas e introducir el resto de propiedades y atributos para completar el editor es tremendamente sencillo. 
El enlace al editor:
Una imagen:

Para introducir los elementos en un contenedor, seleccionaremos el elemento y pulsaremos dentro del contenedor. Si, por ejemplo, queremos introducir un botón como el de la imagen, pondremos un elemento input (por defecto se incluye de tipo texto), pulsaremos sobre él, e iremos a los atributos y cambiaremos a tipo "button". Despues en el value podemos poner el texto a mostrar.

sábado, 31 de diciembre de 2011

Colisiones y sonido

Ver artículo anterior de esta serie
Puff, poco tiempo para estas cosas y con el peque ya dándole a la psp y a la ps3. Llego tarde.
Dejo aquí un ejemplillo sobre cómo montar las colisiones (muy burdamente pero suficientes si se ajustasen mejor los rectángulos) y sobre cómo incluir los sonidos. Algo curioso es que si un sonido esta en uso hay que esperar a que termine antes de que se puede volver a utilizar. Para resolver esto hay que tener en el código distintas variables apuntando al mismo sonido. En el caso de las flechas tengo dos variables así que si uno esta sonando se utiliza el otro. Si se dispara muy rápido serian necesarias más variables.
Para probar esto lo mejor es ir a la segunda pantalla y dedicarse a cazar al pájaro. Una vez que se le ha dado con una flecha, cambia el estado del sprite para que este caía hasta donde corresponda en función de la pantalla, al llegar al suelo se convierte en "premio" y cuando el soldado colisiona con el premio se lo lleva.
El ejemplo:

jueves, 3 de noviembre de 2011

Un arco sin caza que cazar :(

Ver el artículo anterior de esta serie.
El ejemplo:

Esto me ha llevado un rato. De nuevo si lo hubiese pensado antes no me habría liado con la parte de apuntar con el arco. ¡Sir Arthur siempre ha tirado los cuchillos “pa lante”!.
Ahora podemos disparar pero nada de tiros parabólicos, hazte a la idea de que la flecha cae después de que sale de la pantalla o de que tienes una fuerza increíble.
Aunque he revisado el código se me ha guarreado un poco. La trayectoria de las flechas es, cómo no, y=mx+b, donde m es la pendiente de la recta, m=tangente del ángulo, y b es el punto de corte con el eje y.
En función de la escala del prota (si te vas a la tercera pantalla es más pequeño) y en función del ancho del sprite o de hacia dónde apunta hay que echar una serie de cálculos en los que no me he parado demasiado. He probado utilizando la intuición y me veo, cuando intente tratar las colisiones con objetos (siguiente paso), revisando todo el código y parándome a pensar, lápiz en mano, las trayectorias y demás. El hecho de que el eje “y” esté invertido en el canvas y de que yo no tenga muy claro hacia dónde van los ángulos complica las cosas y más si añadimos que he modificado el mapa de los sprites para que sólo aparezcan los que miran hacia la izquierda y debo, entonces, girarlos para tratar el caso de los que miran a la derecha:


Por cierto, las flechas van por el canvas intermedio y, por ejemplo, en el dibujo de arriba, si no pongo algo en el primer plano van por delante del desfiladero, ¡un efecto a lo Michael Escher!

miércoles, 26 de octubre de 2011

Subir escaleras molara pero disparar un arco muchísimo más.

Ver el artículo siguiente de esta serie.
Ver el artículo anterior de esta serie.
Primera prueba para el disparo con el arco (Sí ya sé que estas son horas para darle al Batman Arkham City pero el deber es el deber y, además, el peque se ha puesto a darle al Braid y aunque es “mu” complicado para cuando esté esto ya se es capaz de pasarse el Halo sin problemas).
La prueba es muy sencilla, una pequeña animación para ver cómo se va a mover el arco a la hora de apuntar. El mapa del “sprite” es el siguiente:
 
Primero dibujamos el brazo de atrás, girando el contexto el ángulo adecuado, luego pintamos el cuerpo del muñeco (casi a lo Johnny cogió su fusil) y, finalmente, encima de lo dibujado, volvemos a girar el contexto y pintamos el otro brazo y el arco.

El increíble hombre menguante – I.

Ver artículo siguiente de esta serie.
Ver artículo anterior de esta serie.
Lo del “I” en el título es porque después de ver el ejemplo siguiente está claro que tiene que haber un “II” para continuar con este tema y corregirlo:


Este es el primer intento para ir integrando cosas. Principalmente el cambio de pantallas y el cambio en la escala (dos “sprites”, pequeño y grande).
Poco a poco voy definiendo variables globales que se van utilizando en la mayoría de los scripts. Las más destacadas son un el “sprite”, personaje pequeño, el grande y la pantalla actual en la que tenemos definidos los puntos iniciales, finales, el recorrido posible, de dónde venimos y a donde vamos.
Uno de los cambios más destacados es “el bucle principal del juego” que se intenta ejecutar a razón de 30 frames por segundo:

this.actualizarJuego = function() {
        this.sprite.actualizarEstado();
        //
        var siguientePantalla = pantalla.hayQueCambiarDePantalla(this.sprite.posX, this.sprite.posY);
        if (siguientePantalla != 0) {
            var numPantalla = pantalla.numero;
            pantalla = getPantalla(siguientePantalla);
            if (pantalla.escalaReducida)
                this.sprite = spriteEscalaReducida;
            else
                this.sprite = spriteEscalaNormal;
            if (numPantalla < pantalla.numero)   
                this.sprite.setPosicion(pantalla.getPosicionInicial());
            else
                this.sprite.setPosicion(pantalla.getPosicionFinal());
        }
        //                       
        this.drawFrame();
}

Aquí:
  • Actualizamos el estado del sprite (si hay una tecla pulsada se actualiza la posición en función de ella si es que se puede).  
  • Comprobamos si hay que cambiar de pantalla y si es así colocamos el sprite (pequeño o grande, dependiendo de la pantalla) en su posición inicial o final si venimos de la anterior o la siguiente, respectivamente.
  • Dibujamos todo.
El otro cambio destacable es la inclusión de una función “cargarImagenesPantalla” en el script:
Esta función es la encargada de cambiar las imágenes en función de la pantalla a mostrar. Primero pondrá una ventanita de “cargando …”  y cuando se han cargado las imágenes del fondo y del primer plano (si existe ésta) las mostrará. Aquí tenemos dos problemas que creo que son sencillos de resolver:
  • Cuando la pantalla se está cargando si seguimos pulsando las teclas el muñeco se sigue moviendo y cuando se muestran el muñeco no está al principio. Esto se corrige no aceptando la pulsación de las teclas si estamos cargando.
  • La carga de las pantallas va lenta. Esto se debe corregir cargando previamente las imágenes tanto de la pantalla anterior como de la siguiente al llegar a una determinada. El navegador debería “cachear” (guardar temporalmente) la imagen y al necesitarla ya la deberíamos tener disponible en local (directorio temporal del disco duro).