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, 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!

jueves, 22 de marzo de 2012

Juego: Todajato (GGJ 2012)

"La fama es peligrosa, su peso es ligero al principio, pero se hace cada vez más pesado el soportarlo y difícil de descargar."
Hesíodo (Siglo VIII a.C.) - Poeta griego.
Antes que nada...
Agradecimientos: Nuevamente, gracias a Killabunnies por el aguante de siempre. Agradezco también a todos los que compartieron esta experiencia, en especial a quienes conformaron nuestro grupo de desarrollo e hicieron posible este pequeño juego: Tozy, Tomás y Javier.
Imagen 0. Portada de nuestro juego, Todajato.
¡Saludos!

Este juego fue realizado durante el famoso evento del Global Game Jam, correspondiente al ciclo 2012. Es la primera vez que participo y la verdad estaba algo nervioso, pero tenía muchas ganas de participar. Estuve esperando la oportunidad desde hace muchos meses.
Una de las sedes del evento, fueron las oficinas de Killabunnies en la cual participé junto a otras 18 personas más. Se conformaron 5 grupos en esta ocasión para trabajar en distintos proyectos.
Imagen 1. Algunos participantes desarrollando en la primer noche de la Jam.

Cada año, el evento tiene una temática distinta. Esta vez, se trató sobre una imagen de la cual debíamos encontrar alguna interpretación y plasmarla en un juego. Esta fue la imagen:

Imagen 2. Imagen de la Temática del GGJ 2012.

A partir de ella, se fueron planteando distintos puntos de vista. De entre todas ellas, se eligieron algunas y luego se conformaron los grupos de trabajo.
Nuevamente, mis conocimientos en programación de videojuegos no eran los suficientes, por lo que opté por diseñar la música y los sonidos del juego.

Imagen 3. Quien les escribe, pensando y esperando un poco de inspiración.


Logré componer 2 tracks para el juego: "Jungle" y "Tribu-To". Son simples, pero sinceramente me gustó mucho la percusión del segundo. Se puede escuchar ambos tracks y también descargarlos:



En fin, fue otra gran experiencia que me incentiva a seguir adelante y continuar practicando para poder ayudar mejor en las próximos eventos.

Espero que disfruten de este pequeño juego. Personalmente, me encantó y me pareció muy divertido. Realmente el grupo hizo un gran trabajo, que disfruté de principio a fin.
Invito también a que vean el resto de los juegos que salieron de este GGJ 2012:
Imagen 4. A comienzos del primer día del GGJ 2012.

  • Circles of Destiny: Luciano Vairoli, Andrés Wolkovicz, Tozy, Andrés Miranda.
  • Spider's Loop: Andrés Miranda, Gonzalo Agosta, Emiliano Spesot.
  • Panic Snake: Cesar Regueira, Marcelo Guardia, Mariano Obeid, Esteban Corva, Juan Pablo Ferreyra.
  • Elusive Spirit: Esteban Corva, Luciano Vairoli, Jerónimo Barroco, Ignacio Puccini.



Todajato V 1.0
Global Game Jam: Es el mayor encuentro mundial de creación de juegos. Dura 48 horas y tiene lugar cada año a fines de enero. El GGJ reúne a miles de entusiastas de los videojuegos, los cuales participan desde sedes repartidas por todo el mundo. El GGJ es un proyecto de la International Game Developers Association (IGDA).
"Geko, es un mago atrapado en el cuerpo de una iguana. Debe escapar del antiguo templo y de la jungla para poder dominar el mundo, pero requerirá que implemente una estricta dieta basada en maldiciones y partes de él mismo."

Imagen 5. Un Screenshot del segundo nivel del juego.

Creamos un juego basado en puzzles con algunos elementos de "acción". Controlas a un pequeño reptil que debe alcanzar un cierto peso en orden para abrir puertas a través de interruptores. Para obtener power-ups (soles que te hacen aumentar de peso y calaveras que tienen el efecto inverso) debes sortear crueles trampas. Si una estaca o una sierra hace contacto contigo, perderás una pierna. El número de piernas afecta la velocidad de movimiento. Pero las piernas pueden ser recuperadas. También, si comes mucho aumentarás mucho de peso como para evadir trampas o deslizarse a través de pasadizos.

Imagen 6. De izquierda a derecha: Javier, Tomás, Tozy y yo.
Autores:
  • Manuel Carazo (Tozy) - Diseño de juego y Diseño de niveles.
  • Tomás Gutiérrez - Programación.
  • Javier Oliver - Diseño gráfico general.
  • Dante Costilla - Música y efectos de sonido.
Controles: flechas arriba, abajo, izquierda y derecha.


¡Sigue adelante, siempre!

lunes, 19 de marzo de 2012

Level 01: Creación y Movimiento de un Objeto

"El ser inmóvil mueve como objeto del amor, y lo que él mueve imprime el movimiento de todo lo demás."
Aristósteles (384 a.C. - 322 a.C.) - Filósofo griego.

Antes que nada...

Aviso: Usaremos como base el mismo código fuente desarrollado en la entrada anterior.

¡Saludos!

La entrada anterior trató sobre cómo empezar un nuevo proyecto y un poco sobre el uso de texto, lo cual nos resultará útil para visualizar algunos datos importantes más tarde. En esta ocasión, daremos un poco de vida a nuestro proyecto creando una figura que luego podremos controlar el movimiento de la misma con el teclado. Dicha figura, representará al jugador. También se hablará un poco sobre las características del sistema de coordenadas y haremos algunos experimentos para ir familiarizándonos con él. Eso es todo por ahora. Lo sé... al fin algo entretenido.

Estos son los temas que se verán a continuación:
  1. Pensando en un objeto para el jugador.
  2. Diseñando una clase para el juego.
  3. Creación un objeto para el juego, representando al jugador.
  4. Caracterísitcas del Sistema de Coordenadas 2D en XNA.
  5. Movimiento del objeto creado a través del teclado.
Imagen 0. Manipularemos el movimiento de esta pequeña figura con el teclado.


Pensando en un objeto
Cada vez que tomamos el joystick y encendemos una consola para jugar un juego, controlamos (en la mayoría de los casos) a un personaje y quizás participamos de una atrapante historia llena de desafíos por delante. Ese personaje que nos representa podría tener sus aliados o enemigos que interferirán en el objetivo del juego. Cada uno de ellos tendría sus propias características que los difrencian entre sí. Pero también, hay otras características que podrían tener en común.

Veamos los siguientes personajes improvisados. A simple vista, ¿qué diferencias tienen y qué cosas podrían tener en común?


Pensemos que estos personajes son protagonistas de un juego de pelea, por decir un ejemplo. Seguramente cada uno de ellos tienen sus propios puntos de vida, los de fuerza, los de defensa. Además se podrían diferenciar por su velocidad, su tamaño, sus colores, etc, etc, etc.

Lo importante de esto, es entender que ambos personajes tienen sus propias características que pueden diferenciarlos o asimilarlos. Por ejemplo, lo primero que se vé es la diferencia de tamaños y sus colores. Pero quizás tengan los mismos puntos de vida o la misma velocidad.

Y... ¿para qué sirve este análisis?

Es muy importante tener estas cosas en cuenta, pues estamos utilizando un lenguaje orientado a objetos y sería importante sacar el mayor provecho que podamos. Tengan en cuenta que en un futuro no sólamente vamos a tratar con 1 o 2 objetos (estoy seguro que no), sino con muchos más.

No soy un visionario, pero pensando un poco en las posibles clases de objetos que vayamos a utilizar en un futuro, creo que hay 2 características que van a tener en común la mayoría (por no decir todos) de los objetos: una posición y una textura (sprite). Pensando también en que la mayoría tendrán movimiento, podríamos agregar como otra característica una velocidad. Pensando en herencia, personalmente creo que estos atributos serían los más básicos. A lo mejor me esté equivocando pero lo estaremos comprobando con el correr del tiempo.

Mientras, vamos a crear una clase que va a representar a un jugador. No se trabajará con personajes como los anteriores todavía (lamentablemente). Usaremos algo más sencillo para empezar: Figuras geométricas. Y para hacer la experiencia más sencilla todavía, solamente usaremos por ahora los "atributos base" mencionados en el párrafo anterior.

Asi que... ¡comencemos!



Creación de la clase Figura
Empezaremos creando una clase, el "molde" (como decía mi profesor), para crear los futuros objetos.

Los atributos (o campos) que tendrá nuestra clase Figura serán:
  • textura [Texture2D]: hace referencia al Sprite que usaremos para vizualizar al objeto en la pantalla.
  • posicion [Vector2]:  un vector bidimensional que contendrá los datos sobre la ubicación del objeto en la pantalla, dados una coordenada horizonal 'x' y una vertical 'y'.
  • velocidad [float]: guardaremos en esta variable el valor con la velocidad que deseamos que se mueva la figura.
Serán estos, entonces, los parámetros mínimos para poder crear un objeto de la clase figura. Vamos a la barra de menú y creamos una nueva clase: Project >> Add Class. Elegimos un nombre para la clase y aceptamos. En mi caso, este es su código:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;          // Espacio de nombre necesario para usar Vector2
using Microsoft.Xna.Framework.Graphics; // Espacio de nombre necesario para usar Texture2D

namespace Prueba01
{
    class Figura
    {
        private Texture2D textura;   // Textura que se usará para representar al jugador.
        public Vector2 posicion;            // Contendrá las coordenadas de su posición.
        private float velocidadMov;  // Representa al valor de la velocidad de su desplazamiento.

        public void Inicializar(Texture2D textura, Vector2 posicion, float velocidadMov)
        {
            this.textura = textura;
            this.posicion = posicion;
            this.velocidadMov = velocidadMov;
        }

        public void Dibujar(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(
                            textura,
                            posicion,
                                null, 
                            Color.White,
                            0f,         //MathHelper.ToRadians(45)
                            Vector2.Zero,
                            0.5f,
                            SpriteEffects.None,
                            0f
                            );
        }

        public int GetAncho()
        {
            return textura.Width;   // Se retorna el valor (en pixeles) del ancho de la textura.
        }

        public int GetAlto()
        {
            return textura.Height; // Se retorna el valor (en pixeles) del alto de la textura.
        }

        public float GetVelocidad()
        {
            return velocidadMov; // Se retorna el valor de la velocidad establecida.
        }
    }
}
Noten que al principio del código declaré 2 Namespaces (Microsoft.Xna.Framework y Microsoft.Xna.Framework.Graphics). El primero contiene una clase fundamental que usa un vector bidimensional, llamada Vector2, que nos servirá para establecer una posición en la pantalla. El segundo posee la clase Texture2D, necesaria para poder dibujar luego el Sprite del objeto en la pantalla.

En la entrada referida a la Introducción a XNA 4.0, hablamos un poco sobre su framework y sobre los Espacios de Nombres (NameSpaces) que contiene esa biblioteca. Usaremos varios de ellos a partir de ahora, asi que recomiendo que vuelvan a hechar un vistazo.

Vean que también se agregó un método Dibujar() que necesita como parámetro una referencia de la clase SpriteBatch. Esta clase es proporcionada por el segundo namespace mencionado anteriormente y poseé un método sobrecargado llamado Draw() que nos permitirá dibujar nuestra figura en la pantalla. Podríamos expresar ese método de esta forma:


spriteBatch.Draw(textura, posición, rectángulo de origen, color, rotación, origen, escala, efecto de sprite, profundidad de capa)

En donde:



  • textura [Texture2D]: es la textura del sprite que usaremos para representar a la figura.
  • posición [Vector2]: es la posición en coordenadas (x,y) del punto en la pantalla donde se dibujará el sprite.
  • recángulo de origen [Nullable<Rectangle>]: rectángulo que especifíca los téxeles de origen de una textura. Usamos null para dibujar toda la textura. De lo contrario, pasamos un rectángulo con las dimensiones deseadas. Se dibujará la parte de la textura que esté comprendida dentro de esas dimensiones.
  • color [Color]: indica el color para tintar un sprite. Usamos Color.White para no tintar el sprite, es decir que no se modificarán sus colores originales.
  • rotación [float]: indica el ángulo (dado en radianes) de rotación del sprite respecto a su centro.
  • origen [Vector2]: es el punto de origen de la textura. La rotación de la textura, tomará como referencia este punto. Por defecto, usamos siempre Vector2.Zero o "(0,0)" que representa el punto de la esquina superior izquierda de la textura. Hay que prestar atención cuando se modifica este parámetro, ya que puede generar algunas confusiones luego.
  • escala [float]: el valor de escala del sprite. Para dibujar el tamaño original, se usa como valor 1f. Pero en este caso usé 0.5f, por lo que se dibujará la figura con la mitad de su tamaño original.
  • efecto de sprite [SpriteEffects]: indica si se efectuará algún efecto al sprite. Los efectos que se pueden efectuar son 2: voltear el sprite horinzontalmente o verticalmente.
  • profundidad de capa [float]: representa la "profundidad" del sprite, en relación a otros sprites. Consideramos como una "capa": Imaginen que tienen una pila de hojas de papel (o capas) apoyadas en un escritorio y que las observan desde arriba. La hoja que está en frente de todas (la capa de tope) tendría el valor de 1f mientras que la que está al fondo de todas (la capa base) tendría un valor de 0f. Las hojas que están entre esas 2, tendrían un valor entre el intérvalo [0f - 1f]. Teniendo en cuenta esto, las capas que tengan el valor más cercano a 1, son las que estarán al frente y se verán primero, "tapando" a aquellas que tengan un valor menor (que tiendan a 0).



Creación de un objeto y Sistemas de Coordenadas 2D en XNA

Bien, ya creamos nuestra clase y ahora continuamos creando una figura para mostrarla en pantalla. Elegí este simple e inexpresivo cuadrado para que luego manipulemos sus movimientos con el teclado:



Figura 1. Este será el "personaje" que el jugador controlará.

Es un poco grande, pero el método Dibujar() que realizamos reduce la escala de cualquier textura a la midad. Es sólo por cuestión de comodidad.

Vamos a la clase principal Game1 y declaremos una instancia de la clase Figura. Declaremos también otra variable tipo float que contenga el valor de la velocidad con la que se desplazará el cuadrado.

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.
     
        SpriteFont font;                // Se guardará el tipo de fuente que se usará en los textos.

Ahora continuemos con la creación del cuadrado que representará el jugador. Simplemente agregamos esto en el método Initialize():


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

            base.Initialize();
        }

Continuamos estableciendo el punto de origen y cargando la textura que usaremos para el cuadrado. Esto corresponde hacerlo en el método LoadContent():


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.Y
                                                );
            jugador.Inicializar(Content.Load<Texture2D>("Texturas/Azul"), posicionJugador, velocJugador);
            font = Content.Load<SpriteFont>("SpriteFont1");
        }


Por último, nos queda dibujar a nuestro amigo cuadrado en la pantalla. Como ya tenemos desarrollado este método en la clase Figura, sólo tenemos que llamarlo dentro del método Draw() de la clase principal Game1. Entonces, a lo que ya teníamos, le agregamos lo mencionado y quedaría así:



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);
           
            spriteBatch.End();
            // ============ Termina dibujado ===================           
                 

            base.Draw(gameTime);
        }

A estas alturas, si ejecutamos el programa deberíamos poder observar al cuadrado en la esquina superior izquierda de la pantalla de juego.

Todavía no realizamos un método que pueda mover al objeto que creamos. Lo haremos luego, pero primero hablemos un poco sobre el Sistema de Coordenadas 2D en XNA.



Sistemas de Coordenadas 2D en XNA

El Sistema de Coordenadas 2D aplicado en XNA tiene ubicado su origen (0,0) en la esquina superior izquierda de la pantalla de juego. Los valores del eje X van creciendo desde ese punto hacia la derecha, hasta llegar a su punto máximo, que es el límite de la pantalla (en este caso, el límite es 800). Los valores del eje Y van creciendo desde el origen hacia abajo, hasta llegar a su punto máximo (en este caso, el límite es 480). La siguiente imagen representa todo lo mencionado:


Figura 2. Sistema de Coordenadas 2D en XNA


Podemos obtener esos valores a través de una propiedad llamada "Viewport", que pertenece a la clase "GraphicsDevice" incluída dentro del NameSpace "Microsoft.Xna.Framework".

Recordemos que en la clase Figura definimos el método Inicializar() y que necesita (además de la textura y la velocidad) un parámetro de tipo Vector2, el cual representa su posición inicial. Ahora, volviendo a la clase principal Game1, recordemos también que dentro del método LoadContent() creamos una referencia del tipo Vector2 con 2 valores (x,y) que representan a un punto del sistema de coordenadas bidimensional. Dichos valores fueron (0,0), por lo que el cuadrado aparece dibujado en la esquina superior izquierda de la pantalla.

Podemos modificar esos valores por otros, en caso de que se desee crear el objeto en otra posición. Por ejemplo, podemos crear al objeto en el centro de la pantalla. Para ello, tendríamos que modificar los parámetros al crear la referencia del tipo Vector2. En este caso es útil la propiedad Viewport que mencionamos anteriormente, ya que gracias a ella podemos obtener, entre otras cosas, el punto del centro de la pantalla:


Vector2 posicionJugador = new Vector2(
                                                GraphicsDevice.Viewport.TitleSafeArea.Center.X,
                                                GraphicsDevice.Viewport.TitleSafeArea.Center.Y
                                                );
            jugador.Inicializar(Content.Load("Texturas/Azul"), posicionJugador, velocJugador);

Si hacemos esta modificación, al ejectuar el programa tendríamos que ver a nuestro amigo azul en el centro de la pantalla. De esta forma:


Figura 3. Ubicación del punto del centro de la pantalla y Origen de la textura.


Bueno, no está exactamente dibujado en el centro de la pantalla. ¿Por qué? Porque al inicializar el objeto, le pasamos como parámetros las coordenadas del centro de la pantalla (400,240) con la ayuda de la propiedad Viewport. Este punto va a ser el origen del objeto. Recordemos una vez más que en el sistema de coordenadas 2D de XNA, los valores del eje X y del eje Y se incrementan desde el origen hacia la derecha y hacia abajo respectivamente, por lo que el método Dibujar() dibujará (valga la redundancia) la textura del objeto comenzando desde su punto de origen (parámetro que pasamos como posición en el método Inicializar()) y desplazándose en las direcciones mencionadas.

¿Dí muchas vueltas? Espero que se haya entendido, porque es algo que hay que tener muy en cuenta. Cuesta entender un poco hasta que uno se acostumbra. Experimenten con otros valores para que entiendan mejor el funcionamiento, no hay mejor forma.

Juguemos un poco con la rotación del objeto ahora. Hace poco hablamos del método Draw() que creamos en la clase Figura. Tengamos en cuenta 3 de todos sus parámetros: posición, rotación y origen. En estos momentos, tenemos a nuestra figura ubicada en el punto (400,240), que es el punto de su posición. La rotación tiene un valor de 0f, que es lo mismo que una rotación de 0º. Y por último, el punto origen de la textura es (0,0).
Cambiemos un par de cosas. En la entrada anterior vimos cómo rotar un texto con la ayuda de la clase MathHelper. Cambiemos entonces la rotación de nuestro amigo a unos 45º, de esta manera:


public void Dibujar(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(
                            textura,
                            posicion,
                                null, 
                            Color.White,
                            MathHelper.ToRadians(45), //anteriormente  0f
                            Vector2.Zero,
                            0.5f,
                            SpriteEffects.None,
                            0f
                            );
        }


Entonces lo que veremos al ejecutar el programa será lo siguiente:

Figura 4. Ejemplo de rotación de la textura en 45º, respecto al punto de su origen.

¿Qué pasa ahora si cambiamos el origen de la textura? Quiero tener el origen ubicado en el centro de la textura. En este caso, la textura es de 200x200 pixeles. La clase Figura que diseñamos, tiene 2 métodos: GetAncho() y GetAlto(), los cuales nos devuelven el valor del ancho y del alto respectivamente de la textura que estamos usando. Esto nos resultará útil ahora:

public void Dibujar(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(
                            textura,
                            posicion,
                                null, 
                            Color.White,
                            MathHelper.ToRadians(45), //esto estaba en 0f
                            new Vector2 (getAncho()/2,getAlto()/2),
                            0.5f,
                            SpriteEffects.None,
                            0f
                            );
        }

Veamos qué sucede:


Figura 5. Ejemplo resultante de la modifiación de los valores de origen de la textura.

Ahora sí podría decir que la figura está en el centro de la pantalla. Esto es lo que pasa cuando cambiamos el origen de la textura. Este es otro ejemplo con el origen de la textura modificado:


Figura 6. Otro ejemplo de la modificación del origen de la textura.

En fin, resulta demasiado útil saber cómo lidiar con estos aspectos en el sistema de coordenadas de XNA. Pero también es importante tener cuidado cuando combinamos esto con la detección de colisiones, tema que se desarrollará en la siguiente entrada.


Movimiento del Objeto con Teclado

Agreguemos este último e importante ingrediente al proyecto: controlar sus movimientos con el teclado.

Para ello, necesitamos del NameSpace "Microsoft.Xna.Framework.Input" que contiene clases que reciben entradas desde el teclado, el mouse y el joystick de la consola Xbox360. Es decir, estas clases son las que permiten a nuestro juego interactuar con el/los jugadores. Para empezar de a poco, sólo realizaremos el movimiento de nuestro objeto a través del teclado. La clase que nos permitirá hacer esto, se llama "KeyboardState". ¡Comencemos a dar vida a nuestro proyecto!

Primero, necesitamos crear una referencia a la clase KeyboardState, al inicio de la clase principal Game1:

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.
     
        SpriteFont font;                // Se guardará el tipo de fuente que se usará en los textos.

Ahora que tenemos una referencia en dónde guardar los datos que enviamos con el teclado, necesitamos, justamente, hacer que se guarden. Recordemos que el Loop de Juego de XNA es un ciclo que afecta a 2 principales métodos de la clase principal Game1: Draw() y Update(). Sería correcto que guardemos los datos sobre qué tecla se presionó, en cada loop del juego. Por eso, lo indicado sería hacerlo dentro del método Update().
Una vez que guardamos dicho estado, deberíamos poder mover a nuestro amigo dependiendo de la tecla que hayamos presionado. Es decir, sería necesario tener un método que nos permita modificar la posición del objeto creado, luego de analizar el último estado del teclado. Aplicando todo lo dicho en 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();
            MoverFigura(gameTime);

            base.Update(gameTime);
        }

        private void MoverFigura(GameTime gameTime)
        {
            // Analizamos qué tecla fue presionada y luego modificamos su posición

            if (estadoTeclado.IsKeyDown(Keys.Left))
            { jugador.posicion.X -= jugador.GetVelocidad(); }

            if (estadoTeclado.IsKeyDown(Keys.Right))
            { jugador.posicion.X += jugador.GetVelocidad(); }

            if (estadoTeclado.IsKeyDown(Keys.Up))
            { jugador.posicion.Y -= jugador.GetVelocidad(); }

            if (estadoTeclado.IsKeyDown(Keys.Down))
            { jugador.posicion.Y += jugador.GetVelocidad(); }

            jugador.posicion.X = MathHelper.Clamp(jugador.posicion.X, 0,
                                                GraphicsDevice.Viewport.Width - jugador.GetAncho() /2);
            jugador.posicion.Y = MathHelper.Clamp(jugador.posicion.Y, 0,
                                                    GraphicsDevice.Viewport.Height - jugador.GetAlto() /2);
        }


Eso es todo, es fácil de interpretar el código. Para mover a la figura, sólo debemos presionar las teclas arriba, abajo, izquierda o derecha según hacia dónde se nos apetezca moverlo. Al determinar qué tecla fue presionada, se modificará la posición del objeto dependiendo de la velocidad que indicamos al principio. Lo único extraño es el método "Clamp()", el cual restringe un valor dentro de un intérvalo dado. En nuestro caso, le indicamos que el objeto no se puede pasar de los límites de la pantalla. Otro método muy útil de nuestro ya muy querido amigo MathHelper.

Y ahora sí, si ejecutamos el código vamos a poder mover a la figura cuadrada por toda la pantalla. ¡Al fin algo se mueve!


Conclusión
Hemos pensado en un objeto que pueda controlar un jugador. Realizamos una clase con sus atributos más importantes y logramos moverlo en la pantalla. Hicimos algunas pruebas de rotación para entender y acostumbrarse un poco al sistemas de coordenadas 2D de XNA. Pueden descargar el código fuente haciendo click en el siguiente botón: 



En la próxima entrada crearemos otro objeto para manipularlo esta vez con el mouse y veremos uno de los temas más importantes: Detección de Colisiones. Tengo ganas de hacer un juego muy muy simple luego de eso, como para simplificar todo lo que hicimos hasta ahora. Es una posibilidad, veremos qué sucede.

¡Hasta entonces!


¡Sigue adelante, siempre!