Pages

sábado, 7 de abril de 2012

Level 02: Detección de Colisiones I

"La vida es una serie de colisiones con el futuro; no es una suma de lo que hemos sido, sino de lo que anhelamos ser."
José Ortega y Gasset (1883 - 1955) - Filósofo y ensayista español.
Antes que nada...
Aviso: Hice unos cambios mínimos en la clase "Figura" con respecto a los modificadores de acceso: ahora, la "posicion" es un campo privado. Se puede obtener o modificar su valor con los nuevos métodos agregados. De cualquier forma, creo que este cambio no afectará con los temas que veremos hoy, por lo tanto sólo hice esta pequeña aclaración. Esta modificación está aplicada en el código fuente que puede descargarse al final de la entrada.
¡Saludos!

Hasta ahora básicamente hemos conocido cómo es el manejo de texto y sobre cómo mover un objeto en la pantalla. Tendremos en cuenta todo lo visto y lo aplicaremos conjuntamente para poder explorar un tema fundamental en la programación de videojuegos: Detección de Colisiones.
Si bien cada lenguaje tiene su manera de hacerlo, las ideas sobre la detección de colisones son similares en todas. En esta ocasión conoceremos 2 de ellas, primero observando sus conceptos y luego experimentando con distintos objetos y formas.
Para detectar colisiones sería lógico pensar que necesitaremos a más de un objeto en nuestro proyecto. Por eso crearemos un segundo objeto que, aprovechando la oportunidad, aprenderemos a controlarlo con el mouse; y luego un tercer objeto que permanecerá inmóvil en el centro de la pantalla. Cada uno de estos objetos tendrá su forma particular que nos ayudará a entender mejor las diferentes maneras de detectar colisiones.

Así, los temas que se verán en esta entrada serán:
  1. Creación y Movimiento con Mouse de un nuevo objeto.
  2. Detección de Colisiones.
  3. Detección Simple por Rectángulo y Esfera (BoundingBox & BoundingSphere)
  4. Detección Múltiple por Rectángulo.

Manipularemos un objeto con el Mouse y detectaremos colisiones.

En la próxima entrada veremos otro método muy interesante que es mucho más útil y preciso, pero un poco más complejo que los anteriores. Manejando todos estos temas y aquellos que hemos visto hasta ahora, estaríamos listos para ir pensando en un juego muy, muy sencillo. Así que, ¡empecemos de una vez!




Creación y Movimiento con Mouse de un Objeto
Aviso: Las figuras que elijo son siempre para poder entender mejor cada tutorial. Recomiendo que por ahora utilicen los mismas figuras que yo. Para ello, sólo guarden la textura que siempre muestro y luego úsenla en el proyecto. La textura que muestro en pantalla está en su tamaño original.
Sólo una cosa a tener en cuenta antes de empezar...

En la entrada anterior creamos un objeto que pudimos controlar por medio del teclado. Al presionar una flecha, el cuadrado se desplazaba en esa dirección a una velocidad preestablecida.
Al crear un objeto cuyo movimiento sea controlado con el mouse, este siempre tomará la posición del puntero. Esto significa que la velocidad no depende (en este caso) de una variable con un valor preestablecido como en el caso del cuadrado, pues ya se lo damos "a pulso" cuando movemos el mouse. ¿Me explico?
Este será el nuevo objeto que moveremos con el mouse:

i) Esta será la figura que manipularemos con el Mouse.


Para incluirlo en nuestro proyecto, tenemos que seguir exactamente los mismos pasos que realizamos en la entrada anterior para crear al cuadrado:
  • Declarar una variable de referencia a la clase Figura.
  • En este caso no vamos a necesitar una variable para la velocidad, pero vamos a necesitar una instancia de la clase MouseState. Este, en lugar de guardar el último estado del teclado como lo hace KeyboardState, guarda el último estado del mouse. Esto nos servirá para poder darle vida al objeto.
  • Creamos el nuevo objeto dentro del método Initialize().
  • En el método LoadContent() establecemos su punto de origen definiendo una referencia del tipo Vector2 y cargamos la textura mostrada arriba para pasar como parámetro al método Inicializar() de la clase Figura.
  • En el método Draw() llamamos al método Dibujar() del nuevo objeto creado.
Siguiendo esos pasos, el código tendría que quedar de esta forma en cada método mencionado:
public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Figura jugador;                 // Instancia del objeto de clase Figura.
        float velocJugador = 5f;        // Valor que se le sumará o restará a la posición del objeto, por cada loop.
        KeyboardState estadoTeclado;    // Referencia que guardará el último estado del teclado, por cada loop.

        Figura jugador2;
        MouseState estadoMouse;         // Referencia que guardará el último estado del Mouse, por cada loop.
     
        SpriteFont font;                // Se guardará el tipo de fuente que se usará en los textos.



        protected override void Initialize()
        {
            jugador = new Figura();
            jugador2 = new Figura();

            base.Initialize();
        }



        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Se establece la posición inicial del jugador (x,y)
            Vector2 posicionJugador = new Vector2(
                                                GraphicsDevice.Viewport.TitleSafeArea.X,
                                                GraphicsDevice.Viewport.TitleSafeArea.Center.Y
                                                );
            Vector2 posicionJugador2 = new Vector2(
                                                GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                                GraphicsDevice.Viewport.TitleSafeArea.Center.Y
                                                );

            jugador.Inicializar(Content.Load<Texture2D>("Texturas/Azul"), posicionJugador, velocJugador);
            jugador2.Inicializar(Content.Load<Texture2D>("Texturas/Rojo"), posicionJugador2, velocJugador);
            font = Content.Load<SpriteFont>("SpriteFont1");
        }



        protected override void Draw(GameTime gameTime)
        {
            string anchoPantalla = Convert.ToString(GraphicsDevice.Viewport.Width);
            string altoPantalla = Convert.ToString(GraphicsDevice.Viewport.Height);          
            
            GraphicsDevice.Clear(Color.CornflowerBlue);
            

            // ============ Comienza dibujado ===================
            spriteBatch.Begin();

            // Se dibuja el texto donde informa el valor de Ancho de la pantalla
            spriteBatch.DrawString(font,
                                "Ancho: " + anchoPantalla,
                                new Vector2(GraphicsDevice.Viewport.TitleSafeArea.X,
                                            GraphicsDevice.Viewport.TitleSafeArea.Y),
                                Color.White,
                                0f,
                                new Vector2(0f,0f),
                                0.8f,
                                SpriteEffects.None,0);

            // Se dibuja el texto donde informa el valor de Alto de la pantalla
            spriteBatch.DrawString(font,
                                    "Alto: " + altoPantalla,
                                    new Vector2(GraphicsDevice.Viewport.TitleSafeArea.X+20,
                                                GraphicsDevice.Viewport.TitleSafeArea.Y+50),
                                    Color.DarkBlue,
                                    MathHelper.ToRadians(90),
                                    new Vector2(0,0),
                                    0.8f,
                                    SpriteEffects.None,
                                    0f);

            // Se dibuja la figura del jugador
            jugador.Dibujar(spriteBatch);
            jugador2.Dibujar(spriteBatch);
           
            spriteBatch.End();
            // ============ Termina dibujado ===================           
                 

            base.Draw(gameTime);
        }

Si seguimos bien estos pasos, en estos momentos ya tendríamos creado nuestro nuevo amigo y podríamos visualizarlo en la pantalla al ejecutar el programa:

ii) Visualizando la nueva figura en pantalla.

Ya tenemos a nuestro nuevo objeto creado. Ahora tenemos que crear un método que permita manipular su movimiento con el mouse. Recordemos el método que creamos en la entrada anterior, llamado MoverObjeto(). Gracias a él pudimos mover a la figura cuadrada con el teclado y, gracias también a él, podremos mover la figura circular. 
Para ello, primero debemos obtener el estado actual del mouse y guardarlo en la variable estadoMouse que definimos al principio. Esto lo hacemos dentro del método Update() de esta forma:
 protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            estadoTeclado = Keyboard.GetState();
            estadoMouse = Mouse.GetState();
            MoverFigura(gameTime);

            base.Update(gameTime);
        }

Luego, una vez obtenido el estado del mouse, agregaremos lo siguiente al método MoverObjeto() para controlar el círculo rojo:
private void MoverFigura(GameTime gameTime)
        {
            // Analizamos qué tecla fue presionada y luego modificamos su posición,
            // teniendo en cuenta la velocidad que indicamos.

            // ======== Mover Jugador 1 ====================================
            if (estadoTeclado.IsKeyDown(Keys.Left))
            { jugador.IncreasePosX(-jugador.GetVelocidad()); }

            if (estadoTeclado.IsKeyDown(Keys.Right))
            { jugador.IncreasePosX(jugador.GetVelocidad()); }

            if (estadoTeclado.IsKeyDown(Keys.Up))
            { jugador.IncreasePosY(-jugador.GetVelocidad()); }

            if (estadoTeclado.IsKeyDown(Keys.Down))
            { jugador.IncreasePosY(jugador.GetVelocidad()); ; }

            jugador.SetPosicionX(MathHelper.Clamp(jugador.GetPosicionX(), 0,
                                                GraphicsDevice.Viewport.Width - jugador.GetAncho() / 2));
            jugador.SetPosicionY(MathHelper.Clamp(jugador.GetPosicionY(), 0,
                                                    GraphicsDevice.Viewport.Height - jugador.GetAlto() / 2));


            // ======== Mover Jugador 2 ====================================
            jugador2.SetPosicionX(estadoMouse.X);
            jugador2.SetPosicionY(estadoMouse.Y);

            jugador2.SetPosicionX(MathHelper.Clamp(jugador2.GetPosicionX(), 0,
                                                GraphicsDevice.Viewport.Width - jugador2.GetAncho() / 2));
            jugador2.SetPosicionY(MathHelper.Clamp(jugador2.GetPosicionY(), 0,
                                                    GraphicsDevice.Viewport.Height - jugador2.GetAlto() / 2));
        }

Pueden observar que lo único que hacemos cuando usamos el mouse, es reemplazar las coordenadas de origen (esquina superior izquierda (x,y)) del objeto por las coordenadas (x,y) del puntero. En simples palabras, el objeto estará ubicado siempre en la misma posición del puntero. Es por esto que no hace falta en este caso un valor de velocidad como en el caso del cuadrado. Con esto me refería a darle la velocidad "a pulso".

Muy bien, a estas alturas, si ejecutamos el código, deberíamos poder mover el círculo rojo con el mouse. Y así ya tenemos 2 objetos móviles en la pantalla.

Antes de continuar con los otros temas, agreguemos algo más al código para visualizar un par de datos muy útiles. Estaría bueno poder ver en qué posición (x,y) se encuentra exactamente el origen de los objetos cada vez que se mueven, asi que... ¿por qué no hacerlo? Sólo tenemos que agregar un pequeño fragmento de código en el método Draw() que dibuje un texto en la pantalla y que informe la posición actual de nuestros objetos. Entonces, agregamos lo siguiente dentro de dicho método y entre los llamados a spriteBatch.Begin() y spriteBatch.End():

            // Se dibuja un texto donde indica las coordenadas (x,y) del jugador1
            spriteBatch.DrawString(font,
                        "Jugador 1: (" + jugador.GetPosicionX() + ", " + jugador.GetPosicionY() + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto donde indica las coordenadas (x,y) del jugador2
            spriteBatch.DrawString(font,
                        "Jugador 2: (" + jugador2.GetPosicionX() + ", " + jugador2.GetPosicionY() + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y+25),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);



 Y con esto, ya podríamos ver a cada momento cuáles son exactamente los valores de x e y del punto de origen (esquina superior izquierda) de los objetos que creamos. Algo simple, pero muy útil. 

iii) Un texto añadido que muestra la posición (x,y) de cada figura.
   .




Detección de Colisiones
Aviso: Estoy seguro que los métodos que realizo en este tutorial aún pueden ser mejorados en cuando su implementación y legibilidad. Quiero aclarar que escribo los métodos con la principal intención de que se comprenda cada concepto y se pueda ejemplificar con muestras sencillas. Recomiendo diseñar clases que incorpore dichos métodos y otros que puedan resultar útiles para que, de esta manera, no se extienda demasiado el código principal (Game1) y resulte lo más compacto posible. De todas maneras, cualquier modificación al código actual será notificada en futuros avisos como estos.
Este es un proceso fundamental que es aplicado por casi todos los videojuegos, tanto en 2D como en 3D. Básicamente consiste en detectar una intersección, un contacto entre 2 o más objetos que luego generará (o no) una nueva situación como respuesta.
Que al tocar un hongo, mario incremente su tamaño; que cuando la bala disparada por un terrorista logre hacer contacto con el cuerpo de un policía, le reste a este un porcentaje de su vida; que cuando el vehículo que manejas se destroce al chocar con una pared o con otro automóvil... son sólo algunos de los tantos etcéteras que existen para ejemplificar la causa y efecto de la detección de colisiones.

En XNA podemos analizar una detección de colisión aplicando alguno de estos métodos:
  • Detección Simple, por BoundingBox/BoundingSphere.
  • Detección Múltiple.por BoundingBox/BoundingSphere.
  • Detección a Nivel Pixel.
A continuación veremos algunas características de los 2 primeros métodos y cómo aplicarlos en el código. Pero para ejemplificar y entender mejor el contenido de estos temas, agregaremos un nuevo objeto. Esta nueva figura estará ubicada más o menos al centro de la pantalla y será inmóvil, o sea, no controlaremos su movimiento. Seguimos, entonces, los mismos pasos que realizamos cuando creamos el círculo o el cuadrado, para que finalmente podamos visualizar a la siguiente figura en la pantalla:
iv) Una nueva figura que permanecerá inmóvil en la pantalla.




Detección Simple por BoundingBox y BoundingSphere
Aviso: Existe otra forma de detectar colisiones en lugar de usar la clase BoundingBox. En su lugar, puede crearse una referencia de la clase Rectangle para cada objeto y luego usar el método Intersects() o Contain() para la detección. El problema es que dichos métodos sólamente aceptan como parámetros a otras referencias Rectangle. Es decir, no podríamos comparar una referencia de la clase Rectangle con otra de la clase BoundingBox o BoundingSphere. Estas últimas pueden compararse entre sí para detectar una colisión, por lo que preferí usar estas 2 clases en lugar de la clase Rectangle.
XNA ofrece varias clases para facilitar la detección de colisiones. Esta vez, usaremos las clases BoundingBox y BoundingSphere (que llamaremos a partir de ahora como BnB y BnS, respectivamente sólo por comodidad).
  • BoundingBox: Representa el espacio ocupado por un cuadro. Cada una de sus caras son perpendiculares al eje X, eje Y y/o al eje Z. No puede ser rotado.
  • BoundingSphere: Representa el espacio ocupado por una esfera.
Ambas clases están diseñadas para trabajar con objetos 3D; pero, como no lo aplicamos aún, sólo bastará con darle el valor cero al eje Z y así poder utilizar dichas clases para objetos en 2D.

v) Imagen que describe la forma de un BoundingSphere y un BoundingBox en 3 y 2 dimensiones.

¿Para qué nos sirven estás clases?
Tanto el BnB como el BnS no son más que simples "capas". Su diferencia más notable es su forma. El BnB es una "capa" rectangular, mientras que el BnS es una circular.
Cada una tiene 2 métodos importantes, llamados Intersects() y Contains(). A través de ellos podemos detectar una colisión. La diferencia entre ambos es que el primero "detecta" a un objeto que, al menos, roce sus límites; en cambio, el segundo "detecta" a un objeto únicamente si este se encuentra adentro de sus límites.

vi) Ejemplos de colisiones con método Intersects() y Contains()

Habiendo comprendido lo anterior, procedamos a la escritura del código.

Primero declaramos una variable booleana llamada huboColision al principio de nuestra clase Game1 y la inicializamos en false. La idea es que esta variable nos indiqué cuándo se produce una colisión para poder notificarlo en la pantalla.
Lo que sigue es lo más importante, que es crear un método para identificar las colisiones que se produzcan entre las 3 figuras del proyecto. Pero, ¿en qué parte del código deberíamos realizar esa detección? Lo correcto sería verificarlo justo después del momento en que movemos un objeto. En el método Update() podemos ver que primero obtenemos el estado del teclado y del mouse para luego recurrir al método MoverFigura(). Recordemos que lo que hace este método es modificar la posición de un Sprite, dependiendo de qué tecla se presione o dependiendo de la posición del cursor del mouse. Por lo tanto, deberíamos tener nuestro método DetectarColision() inmediatamente luego de realizar esa acción. Luego hacemos que este método retorne un valor booleano que modifique a la variable huboColision dependiendo del resultado en el análisis de la detección de colisiones. Agregamos lo siguiente a nuestro código:
 protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            estadoTeclado = Keyboard.GetState();
            estadoMouse = Mouse.GetState();
            MoverFigura(gameTime);
            huboColision = DetectarColision(gameTime);

            base.Update(gameTime);
        }



       public bool DetectarColision(GameTime gameTime)
        {
            BoundingBox cuadroLimitacion;       // Un cuadro delimitador que se usará para detectar colisiones.
            BoundingSphere esferaLimitacion;   // Un círculo delimitador que se usará para detectar colisiones.
            BoundingBox neutralLimitacion;      // Un cuadro delimitador que se usará para detectar colisiones.

            Vector3 centroEsfera;

            cuadroLimitacion = new BoundingBox( new Vector3(jugador.GetPosicionX(),
                                                            jugador.GetPosicionY(),
                                                            0f)
                                               ,new Vector3(jugador.GetPosicionX() + jugador.GetAncho(),
                                                            jugador.GetPosicionY() + jugador.GetAlto(),
                                                            0f)
                                               );

            centroEsfera = new Vector3( jugador2.GetPosicionX() + jugador2.GetAncho()/2,
                                        jugador2.GetPosicionY() + jugador2.GetAlto()/ 2,
                                        0f);
            esferaLimitacion = new BoundingSphere(centroEsfera, jugador2.GetAncho() / 2);

            neutralLimitacion = new BoundingBox(new Vector3(figNeutral.GetPosicionX(),
                                                                figNeutral.GetPosicionY(),
                                                                0),
                                                 new Vector3(figNeutral.GetPosicionX() + figNeutral.GetAncho(),
                                                                figNeutral.GetPosicionY() + figNeutral.GetAlto(),
                                                                0)
                                                );

            if (cuadroLimitacion.Intersects(esferaLimitacion)
                || cuadroLimitacion.Intersects(neutralLimitacion)
                || neutralLimitacion.Intersects(esferaLimitacion))
            {
                return true;
            }
            else
                return false;
        }

Descifremos un poco el método DetectarColision(). Lo que hacemos al principio es crear las referencias de los BoundingBox o BoundingSphere (las capas) que usaremos para verificar si hubo algún contacto entre objeto y objeto. Siempre se trata de que, sea el tipo de capa que sea, cubra al Sprite utilizado. Por eso usamos un BnB para el cuadrado verde, un BnS para el círculo rojo y un BnB para la estrella anaranjada. Tanto el constructor del BnB como del BnS són sobrecargados. En este caso, podemos expresar los parámetros de nuestro BnB de esta manera:

BoundingBox nombreCuadroLimitación = new BoundingBox( puntoMínimo, puntoMáximo)

En donde:
  • puntoMínimo [Vector3]: hace referencia al punto que será el origen del BoundingBox. Un Vector3 tiene 3 coordenadas (x,y,z) porque es tridimensional. Pero como trabajamos en 2 dimensiones, sólo hace falta hacer un z=0 y listo.
  • puntoMáximo [Vector3]: es el punto que determinará el ancho y el alto del BoundingBox.

Cuando creamos una referencia de BoundingBox, tenemos que tener en cuenta que la posición de aquel BnB tiene que estar ligada a la posición de su correspondiente Sprite. En el caso del cuadrado verde el puntoMínimo del BnB siempre tendrá la misma coordenada (x,y) del origen de dicho cuadrado; mientras que la coordenada (x,y) del puntoMáximo siempre estará dada por el ancho (x) y alto (y) de la textura usada en ese mismo cuadrado.

Luego tenemos al constructor del BnS, que podemos expresar sus parámetros de esta manera:

BoundingSphere nombreCírculoLimitación = new BoundingSphere ( puntoCentro, radio)


En donde:
  • puntoCentro [Vector3]: hace referencia al punto que será el centro de nuestro BoundingSphere.
  • radio [float]: es el radio que tendrá nuestro círculo, que equivale a la mitad del ancho del círculo.

Pueden ver que antes de construir el BoundingSphere, creé una referencia del Vector3 llamado centroEsfera (que vendría a ser el puntoCentro). Lo hice solamente por una cuestión de legibilidad, para que al crear el BoundingShpere no haya confusiones. Es una buena práctica realizar las cosas de esta manera, no como lo hice al crear el cuadroLimitacion.

Al final del método DetectarColision(), en las líneas del  46 al 54, verificamos si hubo alguna intersección entre los BnB y/o BnS que hayamos creado. El método Intersects() es el que se encarga de realizar ese análisis y nos retorna un valor booleano dependiendo del resultado. Retornamos ese valor para cambiar el estado de la variable huboColision.  

Una vez que tenemos el resultado guardado en la variable huboColision, sólo nos queda informarlo en la pantalla. Para ello, vamos a utilizar un texto en el método Draw(). Entonces lo único que queda por hacer es preguntar si hubo colisión o no y dibujar el texto en pantalla. El método Draw() quedaría modificado de esta forma:

 protected override void Draw(GameTime gameTime)
        {
            string anchoPantalla = Convert.ToString(GraphicsDevice.Viewport.Width);
            string altoPantalla = Convert.ToString(GraphicsDevice.Viewport.Height);
            string textoColision;

            if (huboColision)
                textoColision = "Si";
            else
                textoColision = "No";
            
            GraphicsDevice.Clear(Color.CornflowerBlue);
            

            // ============ Comienza dibujado ===================
            spriteBatch.Begin();

            // Se dibuja el texto donde informa el valor de Ancho de la pantalla
            spriteBatch.DrawString(font,
                                "Ancho: " + anchoPantalla,
                                new Vector2(GraphicsDevice.Viewport.TitleSafeArea.X,
                                            GraphicsDevice.Viewport.TitleSafeArea.Y),
                                Color.White,
                                0f,
                                new Vector2(0f,0f),
                                0.8f,
                                SpriteEffects.None,0);

            // Se dibuja el texto donde informa el valor de Alto de la pantalla
            spriteBatch.DrawString(font,
                                    "Alto: " + altoPantalla,
                                    new Vector2(GraphicsDevice.Viewport.TitleSafeArea.X+20,
                                                GraphicsDevice.Viewport.TitleSafeArea.Y+50),
                                    Color.DarkBlue,
                                    MathHelper.ToRadians(90),
                                    new Vector2(0,0),
                                    0.8f,
                                    SpriteEffects.None,
                                    0f);


            // Se dibuja un texto donde indica las coordenadas (x,y) del jugador1
            spriteBatch.DrawString(font,
                        "Jugador 1: (" + jugador.GetPosicionX() + ", " + jugador.GetPosicionY() + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto donde indica las coordenadas (x,y) del jugador2
            spriteBatch.DrawString(font,
                        "Jugador 2: (" + jugador2.GetPosicionX() + ", " + jugador2.GetPosicionY() + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y+25),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto que indica si hubo alguna colisión o no.
            spriteBatch.DrawString(font,
                        "Colision: " + textoColision,
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.X,
                                    GraphicsDevice.Viewport.TitleSafeArea.Bottom - 25),
                        Color.BlueViolet,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja la figura del jugador
            jugador.Dibujar(spriteBatch);
            jugador2.Dibujar(spriteBatch);
            figNeutral.Dibujar(spriteBatch);
           
            spriteBatch.End();
            // ============ Termina dibujado ===================           
                 

            base.Draw(gameTime);
        }

Si ejecutamos el código, podremos ver que habrá un texto informándonos si se ha producido una colisión o no. Un screenshot de la situación:
vii) Muestra de la superposición de Sprites y de la Detección de Colisiones.

Quiero que noten también cómo se superponen los Sprites según el orden en que dibujamos las figuras. Si miramos el código anterior en la línea 79, veremos que se dibuja primero el cuadrado verde; luego se dibuja el círculo rojo y finalmente la estrella anaranjada. Por lo tanto, como dibujamos al cuadrado primero, aparecerá "al fondo" de la pantalla; mientras que la estrella dibujada por última vez, aparecerá "al frente" de la pantalla. Tengan muy en cuenta eso a la hora de dibujar.

Y Así es cómo las clases BnB y BnS nos ayudan a detectar colisiones. Bien, pero ¿qué sucede con aquellos objetos que no son perfectamente rectangulares o circulares? Podemos comprobar que el tercer objeto que creamos no es igual a ninguna de las 2 formas anteriores, por lo que consecuentemente la detección que resulta no es para nada precisa. Es por eso que veremos el siguiente tema que puede mejorar un poco ese cálculo.




Detección Múltiple por BoundingBox
Acabamos de aprender qué son y para qué sirven las clases BoundingBox y BoundingSphere.

Sabemos que los objetos que usemos en un juegos no van a ser siempre rectágulos y/o círculos, como la figura anaranjada. Entonces ¿qué hacer para que la detección de colisiones sea más precisa?.

El método de Detección Múltiple puede ser una solución, pero sigue algo siendo imprecisa. Este método consiste en emplear varios BnB o BnS( o ambos) para cubrir un objeto de forma irregular, en lugar de usar 1 sólo para todo el objeto. Por ejemplo, si quisieramos mejorar la detección de colisiones del proyecto actual, deberíamos crear varias referencias de la clase BoundingBox y vincularlos con la figura anaranjada, más o menos de esta manera:
viii) Ejemplos de Detección Múltiple por BoundingBox.
Con esto logramos mejorar el cálculo de la detección de colisiones, pero aún sigue siendo un método impreciso. Aún se puede mejorar la detección. Una opción sería crear tantos BnB o BnS como sean necesarios, lo que podría resultar algo tedioso cuando se usan Sprites que tienen una forma con más detalles que la anterior. Hacer docenas (espero estar exagerando...) de BnB o BnS con estas figuras, no creo que sea una buena idea.
Un dato muy importante para agregar es que las "capas" de BoundingBox no se pueden rotar, lo que complica aún más la situación. Los BnB están alineados a los ejes X e Y y no se puede modificar su grado de inclinación. Hay que tener mucho cuidado cuando se pretende rotar Sprites.

Pero hay una buena noticia y se las daré en la próxima entrada. Por ahora, creo que con esto es suficiente.




Conclusión
Aviso: Pueden informarse sobre la detección a Nivel Pixel en una página altamente recomendable, llamada Riemer's XNA Tutorials. Explica sobre cómo mejorar la detección de colisiones combinando los métodos anteriores. El problema es que está en inglés, pero trataré de guiarme de esos tutoriales para armar la siguiente entrada. Este es el link.
Hemos visto cómo controlar un objeto con un Mouse. Luego vimos por qué es tan importante la detección de colisiones y las maneras básicas de llevarlo a cabo, con la ayuda de la clase BoundingBox y BoundingSphere que nos ofrece la librería de XNA. Pero también experimentamos la desventaja más grande de los métodos básicos de Detección Simple y Detección Múltiple, que es su imprecisión. Pueden descargar el código empleado en esta entrada, haciendo click en el siguiente botón:



Existe otra manera de mejorar la detección de colisiones y que esta sea muy precisa, pero tiene su costo: el método de Detección a Nivel Pixel, también conocida como Pixel Perfect. Conoceremos cómo funciona este método en la próxima entrada. Lo único que dire por ahora es que es muy preciso, pero requiere de uso de memoria mucho mayor que al trabajar con BoundingBox o BoundingSphere. Además, su código es algo más complejo que lo que hicimos hasta ahora.

Lo bueno de todo esto es que si logramos dominar estos 3 métodos y los combinamos luego, tendremos muy pulida nuestra manera de detectar colisiones. El truco está en saber cómo y cuándo usar esos 3 métodos.

Por último, quiero decir que con todo lo que hemos visto hasta ahora, estoy seguro que ya estamos en condiciones para hacer un juego... por lo menos uno muy simple. Aunque por mi parte, yo estaré pensando en hacer uno luego de ver el tema de la siguiente Entrada que, por cierto, me está costando mucho tiempo entenderlo y llevarlo a la práctica. Estoy viendo que voy a tardar un poco en armar la próxima Entrada del blog, pero sólo es cuestión de tiempo, la voluntad está.

¡Hasta entonces!

¡Sigue adelante, siempre!

No hay comentarios:

Publicar un comentario