Aprende a crear juegos en HTML5 Canvas

lunes, 17 de septiembre de 2012

Rotación de imágenes

Por petición especial de Sebas, he decidido adelantar este curso unas semanas. Afortunadamente, ya tenemos todas las herramientas básicas necesarias para conseguir esta tarea, así que no habrá problemas en comprender esta parte.

Hay tres técnicas básicas que acompañan a una imagen: Moverla de lugar (Que es lo que hemos estado viendo desde un comienzo), Escalarla (Para hacerla más grande, más chica e invertirla, que lo vimos en el tema pasado Animaciones en el juego) y por último Rotarla. Sin embargo, para conseguir esta última tarea, tenemos que olvidar todo lo que sabemos de las dos primeras.

Rotar una imagen tiene un pequeño grado de complejidad, ya que no existe una función como tal que rote una imagen. Lo que se puede hacer, es rotar el contexto del canvas, y mediante esta técnica, dibujar una imagen rotada. Esto se hace mediante 6 pasos básicos:
  1. Guardar el formato actual del contexto mediante ctx.save().
  2. Trasladar al centro del lugar donde queremos dibujar nuestra imagen con ctx.translate(x,y).
  3. (Opcional) Escalar el contexto para dibujar la imagen escalada mediante ctx.scale(x,y).
  4. Rotar el contexto para dibujar la imagen rotada con ctx.rotate(rads).
  5. Dibujar la imagen, restando la mitad del ancho y del alto para dibujarla centrada, con ctx.drawImage(img,x,y);
  6. Restaurar nuestro contexto a su forma original con ctx.restore().
Hay tres puntos que quiero resaltar en estos datos. Primero que nada, la rotación se hace en radianes, por lo que si tenemos el ángulo en grados, debemos multiplicarlo por Math.PI/180.

Después, observen que subrayo "al centro" en la traslación. Esto es porque la imagen rota desde el punto 0,0 donde está el contexto, y si lo hacemos de otra forma, nos resultará en un efecto no deseado; pueden probarlo si gustan para que comprendan a lo que me refiero. Este punto de centrar la imagen puede ser complicado con los rectángulos, pero tenemos experiencia reciente en dibujar círculos, y como su posición viene desde el centro, aprovecharemos esta propiedad para no complicarnos con cuentas a la hora de dibujar nuestras imágenes.

Por último, noten que en el punto 5 indico que hay que restar la mitad del ancho y alto de la imagen. Esto es claro, por efecto del punto anterior, para que la imagen se dibuje de forma centrada.

Ahora que hemos comprendido esto, ¡Llevemos los conocimientos a la práctica! Tomaremos como base el código de la Parte 2.2. Distancia entre círculos, y borraremos todo lo referente al círculo target. Posteriormente, comenzaremos modificando la función Circle, agregándole dos nuevas variables: scale y rotation.
function Circle(x,y,radius){
 this.x=(x==null)?0:x;
 this.y=(y==null)?0:y;
 this.radius=(radius==null)?0:radius;
 this.scale=1;
 this.rotation=0;
Ahora, importemos la imagen que queremos dibujar. Yo he elegido un simple triángulo que usé en otro juego como nave espacial:
var iShip=new Image();
iShip.src='assets/ship.png';
En la función act, agregaremos una línea para que aumente en 5 la rotación de la imagen:
 player.rotation+=5;
Por último, comentaremos el dibujado del círculo, y agregaremos los seis pasos indicados para dibujar nuestra imagen rotada:
 ctx.save();
 ctx.translate(player.x,player.y);
 ctx.scale(player.scale,player.scale);
 ctx.rotate(player.rotation*Math.PI/180);
 ctx.drawImage(iShip,-iShip.width/2,-iShip.height/2);
 ctx.restore();
De esta forma, tendremos una imagen que rote de forma continua. Ahora con este código, puedes jugar con la rotación y escala de las imágenes. Yo he escalado la imagen al doble de su tamaño en el código final, el cual quedaría de esta forma:

Ejemplo final:

[Canvas not supported by your browser]
'use strict';
window.addEventListener('load',init,false);
var canvas=null,ctx=null;
var mousex=0,mousey=0;
var player=new Circle(0,0,10);
player.scale=2;

var iShip=new Image();
iShip.src='assets/ship.png';

function init(){
 canvas=document.getElementById('canvas');
 canvas.style.background='#000';
 ctx=canvas.getContext('2d');
 run();
}

function run(){
 setTimeout(run,50);
 act();
 paint(ctx);
}

function act(){
 player.x=mousex;
 player.y=mousey;
 player.rotation+=5;
 
 if(player.x<0)
  player.x=0;
 if(player.x>canvas.width)
  player.x=canvas.width;
 if(player.y<0)
  player.y=0;
 if(player.y>canvas.height)
  player.y=canvas.height;
}

function paint(ctx){
 ctx.clearRect(0,0,canvas.width,canvas.height);
 /*ctx.strokeStyle='#0f0';
 ctx.beginPath();
 ctx.arc(player.x,player.y,player.radius,0,Math.PI*2,true);
 ctx.stroke();*/
 ctx.save();
 ctx.translate(player.x,player.y);
 ctx.scale(player.scale,player.scale);
 ctx.rotate(player.rotation*Math.PI/180);
 ctx.drawImage(iShip,-iShip.width/2,-iShip.height/2);
 ctx.restore();
 
 ctx.fillStyle='#fff';
 ctx.fillText('Rotation: '+player.rotation,10,10);
}

document.addEventListener('mousemove',function(evt){
 mousex=evt.pageX-canvas.offsetLeft;
 mousey=evt.pageY-canvas.offsetTop;
},false);

function Circle(x,y,radius){
 this.x=(x==null)?0:x;
 this.y=(y==null)?0:y;
 this.radius=(radius==null)?0:radius;
 this.scale=1;
 this.rotation=0;
 
 this.distance=function(circle){
  if(circle!=null){
   var dx=this.x-circle.x;
   var dy=this.y-circle.y;
   return (Math.sqrt(dx*dx+dy*dy)-(this.radius+circle.radius));
  }
 }
}

26 comentarios:

  1. Gracias crack! podrias hacer uno de colisiones. Cuando una imagen toque otra no puedas avanzar en ese sentido.

    Muchas gracias por los tutos

    ResponderBorrar
    Respuestas
    1. Colisiones ya se han visto desde el juego de la serpiente. Para hacerlo "sólido", solo hay que restar el movimiento original:

      rect1.x+=10;
      if(rect1.intersects(rect2))
      rect1.x-=10;

      Esto sirve muy bien con medidas y movimientos predeterminados, como en el juego de la serpiente, aunque puede dejar pequeños espacios (Casi imperceptibles) en juegos de movimientos no predeterminados, como esos que incluyan aceleración o movimiento libre.

      En el caso de los segundos, si se desea sea exacta la distancia "0" entre ambos objetos, hay que hacer un cálculo un poco más complejo que determine la posición y tamaño de ellos, para saber cuanto desplazarse y así no quede este espacio entre los dos. Espero no necesites este segundo caso por ahora. ¡Suerte!

      Borrar
  2. Hola, estoy haciendo un "juego" por llamarlo algo y tengo algunas dudas. Si el personaje avanza en x, el escenario y el resto de objetos avanzan en -x?. los obejtos aunque parezcan “quietos” se desplazan?
    o es una locura? Gracias

    ResponderBorrar
    Respuestas
    1. Por extraño y loco que parezca, ¡Debo decirte que tienes toda la razón con ello!

      Aunque no hay una función como tal de "mover el escenario en -x", no hasta donde yo se. Las técnicas más populares para hacer este efecto, es mover cada objeto en -x cuando el personaje se desplaza más allá del área visible, o crear variables ScreenX y ScreenY para ver desde dónde se van a dibujar los objetos, y luego restar estas variables a cada uno de los objetos en el juego.

      ¡Suerte!

      Borrar
    2. En Física este fenómeno es conocido como "Movimiento Relativo".

      Borrar
  3. Hola! Como se hacen las colisiones con objetos que rotan??, si rota la imagen no rota su alto y ancho claro y siempre se mantiene fijo por lo que las colisiones son extrañas. Hay alguna forma de "rotar" eso. O crear un cuadrado que rote? en fin..

    Gracias por tu blog, esta genial tio y las explicaciones se salen! Enhorabuena!

    ResponderBorrar
    Respuestas
    1. Esa es otra razón importante por la que usamos círculos en los objetos que rotan, en lugar de rectángulos. Al ser una colisión circular, esta tendrá siempre el mismo área, independientemente de la rotación de la imagen.

      Solo asegúrate que la imagen llene dicho circulo en la mayor cantidad posible y que zonas importantes de impacto no queden fuera de dicha área.

      Borrar
  4. hola, tus tutoriales estan muy buenos muchas gracias.
    TEngo una pregunta, Como se haria para animar Sprites??
    por ejemplo. el paso de un robot consta de 17 sprites, como podria hacer para hacer una secuencia de sprites al apretar una tecla.

    ResponderBorrar
    Respuestas
    1. Ese tema ya lo habíamos tratado antes en http://juegoscanvas.blogspot.mx/2012/04/animaciones-en-el-juego.html

      Tan solo, para que sea animado no siempre, si no cuando se presiona la tecla, debes precisamente hacer un condicional if para detectar si esa tecla es presionada, y de lo contrario, dibujar solo la sección que se demuestre como "quieto". ¡Suerte!

      Borrar
  5. Muy buenos todos los tutoriales, espero más para poder hacer algo mas complejo :) gracias.

    ResponderBorrar
  6. Buenos días,
    Gracias por la información, te comento que estoy tratando de hacer un juego mas complejo el cual tiene varias imágenes y cuando los quiero jugar por Internet tarda en cargar las imágenes no mucho pero el juego comienza aun sin haberlas cargado, mi pregunta es como hacer un "loading" en donde primero se carguen las imágenes y luego empieze el juego.
    ademas de eso estoy muy interesado de que funcione para celulares, pero tengo un problema ya que el celular "ihpone" cuando deslizo el dedo como que me mueve la pantalla aunque este en pantalla completa, tenes alguna solución a esto como para que el juego se vea como si fuera un juego de la appstore?

    Muchas Gracias
    Matias

    ResponderBorrar
    Respuestas
    1. ¡Buenos días Matias! ¡Y vaya que te has ido a aspectos muy avanzados!

      Tras investigar un poco, encontré como hacer el loading que solicitas. Debes crear una variable que contenga el número de imágenes cargadas, y a cada imagen, debes asignarle el evento onload que llame a la función ImageLoaded, que sumará uno a la variable de imágenes cargadas:

      ---
      var myImage=new Image();
      myImage.src='myimage.png';
      myImage.onload=ImageLoaded;
      [...]

      function ImageLoaded(){
      loaded++;
      }
      ---

      Finalmente en run pones una condicional, en la que tu juego no corra mientras las imágenes aun no se hayan cargado. Supongamos que tienes 28 imágenes a cargar (tendrás que contarlas manualmente):

      ---
      function run(){
      if(loaded<28){

      }
      else if(!PAUSE){
      //Tu juego va aquí.
      }
      }
      ---

      Puedes hacer algo similar con la función paint, para que en pantalla se dibuje un preloader o un splash screen.

      Finalmente, para que no se mueva el canvas de pantalla completa, intenta agregar un position:fixed al canvas, y un overflow:hidden al body. Espero esto solucione el problema. Suerte :)

      Borrar
    2. Hola gracias,
      Justo yo había pensado lo mismo mira lo que estoy haciendo
      http://juegosdinatale.comuv.com/Caminantes/

      lo quiero hacer para celulares, pero tengo un par de problemas con el táctil pero para compu ya funciona maso menos, voy a hacer que el fondo se mueva a medida que avanza el personaje
      Muchas Gracias,
      Gracias al tutorial estoy haciendo estos juegos.

      Borrar
    3. ¡Muy buen progreso con el juego! Sin embargo, no he visto respuestas de la pantalla táctil. ¿Aun está por ser implementada, o debía presionar algún lugar específico que no he encontrado?

      ¡Muchas felicidades y mucha suerte! Si necesitas ayuda, aquí seguimos para resolver tus dudas.

      Borrar
    4. Hola, NO no esta implementado, por ahora solo con teclado, (flechas y barra)

      Saludos!

      Borrar
    5. Bueno. Si necesitas ayuda con el táctil, avísame cuando lo implementes para ver como puedo ayudarte. ¡Suerte!

      Borrar
    6. Hola, como estaS?
      ya esta maso menos el que estaba haciendo no lo voy a seguir. estoy pensando en empezar otro el que esta funciona para pantalla tactil tambien.
      ahora quiero hacer un que sea en 3d tenes algun tutorial para canvas en 3d?

      Saludos

      Borrar
    7. Actualmente no tengo tutoriales sobre 3D, pero existen muchos en Internet. Puedes buscarlos como "WebGL", esa es la tecnología implementada por los navegadores para mostrar 3D sobre un canvas.

      ¡Mucha suerte con ello! Si encuentras un buen lugar para aprenderlo, me gustaría tu comentario. Estoy seguro que podría ayudar a muchos otros lectores del blog ;)

      Borrar
  7. Muchísimas gracias! Me ha servido de mucho y es muy simple ^^

    ResponderBorrar
  8. oye no sabes el problemaso que me acabas de ayudar a resolver ya me estaba volviendo loco pero gracias

    ResponderBorrar
  9. Muchas gracias por el blog, me ha servido mucho, pero tengo un problema con la pantalla tactil del celular, es como si estubiera desplazada unos pixeles hacia arriba, tengo un boton en el canvas y tengo que presionar debajo del boton para que funcione, pero en la pc me funciona genial,¿me puedes ayudar?

    ResponderBorrar
    Respuestas
    1. ¿Que móvil y navegador estás usando? Veré si puedo encontrar el origen del problema y alguna posible solución.

      Borrar
    2. uso un alcatel b pop con androi 2.3 y firefox

      Borrar
    3. Realiza una prueba rápida: Utiliza "clientX" y "clientY" en lugar de "pageX" y "pageY". Avísame si esto resuelve el problema, para analizarlo a más detalle, pero te advierto que no será una solución absoluta, pues tiene un efecto colateral. Por ahora, solo dime si esto ayuda con tu problema.

      Borrar
  10. Quisiera preguntar, si tengo la intención de usar el ejemplo de sprites y si quisiera que la imagen que publique en el canvas estuviera en Mirror (de manera inversa). Es decir si la imagen es una cara que mira hacia la derecha, como se le puede hacer para que mire a la izquierda.

    Gracias

    ResponderBorrar
    Respuestas
    1. Utilizando el método presente, utiliza la siguiente línea en la escala:

      ctx.scale(-1,1);

      Borrar