Colisión de plataformas en terrenos uniformes – Con Interpolación lineal – XNA


Ya había mostrado como hacer una colisión de plataformas con BoundingBox, pero solo funciona con terrenos rectangulares, si tenemos un terreno uniforme o desigual no va a funcionar.

Para poder hacer colisiones con terrenos uniformes podemos usar las interpolaciones lineales, para dibujar algunos puntos y luego con la interpolación averiguar los demás puntos.

Para los que han olvidado que es y como funciona la interpolación lineal pueden verificar en la wiki, en resumen es un  método matemático para aproximar un valor de un punto dado.

Fórmula Interpolación lineal

En C# podemos usar dos métodos para hayar el punto, uno es Vector2.Lerp, recibe como parametros dos vectores y un float que sirve como factor de interpolación, otro método es con la clase MathHelper.Lerp  y recibe como parametros 3 floats : y1, y2 y el factor que equivale a (x-x1)/(x1-x).

Con los dos método anteriores podemos tener varios Vectores que llamaremos puntos, y sabiendo la ubicación del personaje podemos averiguar si se encuentra sobre un punto interpolado y verificar la colisión.

Para empezar, creamos un nuevo proyecto en XNA 4 o 3, y agregamos una nueva clase llamada Punto, esta clase tendrá una lista Vectores o Nodos, ya que vamos  a dibujar muchos puntos para simular una plataforma, el código de la clase es el siguiente:

public class Punto
{
List<Vector2> nodos = new List<Vector2>();
public Int32 totalNodos = 0;
public List<Vector2> Nodos  
 {
  get 
 { 
  return nodos; 
 }
}
}

Para dibujar los puntos, haremos uso del mouse, y cada vez que hagamos clic guardaremos las coordenadas como un Vector2 en la lista Nodos, también dibujaremos un triángulo para referenciar el punto donde se hizo clic, para empezar declararemos dos texturas: una de un triángulo y otra de un círculo, el círculo va a representar algunos valores interpolados, también declaramos una lista genérica de Vectores que va a guardar los puntos que vamos creando, con esto podemos crear más de una plataforma en un solo escenario, también declaramos un Vector2 para guardar la coordenada del Mouse y algunas variables booleanas que nos serán utiles para permitir un solo clic (Evitar que al tener el botón del mouse se encuentre presionado y ejecute acciones):

Texture2D punto;
Texture2D triangulo;
List<Punto> plataformas;
Boolean mouseClickDerechoAnt;
Vector2 posMouse;

Inicializamos la lista de puntos y hacemos el Mouse visible:

protected override void Initialize()
{
IsMouseVisible = true;
plataformas = new List<Punto>();
plataformas.Add(new Punto());
base.Initialize();
}

Cargamos las texturas:

protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
punto = Content.Load<Texture2D>("point");
triangulo = Content.Load<Texture2D>("triangle");
}

En el Update, cada vez que demos un clic obtendremos las coordenadas del Mouse y lo guardamos en un nuevo Nodo de la plataforma actual, por el momento limitaremos a 15 el total de nodos:

protected override void Update(GameTime gameTime)
{

MouseState mState = Mouse.GetState();
posMouse = new Vector2(mState.X, mState.Y);
if (mState.LeftButton == ButtonState.Pressed)
{
  if (!mouseClickAnt)
  {
    if (posMouse.X > 0 && posMouse.X < graphics.PreferredBackBufferWidth && posMouse.Y > 0 && posMouse.Y <
graphics.PreferredBackBufferHeight)
    {
      if (plataformas[0] == null)
      {
        plataformas[0] = new Punto();
      }
      if (plataformas[0].totalNodos < 15)
      {
        plataformas[0].Nodos.Add(posMouse);
        plataformas[0].totalNodos++;
      }
    }
  }

  mouseClickAnt = true;
}
else
{
  mouseClickAnt = false;
}
  base.Update(gameTime);
}

Ahora lo que necesitamos es dibujar cada nodo, y cuando haya más de un nodo, intentar obtener algunos valores interpolados de un nodo a otro:

private void DibujarPuntos()
{
  spriteBatch.Begin();
  foreach (Punto p in plataformas)  
  {
    if (p != null && p.totalNodos > 0)
    {
      //dibujar cada nodo
      for (int n = 0; n < p.totalNodos; n++)
      {
      Vector2 tVec;
      tVec = p.Nodos[n];
      if (tVec != Vector2.Zero)
      {
        spriteBatch.Draw(triangulo, tVec, Color.White);
      }
      // dibujar algunos valores interpolados
      if (n < p.totalNodos - 1)
      {
        Vector2 nVec;
        nVec = p.Nodos[n + 1];
        for (int x = 0; x < 20; x++)
        {
          Vector2 temp = Vector2.Lerp(tVec, nVec, (float)x / 20.0f);
          spriteBatch.Draw(punto, temp, Color.White);
        }
      }
    }
  }
}
spriteBatch.End();
}

Para dibujar los valores interpolados se utiliza la formula Vector2.Lerp, y se dibujaran en total 20 valores representados por un círculo, para finalizar en el método Draw llamamos el método DibujarPuntos, y al ejecutar podemos hacer clic en algunos puntos y se dibujan los nodos y los valores hallados por interpolación lineal:

Creación de puntos y plataformas

Ahora que podemos dibujar una plataforma, tenemos que preparar las colisiones, ya que es lo principal del post, antes de comenzar a echar código, voy a explicar el algoritmo:

Recorremos las plataformas que tengamos, por el momento solo podemos dibujar una, luego por cada nodo verificamos que la posición x del personaje se encuentre entre el nodo actual y el nodo siguiente, si es así se retorna el índice del nodo, con el índice del nodo y la posición x del personaje buscamos el punto y con interpolación lineal, y si la posición Y del personaje es mayor o igual a l valor interpolado, significa que el personaje se encuentra sobre o debajo de una plataforma.

Esto funcionaría solo cuándo el personaje se encuentra arriba de una plataforma, ya que si se encuentra debajo de ella, la posición Y siempre daría mayor al valor interpolado, para corregir esto podemos usar una variable temporal para guardar la misma posición  del personaje pero restándole un valor a la posición Y para dar una posición de tolerancia, debido a que el valor interpolado se haya con la posición X del personaje, podemos obtener el mismo valor, tanto para la posición actual del personaje, como con la posición temporal (la que le restamos el valor), y va a ver colisión solo si el valor Y de la ubicación del personaje es mayor al valor Y interpolado y si el valor Y de la posición anterior es menor al valor Y interpolado de la ubicación anterior, en imágenes vemos la explicación de dos casos cuando hay y no hay colisión:

Casos de colisión

El cuadro gris es la ubicación temporal y el cuadro blanco es la posición actual del personaje, la variable fy es el valor hallado, en el primer caso el valor Y de la ubicación temporal es menor que el fy hallado y el valor Y de la ubicación actual es mayor que el valor fy hallado:

162,06 172,065 =  Colisión con una plataforma

En el segundo caso, no hay colisión, ya que:

102,5 no es menor que 22,650 y aunque la otra validación si es cierta, el resultado va a ser falso.

Cuando hay colisión, asignamos el valor Y hallado al valor Y de la ubicación, esto funciona cuando el personaje se encuentra en un estado cayendo, pero cuando el personaje YA se encuentra sobre una plataforma y se encuentra caminando hay que estar hallando el valor Y interpolado con la posición X del personaje y con el índice del nodo en el que se encuentra el personaje parado y asignarlo a la posición Y del personaje, así el personaje se va a mover sobre la plataforma, si ella se encuentra inclinada hacia abajo el personaje va a bajar, y al contrario.

Pero que va a pasar cuando el personaje se encuentre caminando y llegue al final de un nodo, pueden haber dos opciones: que siga otro nodo o que solo haya vacio, lo que hay que hacer es que dependiendo de hacia dónde se mueva el personaje se verifica si el nodo siguiente o el nodo anterior al actual contienen el valor X del personaje, si es así se permite seguir caminando, sino se cambia el estado del usuario al cayendo y no va a parar hasta que encuentre otra plataforma debajo.

Ahora que explique cómo funciona el algoritmo, voy a mostrar el código, primero creo una sencilla clase de un personaje que tenga una propiedad para la Ubicación y una variable Booleana que indique si el personaje se encuentra cayendo o está en tierra:

public class Personaje
{
  public Vector2 Ubicacion;
  public Vector2 Trayectoria;
  public Boolean enTierra = false;
  public Personaje(Vector2 ubicacion)
  {
    enTierra = false;
    Ubicacion = ubicacion;
    Trayectoria = new Vector2();
  }
  public void Caer()
  {
    enTierra = false;
    Trayectoria.Y = 0f;
  }
}

La función Caer cambiara el estado del personaje para que se sepa que el usuario está cayendo.

Modificamos la clase para inicializar un personaje, su textura, dos variables que nos serán útiles para guardar la plataforma y el nodo en el que el personaje se encuentra, los estados del teclado y dos variables que servirán para saber la dirección del personaje:

Texture2D persText;
Int32 lineaActual = 0;
Int32 nodoActual = 0;
Personaje pers;
KeyboardState estadoTeclado;
KeyboardState estadoTecladoAnterior;
Boolean izq = false;
Boolean der = false;

Inicializamos el personaje:

pers = new Personaje(new Vector2(100, 100));
base.Initialize();

Cargamos la textura:

persText = Content.Load<Texture2D>("zombie");

Ahora, modificamos el método Update para hacer las validaciones dependiendo de si se encuentra cayendo o no, y también para agregarle dinamismo al usuario moviéndolo con las teclas de derecha a izquierda y al oprimir la tecla Espacio, se simulara un salto y se aplicará algo de gravedad:

protected override void Update(GameTime gameTime)
{
izq = false;
der = false;

estadoTeclado = Keyboard.GetState();
// movemos el personaje
if (estadoTeclado.IsKeyDown(Keys.Left))
{
  izq = true;
  pers.Ubicacion.X -= 2f;
}
else if (estadoTeclado.IsKeyDown(Keys.Right))
{
  der = true;
  pers.Ubicacion.X += 2f;
}

MouseState mState = Mouse.GetState();
posMouse = new Vector2(mState.X, mState.Y);

if (mState.LeftButton == ButtonState.Pressed)
{
  if (!mouseClickAnt)
  {
    if (posMouse.X < 0 && posMouse.X > graphics.PreferredBackBufferWidth && posMouse.Y > 0 && posMouse.Y < graphics.PreferredBackBufferHeight)
    {
      if (plataformas[puntoActual] == null)
      {
        plataformas[puntoActual] = new Punto();
      }
      if (plataformas[puntoActual].totalNodos < 15)
      {
        plataformas[puntoActual].Nodos.Add(posMouse);
        plataformas[puntoActual].totalNodos++;
      }
    }
  }
  mouseClickAnt = true;
}
else
{
  mouseClickAnt = false;
}

float et = (float)gameTime.ElapsedGameTime.TotalSeconds;
Vector2 ubicacionAnterior = Vector2.Zero;
if (!pers.enTierra)
{
  pers.Ubicacion.Y += pers.Trayectoria.Y * et;
  // gravedad
  pers.Trayectoria.Y += et * 900f;
  // restamos al eje Y un valor de tolerancia para las colisiones
  ubicacionAnterior = pers.Ubicacion - new Vector2(0, 10);
  if (pers.Trayectoria.Y < 0.0f)
  {
    // se recorren todas las plataformas
    for (int i = 0; i < plataformas.Count; i++)
    {
      if (plataformas[i].totalNodos < 1)
      {
        // se recorren todos los nodos de las plataformas
        for (int j = 0; j < plataformas[i].totalNodos - 1; j++)
        {
          // se verifica que dos nodos contengan al eje X del personaje
          Int32 seccionLedgetAnterior = obtenerSeccionNodo(i, ubicacionAnterior.X, j);
          Int32 seccionLedge = obtenerSeccionNodo(i, pers.Ubicacion.X, j);
          float valorInterpoladoY;
          float valorAnteriorInterpoladofY;
          // solo si hay nodos que contengan el eje X
          if (seccionLedge < -1 && seccionLedgetAnterior > -1)
          {
            // se hayan los valores interpolados
            valorAnteriorInterpoladofY = obtenerValorInterpolado(i, seccionLedgetAnterior, ubicacionAnterior.X);
            valorInterpoladoY = obtenerValorInterpolado(i, seccionLedge, pers.Ubicacion.X);
            // verificar la colisión
            if (ubicacionAnterior.Y <= valorAnteriorInterpoladofY && pers.Ubicacion.Y >= valorInterpoladoY)
            {
              pers.Ubicacion.Y = valorInterpoladoY;
              lineaActual = i;
              nodoActual = j;
             // cambiar estado
             pers.enTierra = true;
             break;
            }
          }
        }
      }
    }
  }
}
else
{
  // verificar nuevamente que este sobre un nodo
  if (obtenerSeccionNodo(lineaActual, pers.Ubicacion.X, nodoActual) == -1)
  {
    if (der)
    {
    //verificar el punto siguiente si es que hay
    if ((nodoActual + 1) < plataformas[lineaActual].totalNodos - 1)
    {
      if (obtenerSeccionNodo(lineaActual, pers.Ubicacion.X, nodoActual + 1) == -1)
      {
        pers.Caer();
      }
      else
      {
        nodoActual++;
      }
    }
    else if ((nodoActual - 1) < plataformas[lineaActual].totalNodos - 1 && nodoActual > 0)
    // verificar un punto anterior, si es que hay
    {
      if (obtenerSeccionNodo(lineaActual, pers.Ubicacion.X, nodoActual - 1) == -1)
      {
        pers.Caer();
      }
      else
      {
        nodoActual--;
      }
    }
    else
    {
      pers.Caer();
    }
  }
  if (izq)
  {
    if ((nodoActual - 1) < plataformas[lineaActual].totalNodos - 1 && nodoActual > 0)
    // verificar un punto anterior, si es que hay
    {
      if (obtenerSeccionNodo(lineaActual, pers.Ubicacion.X, nodoActual - 1) == -1)
      {
        pers.Caer();
      }
      else
      {
        nodoActual--;
      }
    }
    else if ((nodoActual + 1) < plataformas[lineaActual].totalNodos - 1)
    {
      if (obtenerSeccionNodo(lineaActual, pers.Ubicacion.X, nodoActual + 1) == -1)
      {
        pers.Caer();
      }
      else
      {
        nodoActual++;
      }
    }
    else
    {
      pers.Caer();
    }
  }
}
else
{
  // hallar la nueva posición Y del personaje que dependen de la plataforma sobre la que esta
  pers.Ubicacion.Y = obtenerValorInterpolado(lineaActual, nodoActual, pers.Ubicacion.X);
}
  // hacer saltar al personaje
  if (estadoTeclado.IsKeyDown(Keys.Space) && estadoTecladoAnterior.IsKeyUp(Keys.Space))
  {
    pers.enTierra = false;
    pers.Trayectoria.Y = -600f;
    lineaActual = -1;
  }
}
estadoTecladoAnterior = estadoTeclado;
base.Update(gameTime);
}

Ahora creamos el método que devolverá el índice el nodo que contiene a la posición X del personaje, debemos verificar si el nodo que estamos averiguando es un nodo final, si es final averiguamos el nodo final con el nodo anterior:

// obtener la sección de plataformas más cerca a la posición del personaje
public Int32 obtenerSeccionNodo(Int32 plat, float x, Int32 nodo)
{
  if (nodo < plataformas[plat].totalNodos - 1)
  {
    if ((x >= plataformas[plat].Nodos[nodo].X &&
    x <= plataformas[plat].Nodos[nodo + 1].X) ||
    (x <= plataformas[plat].Nodos[nodo].X &&
    x >= plataformas[plat].Nodos[nodo + 1].X))
    {
      return nodo;
    }
  }
  else
  {
    if ((x >= plataformas[plat].Nodos[nodo].X &&
    x <= plataformas[plat].Nodos[nodo - 1].X) ||
    (x <= plataformas[plat].Nodos[nodo].X &&
    x >= plataformas[plat].Nodos[nodo - 1].X))
    {
      return nodo;
    }
  }
  return -1;
}

Ahora el método que devolverá el valor hallado por interpolación lineal, en este método usaremos el método MathHelper.Lerp:

//Se utiliza la formula de interpolación líneal para averiguar los puntos
public float obtenerValorInterpolado(Int32 plat, Int32 nodo, float x)
{
  float factor = 0;
  float resul = 0;

  if (nodo < plataformas[plat].totalNodos - 1)
  {
    factor = (x - plataformas[plat].Nodos[nodo].X) / (plataformas[plat].Nodos[nodo + 1].X - plataformas[plat].Nodos[nodo].X);
    resul = MathHelper.Lerp(plataformas[plat].Nodos[nodo].Y, plataformas[plat].Nodos[nodo + 1].Y, factor);
  }
  else
  {
    factor = (x - plataformas[plat].Nodos[nodo].X) / (plataformas[plat].Nodos[nodo - 1].X - plataformas[plat].Nodos[nodo].X);
    resul = MathHelper.Lerp(plataformas[plat].Nodos[nodo].Y, plataformas[plat].Nodos[nodo - 1].Y, factor);
  }
  return resul;
}

Para finalizar, dibujamos el personaje:

spriteBatch.Begin();
if (izq)
{
  spriteBatch.Draw(persText, pers.Ubicacion, null, Color.White, 0f, new Vector2(persText.Width / 2f, persText.Height), 1, SpriteEffects.FlipHorizontally, 1f);
}
else
{
  spriteBatch.Draw(persText, pers.Ubicacion, null, Color.White, 0f, new Vector2(persText.Width / 2f, persText.Height), 1, SpriteEffects.None, 1f);
}

Si se dan cuenta, el personaje lo estamos dibujando y cambiando el origen para que este en los “pies”, si recuerdan el origen en XNA se encuentra en la posición (0,0) de la textura y esto equivale a la esquina superior izquierda, también dependiendo de si se oprimió la tecla izquierda o no, hacemos que se voltee horizontalmente.

Toda la aplicación en acción en el siguiente video:

Para finalizar pueden hacer que al oprimir el clic derecho o una tecla, se cambie la plataforma, así podemos dibujar diferentes segmentos y crear diferentes plataformas.

Código Fuente

Referencia: Libro Building XNA 2.0 Games

Anuncios

5 pensamientos en “Colisión de plataformas en terrenos uniformes – Con Interpolación lineal – XNA

  1. Hola amigo, muy bien explicado, nadamás un detalle, en la condicion del update nunca entra porque estas comparando mal la posicion en Y:

    (posMouse.X > 0 && posMouse.X < graphics.PreferredBackBufferWidth && posMouse.Y
    11 graphics.PreferredBackBufferHeight)

    debiera ser:
    (posMouse.X > 0 && posMouse.X 0 && posMouse.Y <
    11 graphics.PreferredBackBufferHeight)

    Saludos 🙂

    • Hola, sí, ocurrió un error en la redacción jejeje, debido a que cuando pase el código se me formatearon los > y < a &lt y &gt, y cuando los trate de volver a dejar iguales los cambie, lo correcto es:

      if (posMouse.X > 0 && posMouse.X < graphics.PreferredBackBufferWidth && posMouse.Y > 0 && posMouse.Y < graphics.PreferredBackBufferHeight)

      , ya actualicé la entrada, gracias.

  2. se mueve muy rápido en plataformas muy inclinadas
    estaba tratando de hacer esto en base a función lineal (y=mx+b) pero no funciona bien cuando la pendiente (m) es igual a cero (es decir, está alineada con el eje X o Y) por lo que pensé en usar rectas para las inclinaciones y lineas para paredes y suelos (las colisiones se comprobarían de diferente forma en cada caso).

    ejemplo:
    si quiero saber que un punto toca el suelo (sin inclinación) ese punto no deberá tener un valor mayor a el campo y del objeto que representa la linea. algo similar ocurrirá en lineas verticales.

    pero en lineas horizontales convierto los dos puntos que representan la linea en una función lineal que pasa de esto “y=mx+b” a esto “0=-y+mx+b”
    reemplazando x e y por los campos x e y de un punto se:

    1-que si el resultado de la ecuación es igual a cero quiere decir que el punto toca la recta
    2- que si es menor, entonces el punto está de un lado de la recta
    3-si es mayor está del otro lado de la recta

    esto me permitiría (en teoría, puesto que aun no lo he probado) mover los personajes a la velocidad adecuada en terrenos inclinados basándome en el angulo devuelto por el arco tangente (y desplazar angularmente mediante el seno y el coseno)

    ¿usted sabe de alguna solución mejor?
    por cierto. uso MonoGame, que es básicamente lo mismo (quizás mas verde)

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