Colisión sobre Plataformas 2D – XNA


Muchos juegos como Super Mario,  Mega Man, Super Metroid entre otros, solían tener plataformas donde podíamos saltar y empezar a subir.

Hoy a mostrar una forma de cómo hacer un juego donde el héroe pueda saltar sobre varias plataformas, además de incluir gravedad para que cuando no se encuentre sobre una plataforma se caiga hasta que encuentre una plataforma.

Para hacer mejor las cosas, voy a continuar con el proyecto anterior del Administrador de escenas, adicionando nuevas clases para el manejo de Sprites y las colisiones.

Creamos la clase base de los Sprites, creamos un Game Component llamado SpriteComponent y luego hacemos que herede de la clase DrawableGameComponent, además declaramos la clase como abstract para indicar que es una clase base y que solo se puede heredar:

public abstract class SpriteComponent : DrawableGameComponent
    {
        // Propiedades
        ContentManager Content;
        SpriteBatch spriteBatch;
        public Vector2 Posicion { get; set; }
        public Texture2D Textura { get; set; }
        protected Vector2 velocidad;
        public Vector2 Velocidad { get { return velocidad; } set { velocidad = value; } }
        public Vector2 Centro { get; set; }
        public Vector2 Tamano { get; set; }
        public float Peso { get; set; }
        public BoundingBox Bound { get { return bound; } set { bound = value; } }
        public Color ColorImagen { get; set; }
        public String NombreImagen { get; set; }
        BoundingBox bound;

        public SpriteComponent(Game game, Vector2 tamano, Vector2 posicion)
            : base(game)
        {
            Content = (ContentManager)Game.Services.GetService(typeof(ContentManager));
            spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
            Tamano = tamano;
            Posicion = posicion;
            Peso = 1.0f;
            Vector3 min1 = new Vector3(Posicion.X, Posicion.Y, 0);
            Vector3 max1 = new Vector3((Posicion.X + Tamano.X), (Posicion.Y + Tamano.Y), 0);
            bound = new BoundingBox(min1, max1);
        }

        protected override void LoadContent()
        {
            Textura = Content.Load<Texture2D>(NombreImagen);
            base.LoadContent();
        }

        public override void Initialize()
        {
            base.Initialize();
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            spriteBatch.Draw(Textura, Posicion, new Rectangle(0, 0, (int)Tamano.X, (int)Tamano.Y), ColorImagen);
            base.Draw(gameTime);
        }

        public virtual void Show()
        {
            Enabled = true;
            Visible = true;
        }

        public virtual void Hide()
        {
            Enabled = false;
            Visible = false;
        }

        public void Mover(Vector2 velocidad)
        {
            Posicion += velocidad;
            bound.Max = new Vector3((Posicion.X + Tamano.X), (Posicion.Y + Tamano.Y), 0);
            bound.Min = new Vector3(Posicion.X, Posicion.Y, 0);
        }

        public abstract void Colision(SpriteComponent otro, Vector2 desplazamiento);
    }

Algunas propiedades no se usan, pero pueden ser útiles, el peso es usado para saber que tan rápido cae cuando se le aplica la gravedad.

La propiedad Bound es utilizada para crear un BoundingBox usado en las colisiones, las demás propiedades son intuitivas y no necesitan mucha explicación.

La clase solo tiene un constructor y recibe como parámetros el juego actual, un vector que representa el tamaño del Sprite, un vector que representa la posición inicial del Sprite.

Lo primero que hacemos es crear el BoundingBox del Sprite actual, en el método LoadContent cargamos la textura, usando la propiedad NombreImagen, por lo que hay que inicializarla debidamente.

En el método Draw, dibujamos la textura en la posición y con el tamaño que hayamos indicado, además se crea un método llamado Mover que recibe como parámetro un vector que representa la velocidad, luego sumamos la velocidad a la propiedad posición y reasignamos los valores al BoundingBox.

El método abstracto Colision es sobrecargado en las diferentes clases hijas, dependiendo de lo que se quiera hacer cuando hay colisión.

Ahora que tenemos nuestra clase base, vamos a crear una clase hija, creamos una clase que va a representar los muros y las plataformas:

public class Muro : SpriteComponent
    {
        public Muro(Game game, Vector2 tamano, Vector2 posicion )
            : base(game, tamano, posicion)
        {
            Peso = 0.0f;
            NombreImagen = "blank";
            ColorImagen = Color.Black;
            LoadContent();
        }

        public override void Colision(SpriteComponent otro, Vector2 desplazamiento)
        {
            // no se hace nada
        }
    }

Esta clase tiene los mismos parámetros que la clase base, pero el peso es 0 por lo cual la pared no se moverá hacia abajo, y se tiene un Color de Negro para el tinte de la textura, inmediatamente se crea la clase, se llama el método LoadContent para evitar una excepción porque la propiedad Textura se encuentra nula, esto puede ser un problema pero ustedes lo pueden corregir 😛

Ahora creamos otra clase que será la del Jugador:

public class Jugador : SpriteComponent
    {
        public Jugador(Game game, Vector2 tamano, Vector2 posicion, String nombreImagen)
            : base(game, tamano, posicion)
        {
            NombreImagen = nombreImagen;
            ColorImagen = Color.White;
            LoadContent();
        }

        public override void Colision(SpriteComponent otro, Vector2 desplazamiento)
        {
            if (otro is Muro)
            {
                Mover(desplazamiento);
                if (desplazamiento.Y != 0)
                {
                    velocidad.Y = 0;
                }
            }
        }
}

Esta clase tiene un parámetro adicional para el nombre de la Imagen, en esta clase se sobrecarga el método Colision, más adelante mostraré que hace este código.

Para manejar el flujo de cómo se comportan los Sprites, voy a crear una clase llamada Mundo, que será la encargada de tener una lista de todos los sprites y de actualizarlos adecuadamente, manejando las colisiones.

public class Mundo
    {
        Vector2 gravedad = new Vector2(0, 16);
        public List<SpriteComponent> Sprites { get; set; }

        public Mundo()
        {
            Sprites = new List<SpriteComponent>();
       }

        public void AdicionarSprite(SpriteComponent sprite)
        {
            Sprites.Add(sprite);
        }

        public void RemoverSprite(SpriteComponent sprite)
        {
            Sprites.Remove(sprite);
        }

        public Vector2 CalcularMinimaDistanciaTraslacion(BoundingBox bound1, BoundingBox bound2)
        {
            // Nuestro vector resultado de despalzamineento contiene la información del movimiento
            // que hay en la intersección

           Vector2 resultado = Vector2.Zero;
            // Esto es usado para calcular la diferencia entre las distancias de los lados.
            float diferencia = 0.0f;
            // Esto guarda la minimna distancia que nosotros necesitamos para separar los obtejos que van a colisionar

            float distanciaTraslacionMinima = 0.0f;
            // ejes guarda el valor de X o Y.  X = 0, Y = 1.

            // lado guarda el valor de izquierda (-1) o derecha (+1).
            // Estos son usados despues para calcular el vector resultado
            int ejes = 0, lado = 0;

            // Izquierda
            diferencia = bound1.Max.X - bound2.Min.X;
            if (diferencia < 0.0f)
            {
                return Vector2.Zero;
            }
            {
                distanciaTraslacionMinima = diferencia;
                ejes = 0;
                lado = -1;
            }

            // Derecha
            diferencia = bound2.Max.X - bound1.Min.X;
            if (diferencia < 0.0f)
            {
                return Vector2.Zero;
            }
            if (diferencia < distanciaTraslacionMinima)
            {
                distanciaTraslacionMinima = diferencia;
                ejes = 0;
                lado = 1;
            }

            // Abajo
            diferencia = bound1.Max.Y - bound2.Min.Y;
            if (diferencia < 0.0f)
            {
                return Vector2.Zero;
            }
            if (diferencia < distanciaTraslacionMinima)
            {
                distanciaTraslacionMinima = diferencia;
                ejes = 1;
                lado = -1;
            }

            // Arriba
            diferencia = bound2.Max.Y - bound1.Min.Y;
            if (diferencia < 0.0f)
            {
                return Vector2.Zero;
            }
            if (diferencia < distanciaTraslacionMinima)
            {
                distanciaTraslacionMinima = diferencia;
                ejes = 1;
                lado = 1;
            }

            // Sí hay Colisión:
            if (ejes == 1) // Eje Y
                resultado.Y = (float)lado * distanciaTraslacionMinima;
            else // Ejec X
                resultado.X = (float)lado * distanciaTraslacionMinima;
            return resultado;
        }

        public void Update(float deltaTime, float totalTime)
        {
            for (int i = 0; i < Sprites.Count; ++i)
            {
                Sprites[i].Velocidad += gravedad * Sprites[i].Peso;
                Sprites[i].Mover(Sprites[i].Velocidad * deltaTime);
                //verificar colisiones
                for (int j = 0; j < Sprites.Count; ++j)
                {
                    if (Sprites[i] == Sprites[j])
                        continue;
                    Vector2 depth = CalcularMinimaDistanciaTraslacion(Sprites[i].Bound, Sprites[j].Bound);
                    if (depth != Vector2.Zero)
                    {
                        Sprites[i].Colision(Sprites[j], depth);
                    }
                }
            }
        }
    }

La clase como no se va a dibujar no va a heredar de ninguna otra clase, no recibe parámetros en el constructor, y se tiene una variable llamada gravedad usada para aplicar algo de física real.

Como dije anteriormente, se va a tener una lista de los Sprites que vamos a usar en el juego, unos métodos de adicionar y eliminar sprites para administrar la lista de Sprites.

Colisión – Distancia entre Sprites

Hay una función llamada CalcularDistanciaMinimaDeTraslacion, que devolverá en un vector la distancia que hay entre dos Sprites, usando los BoundingBox de cada Sprite. Esta función la saque de un tutorial de Ziggyware, escrita por GroZZleR.

Se calcula la distancia entre lado del BoundingBox de un Sprite con el lado opuesto del otro Sprite, si la distancia o diferencia es menor que cero, se sabe que las cajas no se pueden tocar. Pero si es mayor o igual a cero, se continúan las mediciones, al final de las mediciones se guarda el valor más pequeño usado como la distancia mínima de traslación.

Para entender mejor, vamos a usar una imagen:

Los números son Min y Max de cada BoundingBox, ahora si reemplazamos dichos valores en el código nos va a dar:

Prueba 1 (Izquierda) : diferencia = 64 – 32 = 32

–          Resultado: diferencia > 0, una posible colisión, continuamos el test

Prueba 2 (Derecha): diferencia = 96 – 0 = 96

–          Resultado: diferencia > 0, una posible colisión, continuamos con el test

Prueba 3 (Abajo): diferencia = 64  – 32 = 32

–          Resultado: diferencia > 0, una posible colisión, continuamos con el test

Prueba 4(Arriba): diferencia = 96 – 0 = 96

–          Resultado: diferencia > 0, Sí hay colisión

Ahora se calcula la distancia mínima de traslación:

Resultado = (0,-1 * 32), el -1 es tomado de la variable lado, donde -1 es usado cuando el lado es izquierda o abajo, y 1 para los otros.

Después de esto, movemos el Sprite a la distancia minina, haciendo que ya no haya colisión.

En el método Update, recorremos cada Sprite de la lista, y le aplicamos la gravedad con el peso de cada Sprite, luego movemos el Sprite y verificamos que no hayan colisiones con otros Sprites, si hay colisión (si el método de CalcularDistanciaMinimaDeTraslacion retornó un valor diferente a Cero) se llama la función Colision del Sprite.

Ahora podemos volver a ver la clase Jugador y ver el método sobrecargado Colision, en este método cuando hay colisión, se verifica que la colisión sea con un muro (aunque es el único sprite diferente al Jugador, pero para futuros proyectos) y el Sprite del jugador se mueve al valor que retornó el método CalcularDistanciaMinimaDeTraslacion, pero cuando el valor de Y es negativo no vamos a mover el jugador, sino que dejamos que el movimiento hacia abajo lo haga es la gravedad, si comentamos esa parte del código, cuando el jugador este casi cayendo de una plataforma se caerá rápidamente.

Ahora que tenemos lo necesario, modificamos la clase ActionScene, que será la escena donde se va a mostrar todo el flujo del juego, lo he modificado para recibir una textura para el componente del Fondo:


class ActionScene : GameScene
    {
        ContentManager Content;
        SpriteBatch spriteBatch;
        Mundo mundo;
        Jugador jugador1;
        KeyboardState oldState;

        public ActionScene(Game game, Texture2D background)
            : base(game)
        {
            Componentes.Add(new BackgroundComponent(game, background));
            Content = (ContentManager)Game.Services.GetService(typeof(ContentManager));
            spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
            mundo = new Mundo();
            //Crear los muros
           mundo.AdicionarSprite(new Muro(game, new Vector2(game.Window.ClientBounds.Width, 16), new Vector2(0,0)));           
           mundo.AdicionarSprite(new Muro(game, new Vector2(16, game.Window.ClientBounds.Width), new Vector2(game.Window.ClientBounds.Width - 16, 0)));
            mundo.AdicionarSprite(new Muro(game, new Vector2(game.Window.ClientBounds.Width, 16), new Vector2(0, game.Window.ClientBounds.Height - 16)));
            mundo.AdicionarSprite(new Muro(game, new Vector2(16, game.Window.ClientBounds.Width), new Vector2(0, 0)));

            //crear algunas plataformas
            mundo.AdicionarSprite(new Muro(game, new Vector2(game.Window.ClientBounds.Width / 2, 24), new Vector2(0, 96)));
            mundo.AdicionarSprite(new Muro(game, new Vector2(game.Window.ClientBounds.Width / 2, 24), new Vector2(0, 500)));
            mundo.AdicionarSprite(new Muro(game, new Vector2(game.Window.ClientBounds.Width / 2, 24), new Vector2(game.Window.ClientBounds.Width - (game.Window.ClientBounds.Width / 2), 96 + 96 + 28)));
            mundo.AdicionarSprite(new Muro(game, new Vector2(256, 16), new Vector2(64, (int)(96 * 3.5f))));
            mundo.AdicionarSprite(new Muro(game, new Vector2(256, 8), new Vector2(game.Window.ClientBounds.Width / 2, (int)(96 * 4f))));

            jugador1 = new Jugador(game, new Vector2(32, 46), new Vector2(32, 32), "jetpack");
            mundo.AdicionarSprite(jugador1);
        }

        protected override void LoadContent()
        {
            base.LoadContent();
        }

        public override void Update(GameTime gameTime)
        {
            float deltaTime = (float)((double)gameTime.ElapsedGameTime.Milliseconds / 1000);
            float totalTime = (float)((double)gameTime.TotalRealTime.TotalMilliseconds / 1000);
            KeyboardState newState = Keyboard.GetState();
            Vector2 velocidad = jugador1.Velocidad;

            velocidad.X = 0;
            if (newState.IsKeyDown(Keys.W))
            {
                velocidad.Y = -200;
            }
            if (newState.IsKeyDown(Keys.A))
            {
                velocidad.X = -200;
            }
            if (newState.IsKeyDown(Keys.D))
            {
                velocidad.X = 200;
            }
            jugador1.Velocidad = velocidad;
            mundo.Update(deltaTime, totalTime);
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            foreach (SpriteComponent sp in mundo.Sprites)
            {
                sp.Draw(gameTime);
            }
        }

        public override void Show()
        {
            base.Show();
            Enabled = true;
            Visible = true;
        }

        public override void Hide()
        {
            base.Hide();
            Enabled = false;
            Visible = false;
        }
    }

Declaramos una variable para el Mundo y otra para el Jugador, en el constructor inicializamos la clase mundo y adicionamos a la lista de Sprites varios muros, los primeros muros son los límites de la pantalla, para evitar que el jugador se salga de la pantalla, los otros son algunas plataformas que dibujo en la pantalla, los muros los inicializo con el tamaño y la posición, la textura que tienen es un png de 512 x 512 de color blanco, cuando se hace el Draw del Sprite, con el tamaño recorto la textura origina y con el Color asignado en el constructor lo pinto.

Para la clase Jugador, envío el tamaño real de la imagen, para que se dibuje toda la imagen, he buscado una imagen del niño de los supersónicos, ya que el jugador se va a mover como si tuviera un JetPack, pueden modificar el código para adicionar los estados y hacer que solo pueda saltar una sola vez o dos como en Metroid.

En el método Update hacemos que el Jugador se mueva dependiendo de las teclas que oprima, y llamamos el Update de la clase Mundo para que mueva los diferentes Sprites adicionados a la clase.

En el método Draw, primero dibujamos lo que se tenga en la clase base, en este caso es el fondo y luego dibujamos los sprites, si no hacemos esto no se van a ver los sprites de los muros y del jugador.

Lo que sigue es que modifiquen el código, adicionando más sprites como bonus o enemigos, y jugando con las colisiones, además de adicionar estados y condiciones para que el jugador solo pueda saltar una vez, o continuar con el jetpack y hacer juegos de Jetpacks.

Un video donde se muestra todo:

Código Fuente Aquí

Anuncios

10 pensamientos en “Colisión sobre Plataformas 2D – XNA

  1. Pingback: Tweets that mention Colisión sobre Plataformas 2D – XNA « Escarbando Código -- Topsy.com

  2. Pingback: Paginas Recomendadas | XNA

  3. Pingback: Pagina Recomendada | XNA

  4. Pingback: Colisión de plataformas en terrenos uniformes – Con Interpolación lineal – XNA « Escarbando Código

    • Hola, estas con XNA 4? o XNA 3?, si tienes XNA 4 el método del Draw debe estar:
      spriteBatch.Begin(); o spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);, y otra cosa que hay que corregir es que en la clase ActionScene se reemplaza: float totalTime = (float)((double)gameTime.TotalRealTime.TotalMilliseconds / 1000); por float totalTime = (float)((double)gameTime.TotalGameTime.TotalMilliseconds / 1000);
      Con eso debe funcionar, recuerda que las teclas son AWSD

  5. Saludos martin, podrias realizar un simple ejemplo con una plataforma. me explico mejor supongamos que el personaje esta debajo de la plataforma y desea saltar hasta ella pero no colisiona con ella si no cuando esta por encima o que venga cayendo. gracias

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