Colisiones XNA usando BoundingBox – Rebotando pelotas


En los juegos una de las cosas más importantes es calcular las colisiones, entre más real se vea la colisión el juego va a tener mejor apreciación.

Ya vimos como mover y animar los personajes, en este ejemplo voy  a mostrar con un ejemplo de pelotas rebotando y chocando entre ellas.

XNA nos provee una estructura llamada BoundingBox , esta estructura representa una caja rectangular o cuadrada, dependiendo de las medidas del personaje que queremos calcular. Aunque la estructura Boundingbox esta creada para objetos 3D, podemos usarla en objetos 2D, simplemente le decimos que el eje de la Z es 0.

El objeto BoundingBox necesita 2 parámetros, dos objetos Vector3 que serán el punto mínimo y el punto máximo del objeto, también tenemos dos métodos que nos serán útiles:

–          Contains: Este método nos dice si un objeto contiene a otro, si se encuentra dentro de su superficie

–          Intersects. Este método nos dice si dos objetos han colisionado.

El método Intersects tiene varios constructores, uno de ellos devuelve un valor verdadero o falso, dependiendo de si están haciendo colisión los objetos, hay otro que devuelve un rectángulo que se crea en el área donde hubo la colisión, en nuestro caso vamos a usar el primero.

Abrimos un proyecto nuevo en Visual Studio, vamos a crear una clase nueva llamada Sprite, que va a ser la que tendrá las propiedades y métodos del Sprite, propiedades como la posición, la textura, la velocidad, entre otros.

La clase va a quedar así:

class Sprite
{
      public Texture2D imagen;
      public Vector2 posicion;
      public Vector2 velocidad = Vector2.Zero;
      Vector2 ventanaTamano = Vector2.Zero;
      float escala = 1.0f;
      SpriteEffects efectos = SpriteEffects.None;
      private SpriteBatch batch;
      public float Escala
      {
            set
            {
                  escala = value;
            }
      }
      Vector2 origen = Vector2.Zero;
      public Vector2 Origen
      {
            set
            {
                  origen = value;
            }
      }
      public Sprite(GraphicsDeviceManager graficos, Vector2 _posicion)
      {
            this.posicion = _posicion;
            this.batch = new SpriteBatch(graficos.GraphicsDevice);
            this.ventanaTamano = new Vector2(graficos.PreferredBackBufferWidth, graficos.PreferredBackBufferHeight);
      }
      public void cargarImagen(ContentManager contenido, String nombreImagen)
      {
            this.imagen = contenido.Load<Texture2D>(nombreImagen);
      }
      public void Mover()
      {
            posicion.X += velocidad.X;
            posicion.Y += velocidad.Y;
      }
      public void ponerPosicion(float x, float y)
      {
            posicion.X = x;
            posicion.Y = y;
      }
      public void Dibujar()
      {
            batch.Begin();
            batch.Draw(imagen, posicion, null, Color.White, 0.0f, origen, new Vector2(escala, escala), efectos, 0.0f);
            batch.End();
      }
}

Cuando creamos un objeto Sprite, enviamos el dispositivo grafico, y la posición en donde va a ser dibujado.

La variable VentanaTamano, nos es útil para luego usarla en las colisiones con la pantalla, evitar que los objetos se salgan de la pantalla.

La función cargarImagen, hace lo mismo que hacíamos en el método LoadContent.

La función Mover, aumenta la posición dependiendo de la velocidad que indiquemos.

Ahora para usarla, en la clase Game1.cs declaramos el objeto y lo inicializamos:

Sprite s_pelota;

En el Initialize():

s_pelota = new Sprite(graphics, new Vector2(graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2));
s_pelota.velocidad = new Vector2(100, 15);

En el LoadContent():

s_pelota.cargarImagen(Content,"pelota2");

En el Update():

s_pelota.Mover();

En el Draw():

s_pelota.Dibujar();

Si ejecutamos, veremos a la pelota desde el centro de la pantalla moverse, pero desaparece en cuanto se salga de la pantalla, para evitar eso vamos a crear un método que verifique que la pelota no se salga de la pantalla, para eso volvemos  a la clase Sprite y creamos la función ColisionPared:

public void ColisionPared(Vector2 Ventana)
{
      if (posicion.X >= (Ventana.X - imagen.Width))
      {
            // choco con el lado derecho
            velocidad.X = velocidad.X * (-1);
      }
      else if (posicion.X <= 0)
      {
            // choco con el lado izquierdo
            velocidad.X = velocidad.X * (-1);
      }
      else if (posicion.Y >= (Ventana.Y - imagen.Height))
      {
            // choco con la ventana abajo
            velocidad.Y = velocidad.Y * (-1);
      }
      else if (posicion.Y <= 0)
      {
            // choco con la ventana arriba
            velocidad.Y = velocidad.Y * (-1);
      }
}

La función recibe como parámetro las dimensiones de la ventana, las verificaciones que se hacen son:

–          Si la pelota alcanza el ancho de la pantalla a la derecha, se calcula con Ventana.X – el ancho de la textura, para hacer la colisión más exacta.

–          Si la pelota tiene una posición con un valor negativo en la X, significa que se encuentra en la izquierda de la pantalla

–          Si la pelota alcanza el valor Y de la ventana, significa que ha bajado, para hacer la colisión exacta se resta el valor de Y a el alto de la textura

–          Si la pelota tiene una posición con un valor negativo en la Y, significa que se encuentra arriba de la pantalla

En cualquiera de los casos multiplicamos la velocidad por -1, para que si va hacia en una dirección al chocar se devuelva en la dirección contraria a la que iba.

En algunos casos, la pelota va a chocar con una esquina de la pantalla y puede ocurrir un problema, por ejemplo pongamos que la pelota tenga una posición inicial de 10,10, luego que tenga la velocidad de (-40,-40) para hacer que se dirija hacia arriba y a la izquierda. Al ejecutar el ejemplo no veremos a la pelota rebotar como queremos, por el contrario veremos algo como:

 

Esto sucede porque en la función de verificar si colisiona con la pared verificamos una sola sentencia a la vez, o sea si choca a la derecha primero, luego volvemos a mover la pelota y verificamos si choco arriba, al hacerlo el eje de la X tendrá un valor mucho mayor que la velocidad, por lo tanto siempre estará en una posición negativa.

Modificamos la función y ahora quedara así:

public void ColisionPared(Vector2 Ventana)
{
      if (posicion.X >= (Ventana.X - imagen.Width))
      {
            // choco con el lado derecho
            velocidad.X = velocidad.X * (-1);
            posicion.X = (Ventana.X - imagen.Width);
      }
      if (posicion.X <= 0)
      {
            // choco con el lado izquierdo
            velocidad.X = velocidad.X * (-1);
            posicion.X = 0;
      }
      if (posicion.Y >= (Ventana.Y - imagen.Height))
      {
            // choco con la ventana abajo
            velocidad.Y = velocidad.Y * (-1);
            posicion.Y = (Ventana.Y - imagen.Height);
      }
      if (posicion.Y <= 0)
      {
            // choco con la ventana arriba
            velocidad.Y = velocidad.Y * (-1);
            posicion.Y = 0;
      }
}

Ahora en la función Mover de la clase Sprite, llamamos la función:

public void Mover()
{
      posicion.X += velocidad.X;
      posicion.Y += velocidad.Y;
      ColisionPared(ventanaTamano);
}

Para hacer las cosas más llamativas, voy a poner a rebotar muchas pelotas en la pantalla, para hacerlo más fácil podemos crear una lista de objetos Sprite.

En la clase Game1.cs declaramos la lista:

List<Sprite> sprites = new List<Sprite>();

Inicializamos los objetos, por el momento voy a crear 20 pelotas, cada pelota tendrá una posición y velocidad aleatoria, la posición estará entre los tamaños de la Ventana:

Random x = new Random();
for (int i = 0; i <= 20; i++)
{
      sprites.Add(new Sprite(graphics, new Vector2((float)(x.NextDouble() * graphics.PreferredBackBufferWidth), (float)(x.NextDouble() * graphics.PreferredBackBufferHeight))));
}
foreach (Sprite objeto in sprites)
{
      objeto.velocidad = new Vector2((float)(x.NextDouble() * 50), (float)(x.NextDouble()* 50));
}

En el método LoadContent:

foreach (Sprite objeto in sprites)
{
      objeto.cargarImagen(Content, "pelota2");
}

En el método Draw dibujamos todos los objetos:

foreach (Sprite objeto in sprites)
{
   objeto.Dibujar();
}

Y para finalizar en el método Update movemos las pelotas:

foreach (Sprite objeto in sprites)
{
      objeto.Mover();
}

Y al ejecutar el ejemplo:

 

Para hacer las colisiones entre las pelotas, vamos a usar los BoundingBox, creamos una función en la clase Sprite, que recibirá como parámetros dos objetos Sprite:

public Boolean Choque_BoundingBox(Sprite objeto1, Sprite objeto2)
{
      Vector3 min1 = new Vector3(objeto1.posicion.X, objeto1.posicion.Y, 0);
      Vector3 max1 = new Vector3((objeto1.posicion.X + objeto1.imagen.Width), (objeto1.posicion.Y + objeto1.imagen.Height), 0);
      Vector3 min2 = new Vector3(objeto2.posicion.X, objeto2.posicion.Y, 0);
      Vector3 max2 = new Vector3((objeto2.posicion.X + objeto2.imagen.Width), (objeto2.posicion.Y + objeto2.imagen.Height), 0);
      // Se Multipla por 0.99 para agregar el 1% de tolerancia
      BoundingBox box1 = new BoundingBox(min1, max1 * new Vector3(0.99f, 0.99f, 0));
      BoundingBox box2 = new BoundingBox(min2, max2 * new Vector3(0.99f, 0.99f, 0));
      if (box1.Intersects(box2))
      {
            return true;
      }
      else
      {
            return false;
      }
}

Ya habíamos dicho que el bounding box recibe como parámetro 2 Vector3 , al principio de la función calculamos los puntos mínimo y máximo de los objetos, luego declaramos los dos BoundingBox, estamos multiplicando por 0.99 el punto máximo para adicionar un poco de tolerancia en la colisión, luego ejecutamos el método Intersects y si es verdadero, devolvemos true para evidenciar que hay colisión.

Para hacer uso de la función, en el método Update, vamos a recorrer la lista de las pelotas, y a compararlas 1 por 1, para ello hacemos lo siguiente:

for (int i = 0; i <= sprites.Count - 1; i++)
{
      for (int j = i + 1; j <= sprites.Count - 1; j++)
      {
            if (sprites[i].Choque_BoundingBox(sprites[i], sprites[j]))
            {
                  // colisionó
                  sprites[i].velocidad = Vector2.Zero;
                  sprites[j].velocidad = Vector2.Zero;
            }
      }
}

El recorrido lo hacemos con dos for, el primero va a empezar desde 0 y el segundo desde 1, o desde el siguiente, para explicarlo mejor:

1 Ciclo:
0 – 1
0 – 2
0 – 3

0 – 20
2 Ciclo:
1 – 2

1 – 20

Con el anterior ciclo hacemos que las validaciones se hagan una sola vez, y que no se verifique una pelota con ella misma, al chocar una pelota con otra, ambas son detenidas, para ver el efecto mucho mejor, disminuiremos la velocidad, haciendo que el Random se multiplique por un número pequeño, lo voy a hacer con una velocidad máxima de 2, al hacerlo veremos:

 

Como vemos, en algunos casos la colisión no es exacta, el problema del BoundigBox es que es útil para objetos rectangulares, ya que si lo aplicamos a objetos circulares, no nos va a dar una precisión exacta, debido a que puede haber colisión de las esquinas de los cuadrados, pero a la vista no la vemos.

Nosotros podemos usar nuestra propia función de colisión de pelotas, podemos aplicar matemática a nuestro ejemplo, así que a desempolvar los cuadernos de cálculo y matemáticas.

Para determinar si dos círculos han colisionado hay que determinar la distancia entre los radios de cada circulo, sabemos que el radio es la distancia entre el centro del circulo y los bordes, así que si la distancia entre los dos centros es menor a la sumatoria de los dos radios, hay colisión, en una imagen se visualizará mejor:

 

Para hallar la distancia entre dos puntos, se usa la formula de Pitágoras:

 

Para los que no saben, la raíz cuadrada es uno de los cálculos que más recursos de maquina consume, debido a la exactitud y el control de errores que se deben manejar en el resultado, para optimizar la formula y eliminar la raíz cuadrado, elevamos todo al cuadrado.

Como resultado nos da:

(x2 – x1)2 + (y2 – y1)2 <= (R1+ R2)2

Con esta explicación nuestro método de verificar las colisiones entre círculos nos queda:

public Boolean Choque_circulos(Sprite objeto1, Sprite objeto2)
{
      float dx = objeto2.posicion.X - objeto1.posicion.X;
      float dy = objeto2.posicion.Y - objeto1.posicion.Y;
      Int32 radio1 = objeto1.imagen.Width / 2;
      Int32 radio2 = objeto1.imagen.Width / 2;
      Int32 radioTotal = radio1 + radio2;
      if ((Math.Pow(dx, 2) + Math.Pow(dy, 2)) <= Math.Pow(radioTotal, 2))
      {
            return true;
      }
      else
      {
            return false;
      }
}

Al ejecutar el ejemplo, vemos que la colisión es más exacta:

 

Para ver mejor en acción nuestro método de colisiones, he aumentado la velocidad a 10 y en la función de actualizar, en cuanto haya una colisión voy a invertir la velocidad de los objetos:

if (sprites[i].Choque_circulos(sprites[i], sprites[j]))
{
// choco
sprites[i].velocidad = sprites[i].velocidad * (-1);
sprites[j].velocidad = sprites[j].velocidad * (-1);
}

Este ejemplo lo podemos mejorar, haciendolo más real, aplicandole fisica real como fricción, centro de masa, pesos, etc, pero pues va para otra entrada.

Espero sea útil para alguien este ejemplo jeje, el código Aquí.

14 pensamientos en “Colisiones XNA usando BoundingBox – Rebotando pelotas

  1. hola

    no me puedo descargar el proyecto, dice que se ha eliminado o que no tengo permisos.

    y por otro lado en s_pelota.cargarImagen…

    cuando le digo que cargue “pelota” me dice que tengo que hacer un new porque el objeto no existe

    alguna idea??

    • Hola, que pena, tenía el proyecto en una carpeta sin permisos, ya modifique el link, y para el error que te sale, te debe faltar inicializar la variable s_pelota:
      s_pelota = new Sprite(graphics, new Vector2(graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2));
      Prueba y me comentas

  2. Pingback: El Blog en números del 2010 « Escarbando Código

  3. hola un saludo a todos.. primero felicitarte por la publicacion esta muy buena.. Alguien me podria ayudar tengo k realizar un ejemplo en java basicamente lo que se explico en este tema: que circulos reboten entre si… mi ejemplo solo rebota con las paredes de mi JFRAME alguien me pudiera ayudar. es que la verdad no se como lo puedo implementarlo en mi codigo. dejo mi algoritmo(si gustan publico todo mi codigo) muchisima grax

    public void repo(){

    Random r= new Random();
    switch(sent){

    case 0://arriba

    if(y-vel>radio){

    y-=vel;

    }
    else
    {
    sent=3+r.nextInt(3);
    }

    break;

    case 1:

    if(y-vel>radio && x+vel<maxx-radio)
    {
    y-=vel;
    x+=vel;

    }
    else{
    sent=4+r.nextInt(3);
    }
    break;

    case 2:
    if(x+vel<maxx-radio){
    x+=vel;
    }
    else{
    sent=5+r.nextInt(3);
    }
    break;

    case 3:

    if(x+vel<maxx-radio && y+vel<maxy-radio){

    x+=vel;
    y+=vel;
    }
    else{

    sent=6+r.nextInt(3);
    if(sent==8)
    sent=0;
    }
    break;

    case 4:

    if(y+velradio && y+velradio){
    x-=vel;
    }
    else{

    sent=1+r.nextInt(3);
    }
    break;

    case 7:

    if(x-vel>radio && y-vel> radio)
    {
    x-=vel;
    y-=vel;
    }
    else{
    sent=2+r.nextInt(3);
    }
    break;

    }

    }

  4. Pingback: Cómo hacer un Puzzle Bobble o Bust a Move en XNA – Parte 1 « Escarbando Código

  5. Como le harías para que las pelotas rebotaran un poco mas real, el hecho de que solo cambien de dirección al momento de la colisión lo vuelve un tanto irreal, alguna idea?

  6. hay otro problema que no he podido resolver y es el de al momento de colisionar algunas pelotas se quedan atoradas, esto es por que se sigue cumpliendo la condición de choque y vuelven a cambiar su dirección y así se quedan, estaba pensando en sumarle la distancia que le falta a cada sprite para salir de esa colisión alguna otra idea? gracias por la respuesta

Deseas comentar o sugerir algo?

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s