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!

2 comentarios:

Hola!

Un blog con gran potencial, felicidades y estaré siguiendo tus post

saludos

Muchas gracias tavox!
Eres siempre bienvenido al blog.

Un abrazo.

Publicar un comentario