Aprende a crear juegos en HTML5 Canvas

lunes, 24 de septiembre de 2012

Desplazamiento angular

Anteriormente creamos un juego completo que involucra intersecciones circulares y eventos del ratón. Para continuar con este tema, veremos otra función importante que involucra intersecciones circulares: el Movimiento Angular. Para el ejemplo de hoy, tomaremos el código visto en la entrada Distancia entre círculos.

Hasta el momento, hemos visto como desplazar nuestros objetos en el eje X o en el eje Y. También podríamos desplazarlo a la diagonal cambiando el valor de ambos al mismo tiempo. ¿Pero como hacer si deseamos mover el objeto en un ángulo definido? ¿Y si deseamos que se desplace siempre la misma distancia, sin importar el ángulo de movimiento?

Para desplazar un objeto, dado un ángulo, es necesario obtener el seno del ángulo para obtener su desplazamiento en la coordenada Y, y el coseno del ángulo para obtener su desplazamiento en la coordenada X. Para conseguir estos valores, usaremos las funciones Math.sin y Math.cos, dado nuestro ángulo:
Math.cos(angle);
Math.sin(angle);
A estos valores multiplicamos la distancia que queremos se desplace nuestro objeto, y obtendremos así el desplazamiento angular final; Estos valores simplemente deberán ser sumados a la posición actual de nuestro objeto. Con esta información, podemos crear una función dentro de nuestro círculo que se encargue de este movimiento:
    Circle.prototype.move=function(angle,speed){
        if(speed!=null){
            this.x+=Math.cos(angle)*speed;
            this.y+=Math.sin(angle)*speed;
        }
    }
Con esta fórmula, podemos desplazar cualquier objeto en nuestra pantalla con un movimiento angular. Es importante resaltar que los ángulos recibidos deben ser en radianes, por lo que si deseas indicar el ángulo en grados, debes multiplicarlo por la constante PI sobre 180:
target.move(60*(Math.PI/180),deltaTime*100);
La función de arriba desplaza nuestro objetivo a una velocidad de 100 pixeles por segundo, en un ángulo de 60 grados. Ahora que sabemos como hacer este desplazamiento, veamos una forma dinámica de aprovecharlo; haremos que nuestro objetivo siga al jugador siempre que esté lejos de él.

Para empezar, necesitaremos conocer el ángulo entre el objetivo y nuestro jugador. Para obtener el ángulo entre dos puntos, necesitamos conocer el arco tangente entre la diferencia de las posiciones en X y la diferencia de las posiciones en Y. Agregamos la función que se encarga de esto:
    Circle.prototype.getAngle=function(circle){
        if(circle!=null)
            return (Math.atan2(circle.y-this.y,circle.x-this.x));
    }
Asegurémonos que el ángulo es el correcto, dibujando este valor en pantalla. Como el resultado es entregado en radianes, si deseas ver el valor expresado en grados, deberás multiplicarlo por 180 sobre la constante PI (Proceso inverso a lo que hicimos anteriormente):
        ctx.fillText('Angle: '+(player.getAngle(target)*(180/Math.PI)).toFixed(1),10,20);
Ya con este valor, podremos asignar este desplazamiento a nuestro objetivo, siempre que esté lejos del jugador:
        if(target.distance(player)>0){
            var angle=target.getAngle(player);
            target.move(angle,deltaTime*100);
        }
Recuerda que el valor es recibido en radianes en la primer función, por lo que no es necesario convertirlo en ningún punto antes de entregarlo a la segunda.

Probamos el código. Con esto, ya tenemos al objetivo siguiendo siempre a nuestro jugador. Por supuesto, un juego en que el objetivo vuele a donde estás tú no es divertido, pero hay miles de funciones que se pueden asignar a este tipo de movimiento... Quizá misiles teledirigidos...

Código final:

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

    function init(){
        canvas=document.getElementById('canvas');
        ctx=canvas.getContext('2d');
        canvas.width=300;
        canvas.height=200;
        
        run();
    }

    function run(){
        requestAnimationFrame(run);
            
        var now=Date.now();
        var deltaTime=(now-lastUpdate)/1000;
        if(deltaTime>1)deltaTime=0;
        lastUpdate=now;
        
        act(deltaTime);
        paint(ctx);
    }

    function act(deltaTime){
        player.x=mousex;
        player.y=mousey;
        
        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;
        
        if(target.distance(player)>0){
            var angle=target.getAngle(player);
            target.move(angle,deltaTime*100);
        }
    }

    function paint(ctx){
        ctx.fillStyle='#000';
        ctx.fillRect(0,0,canvas.width,canvas.height);
        
        ctx.strokeStyle='#0f0';
        player.stroke(ctx);
        ctx.strokeStyle='#f00';
        target.stroke(ctx);
        
        ctx.fillStyle='#fff';
        ctx.fillText('Distance: '+player.distance(target).toFixed(1),10,10);
        ctx.fillText('Angle: '+(player.getAngle(target)*(180/Math.PI)).toFixed(1),10,20);
    }

    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;
    }
        
    Circle.prototype.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));
        }
    }

    Circle.prototype.getAngle=function(circle){
        if(circle!=null)
            return (Math.atan2(circle.y-this.y,circle.x-this.x));
    }

    Circle.prototype.move=function(angle,speed){
        if(speed!=null){
            this.x+=Math.cos(angle)*speed;
            this.y+=Math.sin(angle)*speed;
        }
    }

    Circle.prototype.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }

    window.requestAnimationFrame=(function(){
        return window.requestAnimationFrame || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame || 
            function(callback){window.setTimeout(callback,17);};
    })();
})();

23 comentarios:

  1. Esto es de hecho la base de la IA de la mayoría de los juegos 3d
    No solo en el tema de seguimiento (sin mencionar que el seguimiento es el alma de casi todo un juego), sino también en la parte cinematográfica, donde los programadores más avispados se aseguran de que el personaje que hable (por ejemplo) se tome la molestia siquiera de buscar nuestra cara para hablarnos, y no quedarse como si le hablaran a la pared cuando nos movemos del lugar de donde deberíamos estar
    Sorprende ver como de una cosa tan sencilla ofrece tanto a la industria
    saludos

    ResponderEliminar
  2. oye, para añadir el comportamiento, tras la colision, en que cada circulo toma una direccion diferente ?

    ResponderEliminar
    Respuestas
    1. Debes obtener el ángulo entre la colisión de ambos objetos, y la velocidad de cada uno de ellos. Entonces asignas a cada objeto la velocidad del objeto opuesto en dirección opuesta al objeto con el que has colisionado, resultando así el efecto que deseas.

      Eliminar
    2. ¿se podria decir que el angulo de la colision es el mismo arcotangente?

      Eliminar
  3. Jose Romualdo Villalobos Perez dijo...
    Hola querido Karl, entiendo que tu blog trata mas que todo de programacion, pero no entiendo que es el "arcotangente arctgh" en la realidad, intentare decir lo que entiendo,
    1. El arcotangente se abrevia tambien arctgh
    2. El arcotangente es un asunto de trigonometria mas especificamente al tema de las cordenadas polares
    3. Las cordenadas polares se prodria decir asi que se usan para medir el angulo que hay entre dos puntos.
    4. Las cordenadas polares trabajan con dos variables r y angle

    ¿Puedes explicarme la realidad que se esconde a mis ciegos ojos en el arcotangente?

    ResponderEliminar
    Respuestas
    1. En realidad no comprendo cual es tu duda, si todo parece bien definido en los puntos que has tocado. El arcotagente es la operación inversa a la tangente, que si esta retorna un vector dado el otro vector y el ángulo, dicha propiedad inversa nos permite obtener el ángulo dados ambos vectores.

      Eliminar
    2. Pero no entiendo por que arco-tangente, si también hay arco-seno o arco-secante etc... discúlpame, tu explicación me confunde un poco en esta parte "que si esta retorna un vector dado el otro vector y el ángulo", aun no logro ver toda la realidad oculta ( a mis ojos ) que hay en el tema del arco-tangente, mira yo el seno y el coseno los entiendo de manera practica mediante las siguientes reflexiones.

      Seno : dado un triangulo unitario y tomando como referencia el ángulo alpha ubicado en el origen( 0, 0 ) del plano, es la altura ( coordenada en Y ) que tiene el cateto opuesto al ángulo alpha

      Coseno : dado un triangulo unitario y tomando como referencia el ángulo alpha ubicado en el origen( 0, 0 ) del plano, es la longitud ( coordenada en X ) que tiene el cateto adyacente al ángulo alpha

      La verdad es que lo que acabo de decir sin haber visto imágenes seria muy difícil de comprender( para mi ), bueno, el punto es, ¿no conoces alguna definición así de practica para dummies pero que defina el arco-tangente?, otra pregunta ¿por que específicamente usas arco-tangente, por que no usar arco-seno o arco-secante?, esto solo me deja una enseñanza y es que deberé dedicarle mas tiempo a la trigonometría, aun así quedo atento a tu respuesta...

      Eliminar
    3. ¡Ya comprendo tu pregunta!

      Arco seno: Te permite obtener el ángulo dada la hipotenusa y el cateto opuesto.

      Arco coseno: Te permite obtener el ángulo dada la hipotenusa y el cateto adyacente.

      Nosotros desconocemos la hipotenusa (La distancia diagonal desde el origen al destino), pero conocemos el cateto adyacente (Que equivale a la distancia en X entre origen y destino), y el cateto opuesto (Que equivale a la distancia en Y entre origen y destino). Dados estos dos valores, solo nos queda una forma de obtener el valor que deseamos:

      Arco tangente: Te permite obtener el ángulo dado el cateto adyacente y el cateto opuesto.

      Espero esto deje más en claro ahora tu duda.

      Eliminar
    4. Esta imagen ilustra de forma visual como funcionan seno, coseno y tangente:

      http://www.mathematicsdictionary.com/spanish/vmd/images/t/trigonometricratios.gif

      Recuerda que los arcos, son las funciones opuestas a estas operaciones.

      Eliminar
    5. Muchas gracias, tu respuesta es sorprendentemente clara y practica, ahora podre profundizar en el tema con mayor facilidad.... agradecimientos infinitos...

      Eliminar
    6. Hola, quisiera compartir un truco sencillo para aquellos que quieran aprender todas las razones trigonometricas

      Paso 1
      saber el nombre de las funciones trigonometricas en el siguiente orden
      seno, coseno, tangente, cotangente, secante, cosecante

      Paso 2
      aprender el siguiente juego de palabras( hay q respetar el orden)
      co, ca, co, ca, h, h

      Paso 3
      Corresponder uno a uno los elementos del paso uno con los del paso dos de la siguiente manera
      sin = co
      cos = ca
      tg = co
      ctg = ca
      sec = h
      csec = h

      Paso 4
      Escribir en el denominador lo mismo pero en sentido contrario( ahora contad de abajo hacia arriba para ahorrar ram )
      sin = co / h
      cos = ca / h
      tg = co / ca
      ctg = ca / co
      sec = h / ca
      csc = h / co

      Eliminar
  4. Karl mira estoy moviendo un objeto con lo siguientes valores
    var x = Math.cos( Math.PI / 2 );
    var x = Math.sin( Math.PI / 2 );
    el objetivo es que el objeto se mueva hacia arriba y en cambio se mueve hacia abajo, pero cuando uso (3 * Math.PI / 2) las cosas funcionan, quisiera saber ¿por que esta invertido este sistema de grados?, ¿a ti no te molesta trabajar con la circunferencia de este modo?

    ResponderEliminar
    Respuestas
    1. Investigando más a fondo este problema, me he dado cuenta que hay un error en mi función getAngle, pues debe restarse al punto destino el punto origen y no al revés. Ya estoy corrigiendo este error a lo largo del blog; tu función debería funcionar bien tras hacer esta corrección. Gracias por hacerme notar este detalle.

      Eliminar
    2. Sorprendente, ¿podrías explicarme como has llegado a dicha conclusión?, yo ni siquiera se por que me agradeces jaja, ¿es un problema de signos?, de ser así ¿se podría solucionar usando el valor absoluto?, bueno ya lo tendré que entender luego por mi cuenta, o si quieres me ayudas un poco.
      NOTA: si es una pregunta larga de responder que te ocupe mucho tiempo por favor ignórala, en todo caso creo que la lógica para llegar a tu conclusión no debe ser muy difícil...
      Agradecimientos infinitos por tu blog, me despido temporalmente...

      Eliminar
    3. En realidad fue que una vez que me di cuenta de que había un error, busqué la fórmula para compararla con la mía. Yo hacía Math.atan2(y1 - y2, x1 - x2), y la formula en realidad es Math.atan2(y2 - y1, x2 - x1)... Ya le he corregido y ahora ese problema no deberá afectar ningún otro código.

      ¡Mucha suerte! Espero saber pronto de ti por estos lugares.

      Eliminar
  5. Hola querido Karl he regresado de mi vuelta al mundo en 10 horas y he estado reflexionando mucho acerca de la función trigonométrica tangente, personalmente creo que en el JavaScript las cosas están un poco invertidas, me explico...

    Como bien sabes, la tangente no existe en el punto Math.PI / 2 ni -Math.PI / 2 y bueno mágicamente en el JavaScript no hay ningún problema aparente con estos valores, he estado jugando con tu codigo usando el método Math.atan() en lugar del método Math.atan2() y resulta que el movimiento solo se efectúa en el eje Y, mas datos misterioso a continuación

    Usando el método Math.atan()
    1. El objeto seguidor solo se mueve en el eje Y
    2. Existe la tangente de Math.PI / 2
    3. El objeto seguidor no se mueve diagonalmente
    4. La tangente de Math.PI / 2 es un valor negativo ( MISTERIO DE ALTA PRIORIDAD )
    5. La tangente de -Math.PI / 2 es un valor positivo ( MISTERIO DE ALTA PRIORIDAD )

    Usando el método Math.atan2()
    1. La tangente de Math.PI / 2 es un valor negativo ( MISTERIO DE ALTA PRIORIDAD )
    3. La tangente de -Math.PI / 2 es un valor positivo ( MISTERIO DE ALTA PRIORIDAD )
    NOTA: bueno con atan2() las cosas son un poco mas entendible investigando un poco he descubierto que el método tiene un poco de intervención divina

    ResponderEliminar
    Respuestas
    1. Math.atan recibe sólo un valor, y/x si mi memoria no me falla. Dado que a veces x puede ser 0, y esa división causaría un error, se creó especialmente la función Math.atan2, para manejar el cálculo de vectores entre dos puntos, algo muy común en tareas de programación, sin tener que pasar por el filtro de los errores un que pueden generar los límites de 0 y π...

      Para manejar los valores de forma correcta, la rotación resultante en radianes se da en contra-reloj, lo que quizá explique por qué los límites devuelvan valores con signos opuestos, aunque al final, apuntan hacia el mismo ángulo de desplazamiento, por lo que poco importa tener que manipular dicho valor.

      La información la he obtenido de Mozilla Developers Network y Wikipedia. Si buscas atan2, puedes obtener más información detallada al respecto.

      Eliminar
    2. Referencias:

      https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2

      http://en.wikipedia.org/wiki/Atan2

      Eliminar
    3. Muchas gracias por todo, revisando ahora puedo decir que este tema es tan facil como..... um bueno no hay comparacion, pero que lo comprenda ahora se debe en gran parte a tus respuestas, agradecimientos infinitos....

      Eliminar
  6. oye que tal un tuto sobre raycasting en 2D con canvas, algo para entender esto :

    http://jsfiddle.net/nLMTW/

    ResponderEliminar
  7. Hola, par rotar un punto al rededor de otro....

    ResponderEliminar
    Respuestas
    1. Todo depende de si deseas algo visual, o realmente afectar a un objeto en el juego. El el primer caso, es tan sencillo como usar las funciones "translate" y "rotate" del canvas, poniendo el origen de la imagen a dibujar fuera de la misma.

      Sí por el contrario, deseas aplicar esta acción a un objeto, muy posiblemente debas aplicar las leyes de física relacionadas al torque.

      Eliminar