Aprende a crear juegos en HTML5 Canvas

lunes, 4 de junio de 2012

Distancia entre circulos

En el capítulo anterior, aprendimos a obtener la posición del ratón, y a dibujar un círculo para indicar dicha posición en nuestro canvas. Para realmente poder interactuar entre círculos, crearemos nuestra función Circle, como anteriormente hicimos con Rect:
    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.stroke=function(ctx){
        ctx.beginPath();
        ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,true);
        ctx.stroke();
    }
A nuestra función Circle la crearemos pasándole su posición en X y Y (centro) y su radio.

La función principal para que dos círculos interactuen entre sí, es calcular la distancia entre ellos. Eso nos remonta a nuestras amadas cláses de Física de la secundaria, donde aprendíamos distancia entre vectores. ¿Recuerdan esa fórmula?
d = √ (x₂ - x₁)² + (y₂ - y₁)²
Si alguno de ustedes no la recordaba, no se mortifique, yo tampoco; lo busqué en Google...

Pero ahora que ya tenemos esta formula, podemos crear una función que nos permita calcular la distancia con respecto a otro círculo, la cual agregaremos obviamente al prototipo de la función Circle:
    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));
        }
    }
La formula es clara, solo cambié a códigos lo que presenté antes. Calculo dx restando la posición en X de ambos círculos, y lo mismo con dy. Luego los multiplico por si mismo para elevarlos al cuadrado y saco la raíz cuadrada; ¡Tenemos la distancia!

Lo único que hice fue, después de ello, restar la suma del radio de ambos círculos. Esto nos permitirá obtener la distancia respecto a la circunferencia de ambos círculos, y no de su centro. ¿Por qué hago esto? Les explicaré en un momento...

Probemos ahora nuestra nueva función. Crearemos un círculo player, y un círculo target, que será con el que interactuaremos:
    var player=new Circle(0,0,5);
    var target=new Circle(100,100,10);
De nuestro código anterior, cambiaremos todas las posiciones X y Y por player.x y player.y. Ahora, en nuestra función paint, dibujaremos ambos círculos y al final una línea de texto que indicará la distancia entre ambos objectos:
    function paint(ctx){
        ctx.clearRect(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);
    }
Si corremos nuestro ejemplo, veremos que obtenemos la distancia entre nuestros dos círculos con demasiada precisión en decimales. ¡Demasiados dígitos! Eso es difícil de leer. En la última línea de nuestra función paint, cambiemos player.distance(target) por player.distance(target).toFixed(1). Esta función hará que de todos los decimales en la cantidad, solo se muestre los indicados entre paréntesis. En este caso, 1.

Ahora si, probamos el código, podremos analizar con mayor detalle la distancia de ambos círculos, y como en la función, he restado la suma de la circunferencia de ambos círculos, notaremos que al rozarse ambos círculos, la distancia es calculada como 0. ¿Que ocurre si ambos círculos entran en contacto?

¡Así es! Cuando ambos círculos entran en contacto, la distancia retorna un valor negativo. Con esto, no solo hemos encontrado una función que indica la distancia entre ambos círculos, ¡También hemos creado una forma de saber si dos círculos entran en contacto! De forma similar como hicimos con la función Rect.intersect.

Ahora con este conocimiento, podrás crear videojuegos que involucren círculos. ¡Felicidades!

Para hacer mas obvio el momento en que ambos círculos se entran en intersección, he agregado estas líneas en la función run que cambian el color de fondo por uno mas claro cuando ocurre lo anterior mencionado:
        if(player.distance(target)<0)
            bgColor='#333';
        else
            bgColor='#000';
No olvides declarar la variable "bgColor" al inicio para que funcione, y modificar el inicio de la función "paint" de la siguiente forma:
    function paint(ctx){
        ctx.fillStyle=bgColor;
        ctx.fillRect(0,0,canvas.width,canvas.height);

Código final:

[Canvas not supported by your browser]
(function(){
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    var mousex=0,mousey=0;
    var bgColor='#000';
    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);
        act();
        paint(ctx);
    }

    function act(){
        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(player.distance(target)<0)
            bgColor='#333';
        else
            bgColor='#000';
    }

    function paint(ctx){
        ctx.fillStyle=bgColor;
        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);
    }

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

19 comentarios:

  1. Estimado Karl, este material de ayuda en HTML5 ha sido de gran utilidad para mi, estoy aprendiendo a adentrarme en la programacion HTML5 facil y sencillamente. Espero siga con mas lecciones. Aqui le dejo el link donde estoy subiendo mi primer juego basado en la culebrita (aun en construccion) y voy a seguir subiendo otros juegos que estoy diseñando.

    http://www.espiarts.co.cc/

    Muchas gracias. Leonardo

    ResponderEliminar
  2. Hola,sabe alguien como rotar una imagen y no un contexto?

    ResponderEliminar
    Respuestas
    1. Rotar una imagen como tal, no es posible. Lo que debes hacer, es rotar el contexto, dibujar la imagen, y regresar el contexto a su rotación original. Es una técnica avanzada que aun no he mostrado como hacer. Si necesitas más información al respecto, con gusto podré ayudarte. Suerte ;)

      Eliminar
  3. Hola, gracias por contestar tan pronto, he hecho un contexto encima de otro para poder rotarlo. La imagen que hay dentro rota bien. El problema es que la imagen tiene que estar siempre en 0,0 x,y (coche.drawImage(iCoche,0,0);) para que rote como quiero.

    El problema viene cuando quiero saber si la imagen a colisionado con algo ya que no tengo referencia x,y de ella porque lo que se mueve o rota es el contexto.

    Gracias

    ResponderEliminar
    Respuestas
    1. Si. Debes primer trasladar el contexto al centro donde quieres dibujar la imagen con ctx.translate, y después rotarlo con ctx.rotate. Esto dibujará la imagen centrada. Esto envuelto claro, en un ctx.save y ctx.restore para devolverlo a su forma original.

      Adelantaré el tema de rotación de imágenes para que puedas tener una idea clara.

      Eliminar
  4. como gravo y lo veo en HTML el código de los circulos

    ResponderEliminar
    Respuestas
    1. Sigue las instrucciones en la primer entrada: http://juegoscanvas.blogspot.mx/2012/01/parte-1-dibujando-en-el-canvas.html

      Eliminar
  5. lo grabe en notepad++ pero no me corre porque es
    edite:
    'use strict';
    window.addEventListener('load',init,false);
    var canvas=null,ctx=null;
    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');
    canvas.style.background='#000';
    ctx=canvas.getContext('2d');
    run();
    }

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

    function game(){
    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(player.distance(target)<0)
    canvas.style.background='#333';
    else
    canvas.style.background='#000';
    }

    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.strokeStyle='#f00';
    ctx.beginPath();
    ctx.arc(target.x,target.y,target.radius,0,Math.PI*2,true);
    ctx.stroke();

    ctx.fillStyle='#fff';
    ctx.fillText('Distance: '+player.distance(target).toFixed(1),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.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));
    }
    }
    }

    ResponderEliminar
    Respuestas
    1. ¿Lo guardaste como game.js? ¿Guardaste el html también? ¿Corriste el html?

      Eliminar
    2. guarde en una carpeta ambos archivos en js y HTML y me sale al ejecutar en chome o explore
      'use strict'; window.addEventListener('load',init,false); var canvas=null,ctx=null; 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'); canvas.style.background='#000'; ctx=canvas.getContext('2d'); run(); } function run(){ setTimeout(run,50); game(); paint(ctx); } function game(){ 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(player.distance(target)<0) canvas.style.background='#333'; else canvas.style.background='#000'; } 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.strokeStyle='#f00'; ctx.beginPath(); ctx.arc(target.x,target.y,target.radius,0,Math.PI*2,true); ctx.stroke(); ctx.fillStyle='#fff'; ctx.fillText('Distance: '+player.distance(target).toFixed(1),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.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)); } } }

      Eliminar
    3. Este blog ha sido eliminado por un administrador de blog.

      Eliminar
    4. Verdaderamente me preocupa en hecho que no te funcione. He puesto los archivos en un zip para que los descargues, esperando esto resuelva tus dudas respecto al problema:
      https://sites.google.com/site/juegoscanvas/circulos.zip

      Borré también el correo que dejaste, para evitar que los SPAMbots lo encuentren. Avísame si resuelve tu problema o aun quedan dudas.

      Eliminar
    5. Que no funcione me parece normal en una persona que no sabe escribir. Antes de meterte en programación debería ir a aprender ortografía.

      Eliminar
    6. Comentarios como este no ayudan en nada a los que quieren aprender,
      un hurra para el señor erudito en ortografía!
      ( por cierto mucha ortografía pero nada de ayuda )

      Eliminar
  6. Otra vez yo molestando, estoy haciendo "Pruebas" para aprender mas, se puede hacer un addEventListener a un objeto rectangle como el que usas tu, el que resulta de la funcion constructora, darle click al rectangulo y pase un evento, lo mas facil seria capturar las coordenadas del mouse y si son iguales al de el rectangulo pues que pase el evento con un if, pero que no tecnicamente el objeto rectangulo ¿No es un objeto? y deberia de funcionar el addEventListener(click, funcion_hago_algo_conclick, false)

    ResponderEliminar
    Respuestas
    1. Rectangle no es un objeto DOM, algo que puedas insertar en el HTML; tan solo son una serie de coordenadas que usamos para dibujar una figura en el canvas, y dado que no es un objeto DOM, no contiene la función addEventListener.

      Lo que se puede hacer, es crear una función Contains que reciba dos coordenadas, y funcione de forma similar a Intersects pasa saber si estas coordenadas están dentro del rectángulo, mandar las coordenadas del ratón a esta función, y compararla al mismo tiempo que un lastPress o lastRelease para saber si ha sido presionado, de forma similar a como se muestra en el siguiente tema.

      Eliminar
  7. El verdadero poder del teorema de pitagoras explicado en esta entrada, jaja, ver matemáticas junto a este tipo de realidad es un ejercicio increíble...

    ResponderEliminar
  8. Muy buenos tutoriales, gracias por tu aporte. Ando buscando algo de información para realizar colisiones entre imágenes al pasar el ratón por encima de ellas. Digamos que me gustaría que varias imágenes sean golpeadas por el mouse y choquen entre ellas, con las paredes del lienzo, con aceleración, que vayan cayendo... Estoy encaminado con éste tipo de tutoriales? Sabes dónde podría encontrar información? Gracias!

    ResponderEliminar
    Respuestas
    1. Temo que la mayoría de lo que he encontrado en Google ha sido con engines prediseñados. Mi mejor consejo para lo que tienes en mente, es que busques un tutorial de cómo hacer un juego de billar (Eso te dará la lógica para aceleración y colisiones), y de ahí aplicar otras fuerzas como gravedad será más sencillo.

      ¡Mucha suerte! Felices códigos

      Eliminar