Level 03: Detección de Colisiones II

Tercer tutorial sobre Programación con XNA Framework 4.0. Esta vez escribí sobre algunos problemas que pueden presentarse en la detección de colisiones 2D y también sobre cuáles serían las posibles soluciones, como la detección a nivel pixel (Pixel Perfect).

Frogger Kombat

Este juego fue realizado en conjunto con algunos amigos durante una Game Jam, a finales del 2011. Se trata de una pelea de sapos, que son manejados por 2 jugadores simultáneamente. El objetivo del juego es saltar y aplastar al contrincante hasta quitarle todos sus puntos de vida.

Votación CODEAR 2012: DEMO

CODEAR es un concurso en donde compiten desarrolladores de videojuegos. Consiste en producir un juego según una temática. En este caso, la temática fue "Demo". La votación termina el 11 de Septiembre. Puedes ver y jugar a todos los juegos que compiten, haciendo click en la imagen.

Nuevo mini COREAR: Cinemática

Estoy participando en un mini COREAR, que es un concurso en el cual compiten músicos y sonidistas. Cada competencia tiene una temática, a partir de la cual se crean pistas de música para poder participar. Esta vez la temática es "Cinemática". La idea es tomar cualquier cinemática de cualquier juego y componer una pista de música que pueda reemplazar a la original. Si quieres escuchar lo que hice, haz click en la imagen. Para más información sobre este COREAR, haz click en el título.

Sumate a la comunidad de DUVAL!

DUVAL (Desarrolladores Unidos de Videojuegos de América Latina) es una comunidad cuyo objetivo es mantener un lugar donde pueda existir una comunicación libre e independiente entre los desarrolladores de videojuegos de la region, para compartir trabajos, conocimientos, armar grupos, etc.

sábado, 8 de septiembre de 2012

Proyecto 01: Top Down Shooter 2D

"Cada vez que cometo un error me parece descubrir una verdad que no conocía."
Maurice Maeterlinck (1862 - 1849) - Escritor belga.
Antes que nada...
Aviso: El post aún no está terminado, por eso hay cosas que pueden no quedar muy claras. 

¡Saludos!

Ha pasado un largo tiempo desde la última entrada. Por distintas cosas de la vida, no pude dedicar mucho tiempo a este humilde blog. Pero espero poder avanzar, al menos lentamente. Así que mejor tener paciencia por ahora.

Esta vez quiero consolidar las ideas básicas que se vieron anteriormente. ¡Y no hay mejor manera que poner en práctica lo aprendido haciendo un juego! No sé si será algo sencillo (eso espero...) o complicado; no sé cuánto tiempo me llevará terminarlo... pero es hora de hacerlo, por fin.

Desde la última vez, estuve pensando qué hacer. Al final decidí empezar haciendo un Top Down Shooter (en 2D, obviamente). Desde entonces he estado intentando hacerlo, pero muy de vez en cuando.

La idea de esta Entrada, es hacer una especie de "historial" del desarrollo de este juego. A medida que vaya avanzando con el desarrollo, estaré actualizando esta Entrada hasta el día en que, medianamente, lo termine. En cada actualización escribiré sobre los distintos errores, correcciones, problemas, soluciones... y toda modificación en general que vaya surgiendo en el transcurso del tiempo. También estuve pensando un poco las cosas y decidí no colocar el código del programa durante ese tiempo, ya que preveo que será tedioso modificar a cada rato este posteo y podría generar confusiones.

Así que espero que salga todo bien al final. ¡Manos a la obra!


Screenshot de la última actualización: 08/09/2012



Sobre el Juego.
Siempre me gustaron los shooters, de todo tipo. Recuerdo haber jugado un Top Down Shooter para PC llamado Phobia II (1998). No estoy seguro, pero juraría que fue el primer juego que conocí de ese tipo. Y cómo esto me dió mucha nostalgia, decidí hacer algo de ese estilo pero modificando algunas cosas con toque propio.

Básicamente, el juego se va a tratar de un tanque que debe sobrevivir a una oleada de enemigos que irán apareciendo en el mapa, tal como en Phobia II. El jugador manejará los movimientos del tanque con el teclado (flechas o teclas WASD) y podrá apuntar y disparar con el Ratón.

Para comenzar, creí que lo primero que debía hacer era diseñar la mecánica del jugador. Luego me concentraría en diseñar los enemigos y power ups, para que finalmente pueda volcar todo aquello en el diseño de niveles. 

Como recién estoy empezando, seguro que hay cosas que no puedo prever en estos momentos y que en el futuro irán saliendo a la luz. Por eso quiero hacer un seguimiento de todos mis avances en esta Entrada. Entonces, ahí van:

Este es un pequeño video que resume todo lo que hice hasta el día 09/06/2012, con las primeras 3 actualizaciones:



HISTORIAL


Día 1: 06/06/2012 - Comenzando el Proyecto
Voy a empezar con la mecánica del jugador. El tanque que maneja el jugador se formará de 2 partes: una Base y un Cañón. La Base es la que controla solamente el movimiento del tanque, mientras que el Cañón tiene la función de apuntar y disparar. La base se controla con el teclado y el cañón con el ratón.

Antes que nada, hice un modelo provisorio y simple de lo que sería la textura de la Base del Tanque:


Textura provisoria de la Base del Tanque.

Luego creé una clase llamada "Base.cs", que contiene la mayoría de las características de la clase "Figura.cs" vista en la Entrada anterior. De hecho, tomé esa clase y le hice algunas pequeñas modificaciones y la renombré como "Objeto.cs". La idea es que las demás clases, como la Base y el Cañón del tanque, hereden sus atributos (ya que ambas tienen propiedades similares como una posición (x,y), una textura, etc.). Por lo tanto la clase "Base.cs" es una derivada de la clase "Objeto.cs".

Una vez acordado todo esto, procedí con el código del programa hasta que pude visualizar la Base del Tanque en la pantalla y lograr moverla con el teclado:


Primer logro: Mover la Base del Tanque con el teclado.



08/06/2012: Diseñando el Cañón
Una vez lista la Base del Tanque, lo siguiente era empezar con el Cañón. Diseñé entonces la clase "Cañon.cs", también derivada de la clase "Objeto.cs". También, hice otro modelo simple de la textura que tendrá el Cañón:


Textura provisoria del Cañón del Tanque


Luego continué con el código. La particularidad del cañón del jugador, es que siempre apunta a la posición en la pantalla donde se encuentra el puntero del ratón. Luego de varias pruebas, terminé esa parte del código y logré terminar con el movimiento del cañón. Tuve en cuenta el origen de la textura, ya que esta debía girar en torno al centro del círculo. Lo que resultó de todo esto fue lo siguiente:


Segundo logro: El Cañón apunta hacia la posición del cursor del Mouse.



09/06/2012: Armando el Tanque y añadiendo Disparos
Al principio, lo único que hice esta vez fue 'armar' el tanque uniendo los 2 objetos anteriores, de modo que cada objeto siempre se mueva al mismo tiempo y a la misma posición del otro. Un screenshot de lo logrado:


Tercer logro: Tanque formado con la 'unión' de una Base y un Cañón.

Luego diseñé una clase "Bala.cs", derivada también de la clase "Objeto.cs" para que representen los disparos que realizará el Cañón del Tanque. Hice 2 modelos de texturas para dicha clase:



Diseño Bala 01
Diseño Bala 02

Cada vez que se presione el botón del ratón, el Cañón debería lanzar una Bala hacia la dirección donde se encontraba el puntero del ratón. Con esta idea, completé el código del programa hasta que salió bien. Me llevó algo de tiempo aplicarlo, ya que me confundían un poco los cálculos de trigonometría, pero salío bien después de todo.

Tuve en cuenta también que las balas no deberían "viajar" para siempre. Es decir, en cada disparo, las balas deben tener en su trayectoria una distancia máxima. También estoy pensando en agregar al Cañón del Tanque la capacidad de "Recargar" municiones al quedarse sin balas. Esto debería darle un pequeño tiempo de retraso al cañón antes de volver a disparar.

Finalmente agregué unos textos a la pantalla para poder visualizar algunos datos importantes. Y esa fue la última actualización del mes. Un screenshot:


Cuarto Logro: Tanque ensamblado y disparando.



02/09/2012: Agregando un Enemigo
Pasó un largo tiempo desde la última vez. En esta ocasión quise agregar un enemigo al juego. Por ahora sólamente hice que un segundo tanque apunte al jugador y dispare. Si las balas enemigas impactan en el jugador, este debería perder vida. Para ello, agregué una propiedad a la clase "Base.cs" para guardar los puntos de vida del jugador

Un screenshot de esta actualización:


Quinto logro: Un cañón enemigo que dispara a la posición del jugador.


Conclusión
Esto es todo lo que hice hasta el momento. Todavía queda mucho por delante. Así que pronto iré actualizando esta Entrada con las nuevas actualizaciones.

¡Hasta entonces!


¡Sigue adelante, siempre!

jueves, 10 de mayo de 2012

Level 03: Detección de Colisiones II

"El hombre todo lo perfecciona en torno suyo; lo que no hace es perfeccionarse a sí mismo."
Jean Baptiste Alphonse Karr (1808 - 1890) - Escritor francés.
Antes que nada...

¡Saludos!

Uffff... esta vez sí que costó armar esta entrada. ¡Pero se pudo! Costó entender y aplicar todo lo investigado. Pero bueno, este fue un buen logro.

En la entrada anterior se vió principalmente sobre cómo detectar colisiones simples usando las clases BoundingBox y BoundingSphere. El gran problema que se presenta es que la detección de colisiones es imprecisa cuando se trabaja con Texturas (Sprites) de formas irregulares (o para ser más precisos, de forma no circular o rectangular puras). Peor aún cuando las mismas texturas se rotan o escalan ya que tanto los BoundingBox y BoundingSphere están alineados al Eje X y al Eje Y y por lo tanto no pueden ser rotados.

Es por eso que estuve investigando sobre posibles soluciones en muchos (...y la verdad que sí, MUCHOS) sitios. De todas las soluciones que encontré, escogí aquellas metodologías que, me en mi opinión, eran mejores. Compartiré los resultados en esta entrada.
  
Los temas que veremos esta vez, serán:
  1. Los problemas de la Detección de Colisiones con BoundingBox (y BoundingSphere).
  2. Detección de Colisiones a Nivel Pixel ("Pixel Perfect").
  3. Detección de Colisiones combinadas: Pixel Perfect + Rectangle.  
 
Optimizaremos la detección de colisiones frente a distintas transformaciones.

Recomiento repasar un poco los temas vistos anteriormente, porque aplicaremos varios de ellos en esta ocasión. Sin nada más que decir, ¡empecemos entonces!


Problemas de Detección con BoundingBox

En la entrada anterior, se detectaba colisiones con BoundingBox o BoundingSphere. Vimos que la detección funcionaba correctamente con objetos que tenían la forma exacta a la de un rectángulo o círculo perfecto. Pero también vimos que la detección se volvía inprecisa con objetos que no tenían exactamente esas formas, aunque luego vimos que el uso múltiple de BoundingBox o BoundingSpheres era una posible solución a ese problema.

Pero, ¿qué sucede cuando se modifica la rotación o escala de esos objetos, mientras se detecta colisiones simultáneamente?

Antes de contestar esa pregunta, tenemos que hacer unas pocas modificaciones a nuestra clase Figura: Si lo pensamos bien, sabremos que lo que tienen en común todas las figuras que creamos es una posición, una rotación y una escala (recuérdese que por defecto nuestras figuras tenían un ángulo de 0º (0 rad) y una escala de 0.5f). Y como vimos en la entrada Level 01, la rotación guarda relación con el origen o "punto de ancla" (así lo llamo yo) pues, cuando se rota una figura, se toma ese punto como referencia. En fin, estas 4 características tendrán todas las figuras que crearemos, por lo tanto serán los nuevos atributos de nuestra clase Figura. Agreguemos eso a continuación y corrijamos el programa principal Game1.

En la clase Figura, primero agregamos los nuevos atributos: puntoAncla, angulo y escala. Luego agregamos sus respectivos set & get, modificamos nuestro constructor y al método Draw(), que aplica esos atributos. Las partes modificadas de la clase Figura, deberían quedar más o menos así:

class Figura
    {
        private Texture2D textura;  // Textura que se usará para representar al jugador.
        private Vector2 posicion;   // Contendrá las coordenadas de su posición.
                Vector2 puntoAncla; // Contendrá las coordenadas de su punto de anclaje(u Origen) como referencia para rotación y escala.
        private float velocidadMov, // Representa al valor de la velocidad de su desplazamiento.
                      angulo,       // Guardará el valor del ángulo de inclinación del objeto.
                      escala;       // Guardará el valor de la escala de la Textura (o Sprite).

        public void Inicializar(Texture2D textura, Vector2 posicion, float velocidadMov)
        {
            this.textura = textura;
            this.posicion = posicion;
            this.velocidadMov = velocidadMov;
            this.puntoAncla = Vector2.Zero;
            this.velocidadMov = 5f;
            this.angulo = 0f;
            this.escala = 0.5f;
        }

public void Dibujar(SpriteBatch spriteBatch)
        {
            // Dibujo al objeto.
            spriteBatch.Draw(
                            textura,
                            posicion,
                            null, 
                            Color.White,
                            angulo,         //MathHelper.ToRadians(45)
                            puntoAncla,
                            escala,
                            SpriteEffects.None,
                            0f
                            );          
       }
//============= PUNTO DE ANCLA ========
        public Vector2 GetPuntoAncla()
        {
            return puntoAncla;
        }

        public void SetPuntoAncla(Vector2 puntoAncla)
        {
            this.puntoAncla = puntoAncla;
        }

        // ============ ESCALA ================
        public float GetEscala()
        {
            return escala;
        }

        public void SetEscala(float escala)
        {
            this.escala = escala;
        }

        public void IncreaseEscala(float valor)
        {
            if (this.escala + valor > 0.02f && this.escala + valor < 4.5f)
            this.escala += valor;
        }


        // ============ ANGULO ================
        public float GetAngulo()
        {
            return angulo;
        }

        public void SetAngulo(float angulo)
        {
            this.angulo = angulo;
        }

        public void InreaseAngulo(float valor)
        {
            float val = MathHelper.ToDegrees(valor);
            if (MathHelper.ToDegrees(this.angulo + valor) > 360)
            {
                this.angulo = valor;
            }
            else if (MathHelper.ToDegrees(this.angulo + valor) < 0)
            {
                this.angulo = MathHelper.ToRadians(360) - valor;
            }
            else
                this.angulo += valor;
        }
}

De esta manera, podremos modificar la rotación y escala de nuestras figuras, desde el programa principal Game1. Por ejemplo, podríamos crear un método que pueda rotar las figuras y otro que pueda aumentar o reducir su escala de tamaño, ambas cosas mediante teclas. Apliquemos eso solamente en la figura cuadrada y hagamos que la estrella anaranjada pueda rotar por su cuenta. Entonces, agregamos los siguientes métodos y modificamos el Update():

namespace Prueba01
{
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);
            RotarFigura();                          // Se gira una Figura dependiendo de los 2 estados anteriores.
            figNeutral.InreaseAngulo(0.005f);   // Rotación de la Figura Neutral.
            EscalarFigura();                        // Se redimensiona el tamaño de una Figura.
            huboColision = DetectarColision(gameTime);

            base.Update(gameTime);
        }

            public void RotarFigura()
        {
            if (estadoTeclado.IsKeyDown(Keys.A))
            {
                jugador.InreaseAngulo(0.02f);
            }

            if (estadoTeclado.IsKeyDown(Keys.D))
            {
                jugador.InreaseAngulo(-0.02f);
            }
        }

        public void EscalarFigura()
        {
            if (estadoTeclado.IsKeyDown(Keys.W))
            {
                jugador.IncreaseEscala(0.01f);
            }

            if (estadoTeclado.IsKeyDown(Keys.S))
            {
                jugador.IncreaseEscala(-0.01f);
            }
        }
}


Como se puede ver, los métodos RotarFigura() y EscalarFigura(), permiten modificar el ángulo de rotación de y el valor de escala del tamaño total de la figura cuadrada, respectivamente. Esto lo logramos mediante las teclas 'A' y 'D' para la rotación y 'W' y 'S' para la escala. Veamos qué sucede si ejecutamos el programa ahora:

Error que ocurre en la Detección de Colisiones Simples con figuras transformadas.

Volviendo a la pregunta que decía "¿qué sucede cuando se modifica la rotación o escala de esos objetos, mientras se detecta colisiones simultáneamente?" ... bueno, acaba de ser contestada. Podemos notar que la detección de colisiones funciona demasiado mal. Lo que está sucediendo en este caso es que, aunque no se vea, los BoundingBox de la figura cuadrada y el de la estrella nunca se tocan, pues no respetan la escala ni mucho menos la rotación de la textura transformada. Este es el enorme problema al aplicar el método Detección Simple de Colisiones por BoundingBox (o BoundingSphere) en figuras transformadas.

Tratar de solucionar esto, cuesta mucho tiempo, muchos cálculos y muchos dolores de cabeza. Pero por suerte, no es imposible. Veremos a continuación un método que solucionará este gran problema.
Pero antes, debo avisar que lamentablemente descartaremos al método que creamos como DetectarColision(). No usaremos BoundingBox ni BoundingSphere, porque no encontré la forma de hacerlos rotar y porque hay otra clase muy similar que nos servirá mucho. Todo esto lo veremos más adelante, en esta Entrada.


Detección de Colisiones a Nivel Pixel o "Pixel Perfect"

Aviso: Todos los métodos que se presentan a continuación, fueron realizados tomando como referencia otros sitios.
Este método de detección es el más preciso de todos, pero así también conlleva a un gran problema de rendimiento cuando se analiza una gran cantidad de objetos y, peor aún, de gran tamaño (en cuanto a pixeles).  Noté también cuando estaba practicando que, si se comete algún error de programación, el consumo de CPU es tanto que el juego se hace demasiado lento... y eso que contaba solamente con 3 objetos. Así que hay que tener sumo cuidado al aplicar este método. Pero por ahora, veamos de qué se trata.

Anteriormente había aconsejado que vieran el sitio web del experto en XNA, Riemer Grootjans, del cual tomé como referencia para aprender este método. El artículo que leí se llama 2D Colission Detection y, sí, está inglés. Pero a continuación voy a explicar de qué se trata más o menos la Detección Pixel Perfect, citando partes de dicho artículo. Veamos entonces:

La Detección de Colisiones a Nivel Pixel consiste básicamente en tomar la textura de un objeto y verificar por cada pixel si algúno de ellos colisiona con algún pixel de la textura de otro objeto. Veamos el ejemplo que propone Riemer:



  
Cada textura contiene pixeles transparentes (representados en la imagen con color verde oscuro) y pixeles no transparentes (representados en color rosado claro, en el cañon, y en rojo oscuro, en el cohete). Se dice que se produce una colisión cuando pixeles no transparentes de 2 texturas distintas se superponen. Riemer ejemplifica esta situación con la siguiente imagen:


Repito, noten que se produce la colisión únicamente con pixeles no transparentes de 2 texturas de objetos distintos. En otras palabras, la colisión no se produce cuando, entre 2 texturas, al menos 1 de los pixeles no es transparente.

También hay que tener en cuenta que los objetos pueden variar su rotación y/o escala. Este método de detección puede adaptarse a esos cambios mediante el uso de matrices. Gracias a las matrices, podemos adecuar la detección según la posición, la rotación y la escala del objeto que deseemos analizar. Por lo tanto, se podrían detectar colisiones en casos como estos:


Riemer dice:
"Cuando una matriz guarda la posición, rotación y escala de una imagen, puedes usar esta matriz para encontrar la coordenada final de la pantalla de cualquier pixel de esa imagen. Si tomamos la inversa de esta matriz, dada cualquier coordenada de pantalla podemos encontrar la coordenada del pixel en la imagen original.
Como primer paso, se multiplica (="transforma") la coordenada del pixel por la matriz de la imagen A. Esto nos dará la coordenada de dónde será renderizado el pixel en la pantalla.
El siguiente paso es lo opuesto: dada la coordenada de la pantalla, esta se multiplica por la inversa de la matriz de la imagen B. Esto nos dará la coordenada en la imagen B original.
 Luego vemos que el pixel en la imagen original A y el pixel en la imagen original B no son transparentes, por lo que ambos colisionan en la pantalla."
¡Es un capo Riemer! =D
A veces me cuesta mucho entender las cosas y aún más cuando están en otro idioma. Así que tuve que leer varias y varias... y muchas varias veces para entenderlo completamente y aplicarlo en el proyecto correctamente. ¡Pero se pudo! Compartiendo lo que pude aprender, a continuación llevemos esto a la práctica:

Para analizar la detección de colisiones de 2 objetos distintos, necesitamos primero 2 matrices de cada uno:
  • Matriz con los datos de cada pixel de la imagen.
  • Matriz de la imagen (con los datos de la posición, rotación y escala).
Primero, necesitamos obtener los datos de cada uno de los pixeles que tiene la textura (Sprite) del objeto que queremos analizar. Para ello agregamos el siguiente método propuesto por Riemer en nuestra clase Figura:

public Color[,] GetMatrizPixeles()
        {
            Color[] colors1D = new Color[(textura.Width) * (textura.Height)];
            textura.GetData(colors1D);
            Color[,] colors2D = new Color[(textura.Width), (textura.Height)];

            for (int x = 0; x < textura.Width; x++)
                for (int y = 0; y < textura.Height; y++)
                    colors2D[x, y] = colors1D[x + y * textura.Width];

            return colors2D; 
        }

De esta manera obtenemos los datos de los pixeles de una imagen guardados en una matriz. Luego debemos obtener la matriz de la imagen, teniendo en cuenta su posición en la pantalla, su rotación y el valor de su escala del tamaño original. Por suerte, al comienzo de esta Entrada, hemos incluído esos datos como nuevos atributos de nuestra clase. Riemer supone cierta situación y dice:
  • 1: "Si queremos renderizar la imagen tal cual es (o: con la transformación de Identidad), se renderizaría con el tamaño original en la esquina superior izquierda de la pantalla."
  • 2: "Primero, la imagen del cohete es movida por lo que su punto correspondiente a su esquina superior izquierda está en la posición especificada como segundo argumento en el método SpriteBatch.Draw()" (en nuestro caso, ubicado dentro del método Dibujar() en la clase Figura).
  • 3: "Luego, todo (por ejemplo) es reducido. Puedes ver que esto incluye tanto la imagen como sus propios ejes X e Y".
  • 4: "Luego, todo (por ejemplo) es rotado. De nuevo, esto incluye a la imagen como a sus propios ejes X e Y".
  • 5: "Finalmente, la imagen resultante es movida (rotada o escalada) según su origen.X (puntoAncla.X en nuestro caso) sobre su eje X, y según su origen.Y (puntoAncla.Y en nuestro caso) sobre su eje Y."

Lo importante de esto es saber que el orden en que se ejecutan las cosas es muy importante, ya que si se intercambian los pasos podría haber errores como el siguiente, donde se intercambia el cuarto paso por el quinto paso:


Lo que sucedió es que las coordenadas del sistema fueron movidos primero, luego esas coordenadas fueron rotadas y terminaron en una posición totalmente diferente. Esto explica por qué el cohete se sale parcialmente de la pantalla.
Teniendo en cuenta esto, agreguemos entonces el siguiente método a nuestra clase Figura, para obtener la matriz de la textura:

public Matrix GetMatrizTextura()
        {
            Matrix matFigura = Matrix.CreateTranslation(-puntoAncla.X, -puntoAncla.Y, 0) *
                                Matrix.CreateRotationZ(angulo) *
                                Matrix.CreateScale(escala) *
                                Matrix.CreateTranslation(posicion.X, posicion.Y, 0);
            return matFigura;
        }


Ya tenemos listos los métodos que retornan los elementos necesarios para la detección Pixel Perfect. Ahora queda el paso final que consiste en analizar y detectar colisión entre 2 objetos. Esto lo haremos desde la clase principal Game1.
Primero, definamos al principio de la clase 3 booleanos (uno para cada objeto) que nos pueda indicar si hubo, o no, colisión (con true o false, respectivamente). Luego añadimos el siguiente método de detección, realizado por Riemer, que pueda modificar el estado de esos booleanos:

 public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Figura jugador;                 // Instancia del objeto de clase Figura.
        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.

        Figura figNeutral;              // Referencia del tercer objeto para prueba de colisiones. Es inmóvil.

        SpriteFont font;                // Se guardará el tipo de fuente que se usará en los textos.
        
        // Tomarán el valor de 'true' en caso de que exista alguna colisión a nivel pixel.
        bool huboColisionPix;           
        bool huboColisionNJ1Pix;        
        bool huboColisionNJ2Pix;

public bool ColisionPixelPerfectBool(Color[,] pixelesFig1, Matrix matFig1, Color[,] pixelesFig2, Matrix matFig2)
        {
            bool huboColision = false;
            Matrix mat1a2 = matFig1 * Matrix.Invert(matFig2);
            int ancho1 = pixelesFig1.GetLength(0);
            int alto1 = pixelesFig1.GetLength(1);
            int ancho2 = pixelesFig2.GetLength(0);
            int alto2 = pixelesFig2.GetLength(1);

            for (int x1 = 0; x1 < ancho1; x1++)
            {
                for (int y1 = 0; y1 < alto1; y1++)
                {
                    Vector2 pos1 = new Vector2(x1, y1);
                    Vector2 pos2 = Vector2.Transform(pos1, mat1a2);
                    int x2 = (int)pos2.X;
                    int y2 = (int)pos2.Y;

                    if ((x2 >= 0) && (x2 < ancho2))
                    {
                        if ((y2 >= 0) && (y2 < alto2))
                        {
                            if (pixelesFig1[x1, y1].A > 0)
                            {
                                if (pixelesFig2[x2, y2].A > 0)
                                {
                                    huboColision = true;
                                    return huboColision;
                                }
                            }
                        }
                    }
                }
            }
            return huboColision;
        }

   }


Finalmente, agregaremos un método que permita invocar al ColisionPixelPerfect() según la cantidad de figuras que usamos en el juego. En este caso invocaremos a dicho método 3 veces para analizar las colisiones entre los 3 objetos que hemos creado. Luego aplicamos todo esto dentro del método Update() y finalizamos modificando el método Draw(), en donde deberíamos cambiar el texto que indica si hubo colisión, según el resultado del análisis. Tendría que quedar entonces algo así:
        
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();    // Se verifica el estado del Teclado (inactivo / tecla pulsada)
            estadoMouse = Mouse.GetState();         // Se verifica el estado del Mouse (inactivo / tecla pulsada)
            MoverFigura();                          // Se mueve una Figura dependiendo de los 2 estados anteriores.
            RotarFigura();                          // Se gira una Figura dependiendo de los 2 estados anteriores.
            figNeutral.InreaseAngulo(0.005f);   // Rotación de la Figura Neutral.
            EscalarFigura();                        // Se redimensiona el tamaño de una Figura.
            ControlarColisiones();                  // Checkea si hay algún tipo de colisión entre los objetos creados.
            
            base.Update(gameTime);
        }

public void ControlarColisiones()
        {        
                huboColisionPix = ColisionPixelPerfectBool(jugador.GetMatrizPixeles(), jugador.GetMatrizTextura(),
                                                            jugador2.GetMatrizPixeles(), jugador2.GetMatrizTextura()
                                                            );
         
                huboColisionNJ1Pix = ColisionPixelPerfectBool(jugador.GetMatrizPixeles(), jugador.GetMatrizTextura(),
                                                            figNeutral.GetMatrizPixeles(), figNeutral.GetMatrizTextura()
                                                            );
           
                huboColisionNJ2Pix = ColisionPixelPerfectBool(jugador2.GetMatrizPixeles(), jugador2.GetMatrizTextura(),
                                                            figNeutral.GetMatrizPixeles(), figNeutral.GetMatrizTextura()
                                                            );          
        }


        protected override void Draw(GameTime gameTime)
        {
            string anchoPantalla = Convert.ToString(GraphicsDevice.Viewport.Width);
            string altoPantalla = Convert.ToString(GraphicsDevice.Viewport.Height);
            string textoColision;
            GraphicsDevice.Clear(Color.CornflowerBlue);
          
            if (huboColisionPix || huboColisionNJ1Pix || huboColisionNJ2Pix)
                {
                    GraphicsDevice.Clear(Color.DarkSlateBlue);
                    textoColision = "Si";
                }       
   
            else
            {
                GraphicsDevice.Clear(Color.CornflowerBlue);
                textoColision = "No";
            }
            
            // ============ Comienza dibujado ===================
            spriteBatch.Begin();
            
           // ... 
           // ... y aquí va el resto del código original.
           // ... 
        }


Y con esto terminamos con el método de Detección de Colisiones a Nivel Pixel. Ejecutemos el código y veamos qué sucede:

Demostración de la Detección de Colisiones a Nivel Pixel en funcionamiento.

Como podrán notar, el método funciona perfectamente y es muy preciso. Este método es la solución para aquellos objetos con texturas de formas complejas que pudieramos llegar a utilizar alguna vez.
Otra cosa importante es que, no sé si ustedes también, pero noté que inmediatamente luego de arrancar el programa, este funcionó lentamente por unos segundos y luego comenzó a correr normalmente (aunque en ciertas ocasiones se notaba una pequeña lentitud). Seguramente tiene que ver con el costo del que mencioné más arriba, sobre el consumo de CPU que requiere este método. Esa es la gran desventaja de la detección Pixel Perfect. El rendimiento empeora dependiendo de la cantidad de objetos a analizar y sus respectivos tamaños: mientras más cantidades de objetos y mientras más grandes sean, peor será el rendimiento.

Y bueno, no todo lo que brilla es oro. Pero, según lo que leí en algunos sitios, existen muchas maneras de "alivianar" el consumo de CPU, sin tener que dejar de emplear este excelente método. Uno de ellos es combinando la Detección Simple (que vimos en la entrada anterior) con Pixel Perfect. Elegí esta combinación porque me parecía bastante sencilla y además es de mucha ayuda. Lo veremos a continuación.



Detección de Colisiones combinada: Rectangle + Pixel Perfect

Aviso: Busqué la forma de aplicar Pixel Perfect, combinados con BoundingBox o BoundingSphere, pero no encontré nada. Si alguna vez llego a encontrar la forma de aplicarlo, estaré compartiendo eso en alguna futura Entrada. En su lugar, usaremos la clase Rectangle.
Esta combinación de métodos permite mejorar el rendimiento, conservando la precisión de la detección de colisión Pixel Perfect. Sólamente consiste en utilizar una clase muy parecida al BoundingBox, llamada Rectangle. Como se puede deducir de su nombre, un objeto de la clase Rectangle no es más que un rectángulo. La diferencia es que un BoundingBox está pensado para utilizarse en 3D (por sus coordenadas x,y,z), mientras que Rectangle nos resulta útil en 2D. Además, podemos transformarlo según la posición, rotación y escala de la textura de un objeto (al igual que las matrices). Es por esta ventaja que usaremos Rectangle para detectar primero una Colisión Simple, de bajo consumo de CPU, para que luego proceda a la detección Pixel Perfect sólo en caso de que ocurra la situación anterior. De esta manera, los cálculos no serán tan 'pesados'.

Pero antes, agreguemos un par de métodos que servirán para observar el comportamiento del origen de la textura de nuestro objeto. Sabemos que por defecto, el origen, al que llamaremos en mi caso como "puntoAncla", es una instancia de la clase Vector2 con las coordenadas X e Y ubicadas en la esquina superior izquierda (0,0) de la textura de una figura. La rotación tomará como referencia ese punto, tal como vimos en la entrada Level 01. Esta situación se dá con la estrella anaranjada, que contínuamente está rotando según su punto de Ancla (origen).
Agreguemos ahora un par de métodos en la clase Figura que permita cambiar las coordenadas del puntoAncla (a parte del set correspondiente) y ubicarla en el centro de la textura o en la ubicación por defecto (0,0) esquina superior izquierda.
Agregamos entonces los siguientes métodos en la clase Figura:
        
public void CentrarOrigen()
        {
            puntoAncla = new Vector2((textura.Width/2) , (textura.Height/2));
        }

 public void ReestablecerOrigen()
        {
            puntoAncla = Vector2.Zero;
        }


Y llamamos a estos métodos en la clase principal Game1, por ejemplo dentro del método EscalarFigura(). No son relevantes estos métodos, son sólamente para analizar situaciones particulares. Hagamos que afecte únicamente a la figura cuadrada:
        
public void EscalarFigura()
        {
            if (estadoTeclado.IsKeyDown(Keys.W))
            {
                jugador.IncreaseEscala(0.01f);
            }

            if (estadoTeclado.IsKeyDown(Keys.S))
            {
                jugador.IncreaseEscala(-0.01f);
            }

            if (estadoTeclado.IsKeyDown(Keys.F))
            {
                jugador.CentrarOrigen();
            }

            if (estadoTeclado.IsKeyDown(Keys.G))
            {
                jugador.ReestablecerOrigen();
            }
        }

Así, si presionamos la tecla 'G' el puntoAncla tendrá el valor (0,0). En cambio, si presionamos la tecla 'F', el puntoAncla tendrá sus coordenadas en el centro de la textura del objeto (en este caso, el centro de la figura cuadrada de 200x200 pixeles, tiene las coordenadas [100,100]). Pueden comprobarlo ejecutando el código, el método Pixel Perfect también estará funcionando.

Ahora sí, regresemos con el tema sobre el uso de Rectangle para la detección de colisiones.

Necesitamos crear un Rectangle que se adapte según la posición, la rotación y la escala de nuestra figura. Para ello, agregamos el siguiente método en la clase Figura, que retornará un Rectangle transformado de acuerdo con los 3 elementos mencionados:
        
        public Rectangle CrearRectanguloLim()
        {
            // Creamos un Rectangle teniendo en cuenta el ancho y alto de la textura.
            Rectangle rectangulo = new Rectangle(0, 0, textura.Width, textura.Height);

            // Necesitamos adaptar ese Rectangulo según la rotación, escala y posición del objeto.
            Matrix transformada = GetMatrizTextura();

            // Se toma el valor de cada esquina del Rectangulo creado.
            Vector2 esquinaSupIzq = new Vector2(rectangulo.Left, rectangulo.Top);
            Vector2 esquinaSupDer = new Vector2(rectangulo.Right, rectangulo.Top);
            Vector2 esquinaInfIzq = new Vector2(rectangulo.Left, rectangulo.Bottom);
            Vector2 esquinaInfDer = new Vector2(rectangulo.Right, rectangulo.Bottom);

            // Se transforman esas 4 esquinas según la figura.
            Vector2.Transform(ref esquinaSupIzq, ref transformada, out esquinaSupIzq);
            Vector2.Transform(ref esquinaSupDer, ref transformada, out esquinaSupDer);
            Vector2.Transform(ref esquinaInfIzq, ref transformada, out esquinaInfIzq);
            Vector2.Transform(ref esquinaInfDer, ref transformada, out esquinaInfDer);

            // Se busca el mínimo y máximo punto del rectangulo.
            Vector2 min = Vector2.Min(Vector2.Min(esquinaSupIzq, esquinaSupDer),
                                      Vector2.Min(esquinaInfIzq, esquinaInfDer));
            Vector2 max = Vector2.Max(Vector2.Max(esquinaSupIzq, esquinaSupDer),
                                                  Vector2.Max(esquinaInfIzq, esquinaInfDer));

            // Retorna un rectangulo transformado
            return new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y));
            }

Así es como se obtiene un Rectangle que se pueda adaptar a las transformaciones de nuestra figura. Con esto, podremos mejorar el rendimiento de nuestra Detección de Colisiones. ¿Cómo?

La clase Rectangle tiene el mismo método que la clase BoundingBox (o BoundingSphere), que conocimos bajo el nombre de Intersects(). Ya sabemos cómo funciona, recuerden que en la entrada anterior lo expliqué. La idea es alivianar el uso de CPU, verificando primero si hubo alguna intersección entre 2 Rectangles y luego, en caso afirmativo, proseguir con la detección a nivel pixel. Tengan en cuenta que usando el método de detección con Rectangle se realiza un sólo control, mientras que con Pixel Perfect se realizan muchos más controles: piensen que se verifica cada uno de los pixeles de una textura con cada uno de los pixeles de otra figura... son muchísimos controles. Esta es la clave de por qué el uso combinado de Detección de Colisiones por Rectangle y Pixel Perfect mejora el rendimiento y mantiene precisión.
Volquemos en el proyecto lo dicho anteriormente. Para ello, necesitamos definir 3 booleanos más (de nuevo, uno para cada objeto) al principio de la clase Game1, que nos sirvan para indicar si hubo o no hubo alguna intersección entre los Rectangles de nuestras figuras. Recordemos también, de inicializar dichas variables en el método Initialize(). Luego, modificamos nuestro método ControlarColisiones(), de acuerdo a lo que habíamos planteado. Y por último, modificamos el método Draw() para que el texto de la pantalla indique si hubo o no colisión. Todas estas modificaciones deberían quedar de esta manera:
        
 public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Figura jugador;                 // Instancia del objeto de clase Figura.
        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.

        Figura figNeutral;              // Referencia del tercer objeto para prueba de colisiones. Es inmóvil.

        SpriteFont font;                // Se guardará el tipo de fuente que se usará en los textos.
        
        // Tomarán el valor de 'true' en caso de que exista alguna colisión a nivel pixel.
        bool huboColisionPix;           
        bool huboColisionNJ1Pix;        
        bool huboColisionNJ2Pix;

        // Tomarán el valor de 'true' en caso de que exista alguna intersección de Rectangle.
        bool huboColisionRect;
        bool huboColisionNJ1Rect;
        bool huboColisionNJ2Rect;

        protected override void Initialize()
        {
            jugador = new Figura();
            jugador2 = new Figura();
            figNeutral = new Figura();
            huboColisionPix = false;
            huboColisionNJ1Pix = false;
            huboColisionNJ2Pix = false;
            huboColisionRect = false;
            huboColisionNJ1Rect = false;
            huboColisionNJ2Rect = false;
            base.Initialize();
        }

        public void ControlarColisiones()
        {
            if (jugador.CrearRectanguloLim().Intersects(jugador2.CrearRectanguloLim()))
            {
                huboColisionRect = true;
                huboColisionPix = ColisionPixelPerfectBool(jugador.GetMatrizPixeles(), jugador.GetMatrizTextura(),
                                                            jugador2.GetMatrizPixeles(), jugador2.GetMatrizTextura()
                                                            );
            }
            else
                huboColisionRect = false;


            if (jugador.CrearRectanguloLim().Intersects(figNeutral.CrearRectanguloLim()))
            {
                huboColisionNJ1Rect = true;
                huboColisionNJ1Pix = ColisionPixelPerfectBool(jugador.GetMatrizPixeles(), jugador.GetMatrizTextura(),
                                                            figNeutral.GetMatrizPixeles(), figNeutral.GetMatrizTextura()
                                                            );
            }
            else
                huboColisionNJ1Rect = false;


            if (jugador2.CrearRectanguloLim().Intersects(figNeutral.CrearRectanguloLim()))
            {
                huboColisionNJ2Rect = true;
                huboColisionNJ2Pix = ColisionPixelPerfectBool(jugador2.GetMatrizPixeles(), jugador2.GetMatrizTextura(),
                                                            figNeutral.GetMatrizPixeles(), figNeutral.GetMatrizTextura()
                                                            );
            }
            else
                huboColisionNJ2Rect = false;
        }

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

            
            if (huboColisionRect || huboColisionNJ1Rect || huboColisionNJ2Rect)
            {
                GraphicsDevice.Clear(Color.BlueViolet);
                textoColision = "Casi";
            
                if (huboColisionPix || huboColisionNJ1Pix || huboColisionNJ2Pix)
                {
                    GraphicsDevice.Clear(Color.DarkSlateBlue);
                    textoColision = "Si";
                }
            }
            else
            {
                GraphicsDevice.Clear(Color.CornflowerBlue);
                textoColision = "No";
            }
            
            // ============ Comienza dibujado ===================
            spriteBatch.Begin();
            //...
            //... y aquí va todo lo que teníamos anteriormente en el método.
            //...
            }
   }


Ahora si ejecutamos el programa, podrán notar que funciona todo lo que estuvimos haciendo. 

¡Excelente! Pero...¿cómo sabemos si realmente funciona?

Para terminar esta entrada, añadiremos algunos métodos que me ayudaron mucho mientras practicaba. Necesité que se pudiera visualizar algunos datos de la figura cuadrada, como su posición, su ángulo de rotación (en grados y radianes), la coordenada (x,y) de su punto de Ancla (u origen) y las coordenadas de cada esquina de su respectivo Rectangle. Lo único que hice fue añadir textos que puedan leerse en la pantalla y que puedan modificarse según las transformaciones de la figura. Todo esto lo hice en el método Draw(). Agreguemos eso:
        
       protected override void Draw(GameTime gameTime)
        {
            string anchoPantalla = Convert.ToString(GraphicsDevice.Viewport.Width);
            string altoPantalla = Convert.ToString(GraphicsDevice.Viewport.Height);
            string textoColision;
            GraphicsDevice.Clear(Color.CornflowerBlue);

            
            if (huboColisionRect || huboColisionNJ1Rect || huboColisionNJ2Rect)
            {
                GraphicsDevice.Clear(Color.BlueViolet);
                textoColision = "Casi";
            
                if (huboColisionPix || huboColisionNJ1Pix || huboColisionNJ2Pix)
                {
                    GraphicsDevice.Clear(Color.DarkSlateBlue);
                    textoColision = "Si";
                }
            }
            else
            {
                GraphicsDevice.Clear(Color.CornflowerBlue);
                textoColision = "No";
            }
            
            // ============ Comienza dibujado ===================
            spriteBatch.Begin();

            // Se dibuja a cada Jugador
            jugador.Dibujar(spriteBatch);
            jugador2.Dibujar(spriteBatch);
            figNeutral.Dibujar(spriteBatch);

            // 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.OrangeRed,
                                    MathHelper.ToRadians(90),
                                    new Vector2(0,0),
                                    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.Orange,
                        0f,
                        new Vector2(0f, 0f),
                        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 + 100,
                                    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 + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y+25),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto que indica el ángulo (en radiandes) del jugador1
            spriteBatch.DrawString(font,
                        "Angulo J1 [rad]: (" + jugador.GetAngulo() + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y+ 50),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto que indica el ángulo (en grados) del jugador1
            spriteBatch.DrawString(font,
                        "Angulo J1 [gra]: (" + MathHelper.ToDegrees(jugador.GetAngulo()) + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y + 75),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto donde indica las coordenadas del punto de ancla
            // (u "Origen") (x,y) del jugador1
            spriteBatch.DrawString(font,
                        "Punto de Ancla J1: (" + jugador.GetPuntoAncla().X + ", " + jugador.GetPuntoAncla().Y + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y + 150),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);

            // Se dibuja un texto donde indica las coordenadas (x,y) del
            // Rectángulo utilizado como BoundinBox (o Cuadro Delimitador) del jugador1
            spriteBatch.DrawString(font,
                        "Coord (X,Y) RectangleJ1: (" + jugador.CrearRectanguloLim().X + ", " + jugador.CrearRectanguloLim().Y + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y + 175),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);
            // Se dibuja un texto donde indica el valor (ancho,alto) del
            // Rectángulo utilizado como BoundinBox (o Cuadro Delimitador) del jugador1
            spriteBatch.DrawString(font,
                        "Ancho y Alto RectangleJ1: (" + jugador.CrearRectanguloLim().Width + ", " + jugador.CrearRectanguloLim().Height + ")",
                        new Vector2(GraphicsDevice.Viewport.TitleSafeArea.Center.X + 100,
                                    GraphicsDevice.Viewport.TitleSafeArea.Y + 200),
                        Color.Aquamarine,
                        0f,
                        new Vector2(0f, 0f),
                        0.8f,
                        SpriteEffects.None,
                        0f);
           
            spriteBatch.End();
            // ============ Termina dibujado ===================         

            base.Draw(gameTime);
        }

De esta manera, cuando ejecutemos el programa, vamos a poder visualizar algunos datos importantes sobre nuestra figura cuadrada. Se vería algo como esto:

Detección por Rectangle y Pixel Perfect, con algunos datos sobre los objetos.

...mmm... sin embargo, a decir verdad, eso no parece ser suficiente. Estaría muy bueno que los Rectangles que creamos se pudieran visualizar de alguna forma, ¿verdad?. ¡Buenas noticias! Mientras investigaba, me encontré con que eso es posible. Así que para concluir esta entrada, haremos que los Rectangles se puedan ver para observar cómo funcionan. Para ello, necesitaremos añadir a nuestro proyecto una nueva textura de tan solo 1x1 pixel. ¿Extraño? Ya veremos para qué sirve.

Entonces, comencemos añadiendo un nuevo atributo a nuestra clase Figura. Será un Texture2D cuyo modificador será static, pues no pertenecerá únicamente a un objeto, sino a todos los objetos de la misma clase. Llamaremos a este atributo como bound ("límite" en inglés). En este campo guardaremos la textura de 1x1 pixel. Pueden crearla ustedes mismos en Paint y luego agregarla al proyecto. Me hubiera gustado mostrarla aquí abajo, pero es tan pequeña que no puedo observarla jajaja.
Luego crearemos un método sobrecargado a Dibujar(), el cual tendrá 2 parámetros más: uno que indique el color en que se quiera ver el Rectangle, y otro que indique el valor de transparencia del Rectangle.
Añadimos lo mencionado, dentro de la clase Figura:
        
class Figura
    {
        private Texture2D textura;  // Textura que se usará para representar al jugador.
        private Vector2 posicion;   // Contendrá las coordenadas de su posición.
                Vector2 puntoAncla; // Contendrá las coordenadas de su punto de anclaje(u Origen) como referencia para rotación y escala.
        private float velocidadMov, // Representa al valor de la velocidad de su desplazamiento.
                      angulo,       // Guardará el valor del ángulo de inclinación del objeto.
                      escala;       // Guardará el valor de la escala de la Textura (o Sprite).

        public static Texture2D bounds; // Utilizamos esta textura para dibujar los BoundingBox.

        public void Dibujar(SpriteBatch spriteBatch, Color colorDelCuadro, byte transparencia)
        {
            // Se elije aplica Transparencia al color indicado.
            colorDelCuadro.A = transparencia;

            // Utilizando lo anterior, dibujo un Cuadro Delimitador.
            spriteBatch.Draw(bounds, CrearRectanguloLim(), colorDelCuadro);

            // Dibujo al objeto.
            spriteBatch.Draw(
                            textura,
                            posicion,
                            null,
                            Color.White,
                            angulo,         //MathHelper.ToRadians(45)
                            puntoAncla,
                            escala,
                            SpriteEffects.None,
                            0f
                            );
        }
    }

Con esto, logramos que se pueda visualizar nuestro Rectangle en la pantalla. Lo que hace el método spriteBatch.Draw() de la línea 18, es tomar una textura ('bounds' en nuestro caso) y rellenar con esa textura un Rectangle (que obviamente es el Rectangle que usamos para detectar colisiones) con el color y la transparencia que le pasamos como parámetro.
Lo que finalmente queda por hacer es invocar al método sobrecargado que creamos recién dentro del método Draw() de la clase principal Game1. Terminemos con esto reemplazando lo viejo por lo nuevo:
        
        protected override void Draw(GameTime gameTime)
        {
            //...
            //...
            // ============ Comienza dibujado ===================
            spriteBatch.Begin();

            // Se dibuja a cada Jugador
            jugador.Dibujar(spriteBatch, Color.DarkTurquoise, 175);
            jugador2.Dibujar(spriteBatch, Color.DarkTurquoise, 175);
            figNeutral.Dibujar(spriteBatch, Color.DarkTurquoise, 175);
            
            //...
            //...
        }

¡Y eso es todo! Ahora, si ejecutamos nuestro programa podremos ver a nuestro Rectangle de la siguiente forma:

Visualización de las transformaciones de los Rectangle, según las características de sus respectivos objetos.

Podremos notar entonces que el Rectangle se adapta a la posición, rotación y escala de su respectivo objeto. Cuando 2 Rectangles se intersectan, el texto de la esquina inferior izquierda de la pantalla indica que "casi" hubo una colisión. Entonces, al cumplirse la detección por Rectangle, comienza luego la detección Pixel Perfect y, si esta también se cumple, el texto indicará finalmente que "" hubo colisión. Realmente es muy útil que podamos visualizar la manera en que se llevan a cabo las detecciones de colisiones. ¡Espero que les sirva este método!
 
Conclusión
Aviso: Tengo mucho sueño...
Uff... ¡Por fin! Esta es la Entrada más difícil que me tocó armar hasta ahora. Costó muchos dolores de cabeza, pero me alegra demasiado haber terminado esta parte. ¡Y voy por más!

Aún no estoy seguro de lo que vendrá en la siguiente Entrada, pero estoy pensando en desarrollar un juego bastante básico, donde se aplique todo lo que vimos hasta ahora y quizás otras cosas nuevas más. Lo importante es que con esta entrada, en mi opinión, ya avanzamos lo suficiente. De aquí se pueden hacer muchas cosas, lo importante es animarse y, por lo menos, intentar.

Pueden descargar el código fuente de este proyecto en el siguiente botón. Les comento que hay agregadas más cosas que no muestro en esta Entrada, por el simple hecho de que no eran importantes. Por ejemplo, los métodos tienen sus respectivos comentarios, hay otra textura que pueden intercambiar por la de otro objeto, hay más métodos sobrecargados, etc. ¡Espero que les sirva tanto como a mí! 



Ahora me toca descansar un poco. ¡Hasta la próxima!


¡Sigue adelante, siempre!

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!