[XNA] Cambios en la API gráfica de XNA4: adiós ResolveTexture2D

Lo cierto esque aunque todavía BETA, la nueva API de XNA 4 contiene algunos cambios que nos pueden hacer un poco la puñeta si estamos migrando un juego de XNA 3.1 a 4.0. Entre ellos, la desaparición e ResolveTexture2D. Esta clase podía usarse por ejemplo para hacer «capturas de pantalla». ¿Para qué queremos hacer una captura de pantalla en un juego? Pues a mi se me ocurre que por motivos de depuración, y también para hacer efectos de fade-in y fade-out. Yo lo estaba usando en un proyecto personal para hacer fade-outs en los menús del juego. Pues bien, para quien no tenga muchas ganas de buscarse la vida, el código que nos permite capturar la pantalla en XNA 4 se basa en la modificación del RenderTarget y es el siguiente:

protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            RenderTarget2D render = new RenderTarget2D(GraphicsDevice,
                GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height,
                false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);

            GraphicsDevice.SetRenderTarget(render);

            spriteBatch.Begin();
            spriteBatch.Draw(texture, Vector2.Zero, Color.White);
            spriteBatch.End();

            System.IO.FileStream fs = new System.IO.FileStream(@»archivo.png», System.IO.FileMode.OpenOrCreate);

            GraphicsDevice.SetRenderTarget(null);
            render.SaveAsPng(fs, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);

            fs.Flush();        

            base.Draw(gameTime);
        } 

Como véis se trata de crear un nuevo RenderTarget, establecerlo en el Graphics Device, hacer el render, y volver a dejar el RenderTarget del GraphicsDevice a su estado previo, después de eso podemos hacer lo que queramos con la textura. Aquí claramente estoy guardando la textura como PNG, pero también podríais guardarla en un objeto textura (en Phone por ejemplo no podéis guardar una imagen en la C:, sí en el isolatedstorage pero eso sería otra historia),  Esto sería simplemente lo siguiente:

 Texture2D newTexture = render;

[XNA] Primeras pruebas con la beta de Windows Phone 7 Developer Tools

Hace un tiempo que estoy trabajando en secreto en la tecnología windows phone bajo la versión CTP (vaya se me escapó). Lo cierto es que uno de los problemas principales que tenía esa versión era el rendimiento: es horrible (y tampoco es que tenga una mala máquina). Así que, tube que olvidarme de hacer mucha cosa con esa versión del emulador. Con la salida hace pocos días de la versión BETA de Windows Phone 7 Developer Tools, lo primero que busqué fueron las mejoras en rendimiento. Y sorpresa ha sido la mía, ya que ha superado mis expectativas.

En este vídeo he grabado la ejecución de las demos del engine de físicas BepUPhysics Engine, uno de los pocos engines de física gratuitos que es compatible para WP7 desde la versión CTP.  Podéis verlo vosotros mismos, no está nada mal:

[View:http://www.youtube.com/watch?v=73J7Vi1X1o8:550:0]


Otras novedades interesantes de la BETA respecto la CTP

Las novedades de la versión Beta respecto la CTP son muchísimas, podéis consultarlas en las release notes, algunas que me han parecido más curiosas y/o interesantes:

  • Expression Blend 4 integrado y compatible con la Beta de las WPDT
  • Herramienta de registro, para probar la aplicación en un dispositivo físico antes de que la aplicación aparezca en el marketplace
  • Reestructuración de los namespaces
  • Nuevos controles para Silverlight

 

[webcast] Windows Phone 7 Jump Start. Cuatro sesiones de XNA y Silverlight para WP7!! (en inglés)

Con el reciente lanzamiento de la versión BETA de Windows Phone 7 (hasta ahora sólo teníamos una CTP) , el equipo de WP7 ha preparado una serie de sesiones formativas, en formato webcast, que no nos podemos perder, tanto los desarrolladores de aplicaciones «clásicas» como de videojuegos:

Session One – Getting Started with Microsoft Windows Phone 7 and Silverlight
https://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032455932&EventCategory=2&culture=en-US&CountryCode=US

Session Two – Programming Game Applications with XNA
https://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032455936&EventCategory=2&culture=en-US&CountryCode=US

Session Three – Programming Applications with Silverlight
https://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032455937&EventCategory=2&culture=en-US&CountryCode=US

Session Four- Review and Wrap Up
https://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032455938&EventCategory=2&culture=en-US&CountryCode=US

 

Nos vemos pronto!

[XNA] Conectando a un servicio web desde XNA 4.0 con Windows Phone 7

Conectarnos a un servicio web desde nuestros juegos puede ser útil, por ejemplo para recoger información de cuanta gente juega, desde donde, cuando… entre otras cosas que se nos puedan ocurrir. Lo malo es que desde XNA, en Windows Phone, no podemos utilizar referencias web. Bueno, de hecho sí se podría… añadiendo una DLL de Silverlight a la solución, añadirle la referencia al servicio, referenciar este proyecto desde el juego y instanciar el proxy del webservice… pero da un warning muy feo, y no se si este pequeño «hack» tendrá cabida en el Marketplace. Así que la forma elegante de hacerlo es mediante los webrequest de toda la vida.

Para empezar la prueba lo primero que necesitaremos es un webservice al que conectarnos. Para ello yo me he creado un WebService de toda la vida con VS2008 y lo he colgado en mi IIS local, de manera que esta es la URL de mi servicio: http://localhost/service/service1.asmx. El servicio contiene el método HelloWorld que viene creado por defecto en cualquier servicio que hagamos con Visual Studio, que devuelve un simple string: «Hola mundo!!!». Asumiendo que todos sabemos crear un webservice como este, vamos a ver como recuperar sus datos desde el teléfono.

En el método Initialize de cualquier proyecto XNA, en la clase Game, instanciaremos un objeto HttpWebRequest y haremos un post. Observad que el post se realiza asíncronamente con BeginGetResponse, y es que en XNA para Phone no hay otra… no lo podemos hacer síncronamente. En esta llamada asíncrona se pasan como parámetros primero Response_Completed, que es el método que se llamará cuando termine el Response, y el objeto httpWebRequest, que es el que queremos recuperar en este método.

        protected override void Initialize()
        {
            string webServiceAddress = @»http://localhost/service/service1.asmx«;           
            string methodName = «HelloWorld»;

            string webServiceMethodUri = string.Format(«{0}/{1}», webServiceAddress, methodName);

            HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(webServiceMethodUri);
            httpWebRequest.Method = «POST»;

            httpWebRequest.BeginGetResponse(Response_Completed, httpWebRequest);
           
            base.Initialize();
        }

 Y esta sería la implementación del método Response_Completed. lo más interesante es que tenemos que parsear el XML resultante. Esto podría hacerse mediante una serialización / deserialización de XML en objetos, y nos ahorraríamos tener que hacer el parseo a mano, pero como ejemplo sencillo esto nos vale.

       void Response_Completed(IAsyncResult result)
        {
            HttpWebRequest request = (HttpWebRequest)result.AsyncState;
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

            using (StreamReader streamReader = new StreamReader(response.GetResponseStream()))
            {
                string xml = streamReader.ReadToEnd();

                using(XmlReader reader = XmlReader.Create(new StringReader(xml)))
                {
                    reader.MoveToContent();
                    reader.GetAttribute(0);
                    reader.MoveToContent();

                    this.mensaje = reader.ReadInnerXml();
                }
            }
        }

Y ya sólo queda pintar el mensaje que hemos obtenido en pantalla… este código es trivial:

       protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();
            spriteBatch.DrawString(fuente, this.mensaje, new Vector2(30.0f, 40.0f), Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }

 El resultado de la ejecución sería el siguiente:

[XNA] Localización de videojuegos en XNA (multiidioma)

Si haces un juego con el objetivo de venderlo, en un mercado global como es el de hoy día… es imprescindible que tu videojuego sea multiidioma. Cuantos más idiomas tenga, por supuesto tanto mejor, y aunque se supone que todos somos capaces de entender el inglés, siempre se hace más cómodo jugar con el idioma nativo de uno: el cliente lo agradece! Como se verá en este tutorial, no es una tarea complicada. Una vez la aplicación está preparada para ser localizada a un idioma, es muy fácil añadir muchos más idiomas, consistiendo la única dificultad a partir de ese momento en traducir las cadenas de texto.

Este vídeo refleja el resultado de la ejecución del código que os podéis descargar con este ejemplo, se muestran textos y una foto en Castellano, Inglés y Catalán. La foto refleja claramente la cultura de cada país: Bocata calamares, Hamburguesa y Calçots respectivamente.

En cualquier aplicación .Net la localización se hace de forma muy parecida, y XNA no iba a ser menos. La traducción de los textos se basa en ficheros de recursos, de extensión “resx”. Para añadir uno de estos documentos iremos al proyecto –> Añadir -> Nuevo elemento –> Visual C# –> Fichero de recursos.

Al fichero le podemos poner el nombre que queramos, en mi caso le he puesto “Textos.resx”, y este será el fichero que contenga los textos del idioma por defecto del juego, ejemplo:

Textos

Una vez tengamos todos los textos en un idioma, necesitaremos crear nuevos ficheros Resx, uno para cada uno de los idiomas a los que queramos traducir la aplicación. En mi caso la he traducido al inglés y al catalán, así que añado los ficheros:

  • Textos.en.resx
  • Textos.ca.resx

Como ya habréis deducido, es necesario traducir sólo la columna “Value” en el fichero RESX, no así la columna “Name”, que hace la función de clave de acceso al recurso. En el fragmento de código podéis ver como se establece la cultura por defecto a español, al inicializar el juego. Podríamos inicializarla también al idioma que tenga el jugador… y eso es lo ideal, así no tenemos que preguntarle que idioma quiere, la cultura del jugador la obtenemos con CultureInfo.CurrentCulture (puede observarse comentado en el siguiente código):

   1: protected override void Initialize()

   2: {

   3:     font = Content.Load<SpriteFont>("font");

   4:  

   5:     this.EstablecerCultura("es");

   6:  

   7:     //CultureInfo.CurrentCulture.ToString();

   8:  

   9:     base.Initialize();

  10: }

El método “EstablecerCultura” no es más que un método propio, un helper que cambia la cultura que referenciaremos de los ficheros de recursos, así como cargar la foto en la cultura que toca.

   1: private void EstablecerCultura(string cultura)

   2: {

   3:     Textos.Culture = CultureInfo.GetCultureInfo(cultura);

   4:  

   5:     foto = Content.Load<Texture2D>("foto-" + cultura);

   6: }

Y ya casi estamos! Ahora sólo falta cambiar la cultura bajo petición del usuario, eso lo hago en el método Update():

   1: protected override void Update(GameTime gameTime)

   2: {

   3:     // Allows the game to exit

   4:     if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)

   5:         this.Exit();

   6:  

   7:     KeyboardState kbState = Keyboard.GetState();

   8:  

   9:     if (kbState.IsKeyDown(Keys.NumPad1))

  10:         this.EstablecerCultura("en");

  11:     else if (kbState.IsKeyDown(Keys.NumPad2))

  12:         this.EstablecerCultura("es");

  13:     else if (kbState.IsKeyDown(Keys.NumPad3))

  14:         this.EstablecerCultura("ca");

  15:  

  16:     this.titulo = String.Format(Textos.Titulo, Textos.Culture);

  17:     this.descripcion = Textos.Descripcion;

  18:     this.opciones = Textos.Opciones;

  19:  

  20:     base.Update(gameTime);

  21: }

Y finalmente lo dibujamos todo en pantalla… lo cual genera el resultado que podéis ver en el vídeo.

   1: protected override void Draw(GameTime gameTime)

   2: {

   3:     GraphicsDevice.Clear(Color.Black);

   4:  

   5:     spriteBatch.Begin();

   6:     spriteBatch.DrawString(font, titulo, new Vector2(10, 10), Color.Green);

   7:     spriteBatch.DrawString(font, descripcion, new Vector2(10, 40), Color.Green);

   8:     spriteBatch.DrawString(font, opciones, new Vector2(10, 70), Color.Green);

   9:     spriteBatch.Draw(foto, new Vector2(10, 120), Color.White);

  10:     spriteBatch.End();

  11:  

  12:     base.Draw(gameTime);

  13: }

 Quien quiera ir más allá, y necesite localizar sus juegos a idiomas que requieren gran cantidad de carácteres, con diccionarios distintos al nuestro (como japonés, coreano, chino, árabe…), echad un vistazo al ejemplo disponible en XNA Creators club respecto localización, donde además de las técnicas que os comento aquí hay un ejemplo de utilización del Content Pipeline para cargar sólo los carácteres que cada idioma requiere.

Como siempre podéis descargar el código fuente del ejemplo. Y ahora a jugar! (mmm esto me ha recordado al “precio justo” xD)

[XNA] Cámaras 3D y cámaras en 3a persona

¿Recordáis el juego Tomb Rider? Si habéis jugado, os acordaréis seguro de que como jugadores movíamos a Lara Croft, viéndola desde una prespectiva «trasera», es decir, que la cámara se movía siempre por detrás de ella. Esto es lo que se llama en desarrollo de videojuegos una cámara en 3a persona, o cámara persecutória (de «chase cam» en inglés, que sin duda queda mejor dicho así :-P). Dando por supuesto que habéis desarrollado una cámara para un juego 3D de XNA antes, añadir esta funcionalidad a un juego es sencillo, de todos modos vamos a repasar los conceptos de las cámaras 3D en XNA. 

Si un juego 2D es muy parecido a los clásicos dibujos animados, en que unos dibujos se superponían sobre otros, sobre una película transparente, para generar una composición final, un juego 3D es más bien parecido al rodaje de una película, mediante un objeto cámara.  La información de la cámara se almacena en las matrices de vista y proyección. La vista contiene información acerca de la posición de la cámara, la proyección define básicamete lo que la cámara puede ver. Más tarde el contenido de esta proyección es lo que la targeta gráfica transformará en una imagen 2D de la pantalla (sí. qué desengaño verdad? todavía no existen las pantallas 3D «reales», y están lejos de existir ahora mismo…). Ahh las matrices… cómo os iban en el instituto? las vísteis en la universidad? os gustaban, eh pillines? pues bueno, aquí lo bueno es que XNA nos facilita enormemente el trabajo con las matrices!!

Crear la vista

Para crear la matriz vista tenemos un método estático llamado Matrix.CreateLookAt, fácil verdad? Los parámetros que debemos pasarle son:

  1. Posición de la cámara, en un Vector 3D.
  2. Objetivo de la cámara (o dónde la cámara está «mirando»), en un Vector 3D.
  3. Vector que indica dónde está la posición «arriba» (sí, en un mundo 3D el concepto de arriba y abajo hay que definirlos, estilo barrio sésamo :-P), en un Vector 3D.

Esto en el código se traduce de la siguiente forma:

   1: private void IniciarVista() 

   2: {

   3:     posicionCamara = new Vector3(0.0f, 900.0f, 600.0f);

   4:     mirar = new Vector3(0.0f, 10.0f, 0.0f);

   5:     arriba = Vector3.Up;

   6:  

   7:     Vista = Matrix.CreateLookAt(posicionCamara, mirar, arriba);

   8: }

Donde como he comentado, posicionCamara, mirar y arriba son variables de la clase Camara de tipo Vector3, y Vista es una matriz.

Crear la proyección

Crear la proyección de una cámara no es mucho más difícil… existe otro método estático en Matrix que se llama Matrix.CreatePrespectiveFieldOfView. Este acepta los siguientes parámetros:

  1. Ángulo de la cámara en radianes, normalmente se pone π/4 (Pi/4).
  2. Proporción, o “aspect ratio” (otra vez el inglés es más claro que el castellano…).
  3. Plano cercano: distancia a partir de la cual la cámara puede “ver”.
  4. Plano lejano: distancia hasta la cual la cámara puede “ver).

Todos estos parámetros son de tipo float. La matriz proyección sirve a XNA para definir el frustum de la cámara, que es el espacio visible por la cámara. Esta imagen vale más que mil palabras…:

¿Porqué ponemos un plano cercano y uno lejano? ¿no sería mejor que se viera todo? Quizá sí… pero nos arriesgaríamos a tener serios problemas de rendimiento…

Esto en código se traduce en lo siguiente:

   1: private void IniciarProyeccion()

   2: {

   3:     float nearClip = 10.0f;

   4:     float farClip = 10000.0f;            

   5:     float aspectRatio;

   6:     

   7:     aspectRatio = (float)Configuracion.graficos.Viewport.Width / 

   8:         (float)Configuracion.graficos.Viewport.Height;

   9:     

  10:     Proyeccion = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, nearClip, farClip);

  11: }

En casi cualquier cámara 3D, el código de inicialización va a ser muy parecido a estos dos métodos que os he descrito, IniciarVista() e IniciarProyeccion(). Con eso ya podríamos renderizar una escena 3D como esta:

escena

Cámara en 3a persona

Vale, tenemos una fantástica cámara 3D… ¿ahora cómo hacemos para que vaya detrás de nuestro personaje del juego? Ya os lo debéis imaginar… simplemente hay que hacer que la posición de la cámara, el view, esté por detrás del jugador. Esto lo haremos con el método siguiente, que se llamará constantemente, a medida que vayamos moviendo al jugador:

   1: public Matrix UpDateCam(Matrix worldJugador)

   2: {

   3:     float distanciaCamara = 100.0f;

   4:  

   5:     posicionCamara = (worldJugador.Translation - (worldJugador.Backward * distanciaCamara)) 

   6:         + new Vector3(0.0f, 200.0f, 0.0f);

   7:  

   8:     Vista = Matrix.CreateLookAt(posicionCamara, worldJugador.Translation, arriba);

   9:  

  10:     return Vista;

  11: }

Básicamente lo que estamos haciendo es calculando la posición de la cámara a partir de la matriz que representa la posición del jugador en el mundo. Usamos el método Translation de la instancia de la clase que representa la posición del jugador, y otra propiedad muy útil de la matriz: Back, que representa el vector que queda detrás de la posición del jugador, a partir de su matriz mundo. A ello, le sumamos un nuevo Vector3, que nos sirve para subir la cámara, para obtener una mejor prespectiva del juego.

¡Y esto es todo amigos! Así es como quedaría la cámara aplicada al robot modelado por Jordi Giménez que vengo usando desde hace algunos tutoriales:

 

El código completo de la cámara

El código completo de la cámara sería este:

   1: using Microsoft.Xna.Framework;

   2: using Colisiones3D;

   3:  

   4: namespace PruebaColisiones3D

   5: {

   6:     public class Camara

   7:     {

   8:         public Matrix Vista;

   9:         public Matrix Proyeccion;

  10:  

  11:         Vector3 posicionCamara;

  12:         Vector3 mirar;

  13:         Vector3 arriba;

  14:  

  15:         public Camara()

  16:         {

  17:             this.IniciarVista();

  18:             this.IniciarProyeccion();

  19:         }

  20:  

  21:         private void IniciarVista() 

  22:         {

  23:             posicionCamara = new Vector3(0.0f, 900.0f, 600.0f);

  24:             mirar = new Vector3(0.0f, 10.0f, 0.0f);

  25:             arriba = Vector3.Up;

  26:  

  27:             Vista = Matrix.CreateLookAt(posicionCamara, mirar, arriba);

  28:         }

  29:     

  30:         private void IniciarProyeccion()

  31:         {

  32:             float nearClip = 10.0f;

  33:             float farClip = 10000.0f;            

  34:             float aspectRatio;

  35:             

  36:             aspectRatio = (float)Configuracion.graficos.Viewport.Width / 

  37:                 (float)Configuracion.graficos.Viewport.Height;

  38:             

  39:             Proyeccion = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, aspectRatio, nearClip, farClip);

  40:         }

  41:  

  42:         public Matrix UpDateCam(Matrix worldJugador)

  43:         {

  44:             float distanciaCamara = 100.0f;

  45:  

  46:             posicionCamara = (worldJugador.Translation - (worldJugador.Backward * distanciaCamara)) 

  47:                 + new Vector3(0.0f, 200.0f, 0.0f);

  48:  

  49:             Vista = Matrix.CreateLookAt(posicionCamara, worldJugador.Translation, arriba);

  50:  

  51:             return Vista;

  52:         }

  53:  

  54:     }

  55: }

[XNA] Detección de colisiones en un mundo 3D (Episode III)

Continuando con el ejemplo del episodio anterior, por fin aplicaremos detección de colisiones al nivel 3D que hemos generado para «el videojuego». Tenemos un XML en el que se definen los objetos BoundingBox y BoundingSphere que se usarán en el nivel para la detección de colisiones contra el mismo, tenemos también un mundo que se carga a partir de este XML… y tenemos además un robot que se mueve con animaciones propias por este mundo tridimensional. ¿Qué nos falta ahora? Es bien sencillo… detectar las colisiones entre el robot y el mundo! Es decir, hacer que el robot no pueda “traspasar las paredes” como lo hacía en el vídeo anterior. Lo que vamos a conseguir es lo siguiente:

Superados los pasos anteriores, añadir esta funcionalidad no va a ser demasiado difícil…  lo primero que haré es asegurarme de que se rendericen los boundingbox y boundingsphere cuando lo necesite. En un videojuebo “en producción” obviamente estos objetos no se renderizan, pero durante el desarrollo puede ser muy útil, con objeto de debugación. Y además, en nuestro caso tenemos que definir la posición de los boundings en un XML sin un editor de niveles visual… así que nos será muy útil poder ver donde queda cada bounding, así será más fácil editar dicho XML. Así pues el código sería el siguiente:

   1: public void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion) 

   2: {

   3:     // Pintar modelos

   4:     foreach (ObjetoInerte modelo in nivel.modelosMundo)

   5:     {

   6:         modelo.Draw(gameTime, vista, proyeccion);

   7:     }

   8:  

   9:     if (Configuracion.Debug)

  10:     {

  11:         // Pintar cajas

  12:         BoundingBoxRender renderCaja = new BoundingBoxRender();

  13:  

  14:         foreach (BoundingBox caja in nivel.boundingBoxes)

  15:         {

  16:             renderCaja.Render(caja, Configuracion.graficos, vista, proyeccion, Color.Orange);

  17:         }

  18:  

  19:         // Pintar esferas

  20:         BoundingSphereRender renderEsfera = new BoundingSphereRender();

  21:  

  22:         foreach (BoundingSphere esfera in nivel.boundingSpheres)

  23:         {

  24:             renderEsfera.Render(new BoundingSphere(esfera.Center, esfera.Radius), Configuracion.graficos, vista, proyeccion, Color.Orange);

  25:         }

  26:     }            

  27: }

 

¿Algo extraño? Supongo que habréis observado la existencia de una instancia de una clase que se llama BoundingBoxRender y otra BoundingSphereRender. Esta es una clase que permite renderizar BoundingBox y BoundingSpheres respectivamente (con ese nombre nádie lo diría eh? :-P). Estas son clases sencillas pero extermadamente útiles, que supongo que bajé algun día de Ziggyware (snif, snif, esa web “ha muerto…”). Pues bien, esta pequeña clase nos ayudará a debugar nuestros juegos.

Ahora que tenemos los boundings cargados y pintándose… hay que comprobar si el robot colisiona contra ellos. Para ello añadiré algunas líneas al método que ya existía en el ejemplo anterior: LeerInput.

   1: Vector3 posicionInicial;

   2:  

   3: posicionInicial = base.transformacionRaiz.ObtenerMundo().Translation;

Básicamente aquí guardo la posición del robot antes de aplicarle todas las transformaciones. El método Matrix.Translation permite obtener la posición de la misma en un vector 3D.

Aplicamos las transformaciones pertinentes a las matrices, tal y como hacíamos en el ejemplo anterior, todavía dentro de LeerInput, y comprobaremos si tras las mismas existe colisión entre el robot y el mundo. Si eso ocurre, devolvemos al robot a su posición inicial antes de leer el input de teclado. ¡El resultado es que el robot ya no puede atravesar las paredes!

   1: if (nivel.HayColision(this.esfera))

   2: {

   3:     base.transformacionRaiz.Traslacion.Translation = posicionInicial;

   4:     this.esfera.Center = Posicion + desplazamiento;

   5: }

El método “HayColision” no hace magia… simplemente recorre los boundings del nivel para ver si hay colisión contra el boundingsphere que tiene el propio robot.

   1: public bool HayColision(BoundingSphere bounding) 

   2: {

   3:     foreach (BoundingSphere boundingSphereCheck in boundingSpheres)

   4:     { 

   5:         if(boundingSphereCheck.Intersects(bounding))

   6:             return true;

   7:     }

   8:     foreach(BoundingBox boundingBoxCheck in boundingBoxes)

   9:     {

  10:         if (boundingBoxCheck.Intersects(bounding))

  11:             return true;

  12:     }

  13:  

  14:     return false;

  15: }

Importante: En un juego “real” tendríamos que limitar la comprobación de colisiones de alguna manera. Existen múltiples formas, y en uno de mis artículos hablaba de una de ellas, así que para no repetirme os dejo con el enlace.

Como siempre… os dejo aquí el código del ejemplo calentito para descargar.

[XNA] Detección de colisiones en un mundo 3D – (Episode II)

En el episodio anterior comencé a introducir el tema de la detección de colisiones en juegos 3D, aplicándo el concepto a un posible nivel de un videojuego. En este episodio seguiré desarrollado este «nivel» del videojuego, en concreto diseñaré una pantalla muy simple, pero lo más importante: intentaré simplificar el trabajo que supondría añadir más niveles a un juego y gestionar su información. Así pues esta parte del tutorial podría considerarse prácticamente más afín a las estructuras de datos que no a la detección de colisiones (tema al que volveremos en el episodio III).

Lo primero va a ser disponer de 1 o N modelos que conformen nuestro mundo. En este momento pondré un modelo que se compondrá de distintos objetos, no obstante, lo ideal es que existan N objetos formando nuestro «mundo», y que renderizemos sólo aquellos que son visibles por el jugador (los que están en el frustum de la cámara). Pero por ahora no nos compliquemos la vida… un mundo con un solo modelo. En mi caso lo he hecho en MAX, pero hay un montón de aplicaciones de modelado 3D gratuitas, posteriormente habrá que exportar el modelo a FBX.

Como podéis comprobar ni siquiera aplico texturas al modelo… en este caso se trata de una simple prueba de concepto que iremos perfeccionando.

 

 

Al final el resultado será este (otra vez estoy utilizando el robot modelado por Jordi Gimenez):

Así pues vayamos a ver qué aproximación he utilizado para implementar un gestor de niveles… he creado una clase ManagerNiveles, que gestiona y contiene un “almacén de datos” al que le he llamado “Nivel”.

Nivel podría contener cualquier nivel de nuestro videojuego, mientras la definición del mismo exista en el fichero XML correspondiente (nivelN.xml), y es que basándome en el artículo anterior, ManagerNiveles carga toda la información desde un fichero externo para hacer más fácil de gestionar N niveles en un videojuego. Este XML contiene información de los boundingbox y boundingspheres que se utilizarán en la siguiente parte del tutorial.

clases

Esta clase ManagerNiveles es muy interesante porque encapsula la información del nivel y el método necesario para pintarlo, así como la referencia a todos sus modelos y sus objetos bounding. Así pues, en la clase Game, tendremos un código muy simple, comenzando obviamente por una instancia a la clase:

   1: ManagerNiveles managerNivel;

Después sólo tendremos que hacer un new de la instancia y inicializar el nivel que queramos, en nuestro caso el 1:

   1: // Inicializo el nivel

   2: managerNivel.IniciarNivel(1);

Y para renderizar el nivel, el método Draw de la clase game hará simplemente lo siguiente:

   1: // Pintamos el nivel

   2: managerNivel.Draw(gameTime, camara.Vista, camara.Proyeccion);

Sencillo, verdad? Ahora veamos el método IniciarNivel de la clase ManagerNiveles.

   1: // Deserializamos los datos desde el XML y establecemos los mismos en la clase colisionesMundo

   2: nivel = new Nivel();

   3:  

   4: XmlSerializer ser = new XmlSerializer(typeof(Nivel));

   5:  

   6: TextReader lector = new StreamReader(

   7:     string.Format(@"{0}ContentNivelesNivel{1}.xml", Environment.CurrentDirectory, numeroNivel));

   8: nivel = (Nivel)ser.Deserialize(lector);

   9: lector.Close();

  10:  

  11: // Inicializamos los objetos del mundo

  12: foreach (DatosModelo objeto in nivel.objetosMundo)

  13: {

  14:     nivel.modelosMundo.Add(new ObjetoInerte(objeto.nombreModelo, content));

  15:     nivel.modelosMundo[nivel.modelosMundo.Count - 1].transformacionRaiz.Escala *= Matrix.CreateScale(objeto.Escala);

  16: }

Ese lo que hace es deserializar la información del XML en un objeto de tipo Nivel. Posteriormente carga en memoria todos los modelos que contiene el nivel.

El dibujado, al que hemos llamado antes desde la clase game, simplemente recorre los modelos y los pinta uno a uno (aquí es donde tendremos que utilizar más tarde un frustum culling para pintar sólo los modelos que se encuentran dentro del frustum de la cámara):

   1: public void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion) 

   2: {

   3:     foreach (ObjetoInerte modelo in nivel.modelosMundo)

   4:     {

   5:         modelo.Draw(gameTime, vista, proyeccion);

   6:     }

   7: }

 Y nada más por hoy! En el próximo episodio veremos como el robot tiene detección de colisiones, que le impide atravesar las paredes, como podéis observar que ahora oucrre en el vídeo.

[XNA] Detección de colisiones en un mundo 3D – (Episode I)

Con este inicio una serie de artículos telacionados con la detección de colisiones en videojuegos 3D. Esta servirá más tarde para mover objetos por este mundo 3D sin que se salgan de los límites, o se atraviesen los unos a los otros por ejemplo.

Así pues la detección de colisiones consiste básicamente en comprobar si nuestros objetos interseccionan en el espacio, pues bien, la detección de colisiones en un espacio tridimensional, se basa en XNA en los objetos BoundingBox y BoundingSphere, que no son más que cuerpos geométricos que no se renderizan pero que utilizaremos para realizar el cálculo de las colisiones. Tened en cuenta que si hiciéramos la comprobación de colisiones polígono a polígono necesitaríamos una CPU que todavía hoy no se ha fabricado para poder hacer los cálculos de forma rápida….

boundings

Como una imagen vale más que mil palabras, aquí tenemos un par de ejemplos de boundingboxes, como véis, no son más que prismas que envielven unos modelos 3D. En un juego obviamente estos prismas no se renderizan, pero puede ser útil hacerlo para debugar nuestro juego. Un boundingShpere sería exactamente lo mismo, pero obviamente tendríamos una esfera en lugar de un prisma.

Recordar aquí que siempre va a ser mucho más óptio utilizar BoundingSpheres que no BoundingBoxes. ¿Porqué? porque las esferas son fáciles de transformar… y es muy fácil de comprobar si interseccionan con otras esferas. Al fin y al cabo, matemáticamente estamos hablando de un centro en un vector 3D y un radio… así pues las operaciones con esferas son muy rápidas. Claro está que las cajas van a ser necesarias a veces… pero es recomendable usarlas solo para objetos del escenario, como es nuestro caso en estos momentos.

Tanto la clase BoundingBox como la BoundingSphere tienen como método más importante el “Intersects”. Como no quiero quitarle el pan al MSDN, la explicación detallada de estas clases y sus métodos podéis consultarla allí. Lo que si haré es destacar que el intersects nos permitirá comrpobar si el objeto bounding intersecciona con múltiples objetos muy interesantes:

  • otros boundingbox
  • otros boundingsphere
  • rayos
  • frustrums
  • planos

Básicamente aquí lo utilizaré para detectar colisiones con otros objetos de tipo bounding, pero tened presentes las posibilidades. En artículos anteriores he hablado tanto de frustrums como de rayos, podéis consultarlos si tenéis ganas 🙂

En este caso concretamente desarrollaré una clase que permitirá aplicar BoundingBoxes y BoundingSpheres de forma masiva sobre un “mundo 3D”. El hecho es que si tenemos un mundo, o nivel del juego, sobre el cual queremos hacer detección de colisiones, esto se traduce en que tenemos que crear un montón de BoundingBox y BoundingSpheres. ¿Cómo los creamos y mantenemos? ¿Cómo los instanciamos en el código? A ver, lo ideal, sería tener un editor visual de niveles, y hacerlo gráficamente, y finalmente serializar el “mundo” en un XML, para cargarlo después desde el juego, deserializando ese XML.

Pues bien… como no voy a currarme aquí y ahora un editor de niveles… espero que os conformaréis con la serialización y deserialización de / a XML. Esto es algo sencillísimo para cualquier programador de .Net, pero a ser posible hay que hacerlo con un poco de gracia. Lo que he planteado es lo siguiente:

diagrama_boundings

El caso es que tendremos una clase que he llamado DatosBounding que será deserializada desde un fichero XML, y con ella una lisa de objetos de tipo DatosBoundingBox y DatosBoundingSphere. Esta información posteriormente la podremos asignar a la clase que contenga la información del mundo o escenario 3D.

Para serializar la clase a un XML tendríamos el siguiente código:

   1: DatosBounding listaBoundings = new DatosBounding();

   2:  

   3: listaBoundings.listaBoundingSpheres.Add(new DatosBoundingSphere(new Vector3(1.0f,2.5f,3.2322f), 5.4f));

   4: listaBoundings.listaBoundingSpheres.Add(new DatosBoundingSphere(new Vector3(4.0f, 4.5f, 2.2322f), 22.4f));

   5: listaBoundings.listaBoundingBoxes.Add(new DatosBoundingBox(new Vector3(1,2,3), new Vector3(5,6,7))); 

   6:         

   7: XmlSerializer ser = new XmlSerializer(typeof(DatosBounding));

   8: TextWriter escritor = new StreamWriter(@"C:test.xml");

   9: ser.Serialize(escritor, listaBoundings);

  10:  

  11: escritor.Close();

Lo cual, una vez ejecutado, generará el siguiente fichero XML:

<?xml version=»1.0″ encoding=»utf-8″?>
<DatosBounding xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance» xmlns:xsd=»http://www.w3.org/2001/XMLSchema»>
  <listaBoundingSpheres>
    <DatosBoundingSphere>
      <Radio>0</Radio>
      <Posicion>
        <X>1</X>
        <Y>2.5</Y>
        <Z>3.2322</Z>
      </Posicion>
    </DatosBoundingSphere>
    <DatosBoundingSphere>
      <Radio>0</Radio>
      <Posicion>
        <X>4</X>
        <Y>4.5</Y>
        <Z>2.2322</Z>
      </Posicion>
    </DatosBoundingSphere>
  </listaBoundingSpheres>
  <listaBoundingBoxes>
    <DatosBoundingBox>
      <PosicionMin>
        <X>1</X>
        <Y>2</Y>
        <Z>3</Z>
      </PosicionMin>
      <PosicionMax>
        <X>5</X>
        <Y>6</Y>
        <Z>7</Z>
      </PosicionMax>
    </DatosBoundingBox>
  </listaBoundingBoxes>
</DatosBounding>

Posteriormente, desde nuestro juego, necesitaremos cargar esta información (supuestamente en un juego el XML sería muchísimo mayor, la cual cosa compensaría la serialización y deserialización). El proceso de carga de un XML se llama deserialización, y el código sería este:

   1: DatosBounding listaBoundings2 = new DatosBounding();

   2: 

   3: TextReader lector = new StreamReader(@"C:test.xml");

   4: listaBoundings2 = (DatosBounding)ser.Deserialize(lector);

   5: lector.Close();

Observad que leo/almaceno los datos en la raíz de la C:, chicos, no hagáis esto en casa 🙂 Lo importante aquí es que después de llamar al método Deserialize ya tendríamos una lista de Boundings (listaBoundings2) cargada con los datos del XML!

En el siguiente episodio… más! Hasta entonces, si queréis podéis apalearme por escribir tanto 😛

[XNA] Animando un robot en 3D

En este artículo continuaré hablando de las animaciones en 3D, basadas en transformaciones de matrices. En esta ocasión animaré un modelo más complejo (y también más guapo, creado por el artista 3D Jordi Gimenez Ruiz -gracias Jordi!!), concretamente un robot. El robot en sí no es todavía un T1000, sus movimientos se limitan a la cabeza, brazos y ruedas con las que se desplaza.

Estos tipos de animaciones son básicamente traslaciones y rotaciones de matrices en las que se basa la posición del modelo en el espacio tridimensional. Lo más importante en estos casos, para empezar, es recordar que estas operaciones son multiplicaciones de matrices, y que el órden en que apliquemos la multiplicación variará el resultado de la animación. Para ello, os paso el enlace a un artículo anterior donde introduzco el tema de las animaciones y explico cuál es el orden en que se deben aplicar siempre las multiplicaciones sobre las matrices, leyendo primero ese artículo se entenderá mejor este tutorial.

Siguiente paso… si queremos animar un Modelo, necesitaremos acceder a sus distintos Mesh, para ello, desde la herramienta de diseño 3D que utilicemos tendremos que poner nombre a los distintos objetos que queramos animar. Lo mejor será usar nombres que recordemos fácilmente, u hacernos un esquema, como este:

esquema 

Después, desde el código podremos aplicar transformaciones a cada uno de estos objetos del modelo de forma individual. Como aplicar las transformaciones a distintos mesh de un modelo puede llegar a complicar el código, hay que pensar bien como hacerlo. En este caso os ofrezco una implementación que creo mejorable, pero que es muy muy sencilla, y sobretodo reutilizable en otros modelos.

esquema

Si os fijáis tenemos una clase abstracta “ModeloExtendido”, que se encarga de pintar un modelo y aplicarle todas las transformaciones que le apliquemos mediante el objeto Transformaciones. El propio ModeloExtendido tendrá una instancia de estas transformaciones, que aplicarán al global del modelo (por ejemplo, si queremos hacer que el robot avance, tendremos que transformarlo “todo”, no las ruedas, la cabeza o cualquier otra parte de él, pero obviamente queremos que la cabeza y demás se muevan con el resto del cuerpo… Veamos en detalle la clase Transformaciones, que como vemos es prácticamente un contenedor de datos:

   1: public class Transformaciones

   2: {

   3:     public Matrix Rotacion { get; set; }

   4:     public Matrix Traslacion { get; set; }

   5:     public Matrix Orbitacion { get; set; }

   6:     public Matrix Escala { get; set; }

   7:  

   8:     public string NombreMesh { get; set; }

   9:  

  10:     /// <summary>

  11:     /// Inicializa las matrices

  12:     /// </summary>

  13:     public Transformaciones(string Mesh) 

  14:     {

  15:         this.NombreMesh = Mesh;

  16:         Rotacion = Matrix.Identity;

  17:         Traslacion = Matrix.Identity;

  18:         Escala = Matrix.Identity;

  19:         Orbitacion = Matrix.Identity;

  20:     }

  21:  

  22:     /// <summary>

  23:     /// Genera la matriz mundo

  24:     /// </summary>

  25:     /// <returns></returns>

  26:     public Matrix ObtenerMundo() 

  27:     {

  28:         return Escala * Rotacion * Orbitacion * Traslacion;

  29:     }

  30: }

Así pues las clases que hereden de ModeloExtendido podrán contener una colección de N transformaciones, en este caso, el objeto RobotPatines tiene las siguientes instancias, que representan cada uno de los mesh que hemos visto en el esquema anterior:

   1: private Transformaciones patines_derecha_delante = new Transformaciones("patins_dreta_davant");

   2: private Transformaciones patines_derecha_detras = new Transformaciones("patins_dreta_darrera");

   3: private Transformaciones patines_izquierda_delante = new Transformaciones("patins_esquerra_davant");

   4: private Transformaciones patines_izquierda_detras = new Transformaciones("patins_esquerra_darrera");

   5: private Transformaciones cabeza = new Transformaciones("cap");

   6: private Transformaciones brazo_derecho = new Transformaciones("brac_dret");

   7: private Transformaciones brazo_izquierdo = new Transformaciones("brac_esquerra");

En el método Update() de RobotPatines hay que leer del teclado y calcular las transformaciones matriciales necesarias. En XNA esto es sencillo! Veamos:

   1: private void LeerInput() 

   2: {

   3:     KeyboardState kbState = Keyboard.GetState();

   4:  

   5:     if (kbState.IsKeyDown(Keys.Up))

   6:     {

   7:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

   8:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

   9:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  10:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  11:  

  12:         base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Backward * VELOCIDAD_DESPLAZAMIENTO);

  13:     }

  14:     if (kbState.IsKeyDown(Keys.Down))

  15:     {

  16:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  17:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  18:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  19:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  20:  

  21:         base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Forward * VELOCIDAD_DESPLAZAMIENTO);

  22:     }

  23:     if (kbState.IsKeyDown(Keys.Left))

  24:     {

  25:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  26:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  27:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  28:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  29:  

  30:         base.transformacionRaiz.Rotacion *= Matrix.CreateRotationY(VELOCIDAD_GIRO);

  31:     }

  32:     if (kbState.IsKeyDown(Keys.Right))

  33:     {

  34:         this.patines_derecha_delante.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  35:         this.patines_izquierda_delante.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  36:         this.patines_derecha_detras.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_RUEDAS);

  37:         this.patines_izquierda_detras.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_RUEDAS);

  38:  

  39:         base.transformacionRaiz.Rotacion *= Matrix.CreateRotationY(-VELOCIDAD_GIRO);

  40:     }

  41:     if (kbState.IsKeyDown(Keys.O))

  42:     {

  43:         this.cabeza.Rotacion *= Matrix.CreateRotationY(-VELOCIDAD_CABEZA);

  44:     }

  45:     if (kbState.IsKeyDown(Keys.P))

  46:     {

  47:         this.cabeza.Rotacion *= Matrix.CreateRotationY(VELOCIDAD_CABEZA);

  48:     }

  49:     if (kbState.IsKeyDown(Keys.U))

  50:     {

  51:         this.brazo_derecho.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_CABEZA);

  52:     }

  53:     if (kbState.IsKeyDown(Keys.I))

  54:     {

  55:         this.brazo_derecho.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_CABEZA);

  56:     }

  57:     if (kbState.IsKeyDown(Keys.J))

  58:     {

  59:         this.brazo_izquierdo.Rotacion *= Matrix.CreateRotationX(VELOCIDAD_CABEZA);

  60:     }

  61:     if (kbState.IsKeyDown(Keys.K))

  62:     {

  63:         this.brazo_izquierdo.Rotacion *= Matrix.CreateRotationX(-VELOCIDAD_CABEZA);

  64:     }

  65: }

Fácil verdad? La única operación que tiene un poco más de “chicha” es la traslación de la instancia de transformaciones transformacionRaiz, que afecta al modelo entero, en concreto a la transformación de Traslación. Esto lo que hará será mover el robot hacia el frente teniendo en cuenta su giro:

   1: base.transformacionRaiz.Traslacion *= Matrix.CreateTranslation(base.transformacionRaiz.Rotacion.Forward * VELOCIDAD_DESPLAZAMIENTO);

Aquí tenemos que dar gracias a los programadores del framework de XNA porque realmente nos están facilitando mucho la vida con el tema de las matrices…

Vale, ahora viene lo más “complicado”, aplicar todas estas transformaciones al modelo… pues no es tan difícil no creáis. He creado una sobrecarga al Draw de la clase base, al cual le envío todas las transformaciones de esta forma (esto es todavía RobotPatines):

   1: public override void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion)

   2: {

   3:     List<Transformaciones> transformaciones = new List<Transformaciones>();

   4:  

   5:     transformaciones.Add(patines_derecha_delante);

   6:     transformaciones.Add(patines_derecha_detras);

   7:     transformaciones.Add(patines_izquierda_delante);

   8:     transformaciones.Add(patines_derecha_delante);

   9:     transformaciones.Add(patines_izquierda_detras);

  10:     transformaciones.Add(cabeza);

  11:     transformaciones.Add(brazo_derecho);

  12:     transformaciones.Add(brazo_izquierdo);

  13:  

  14:     base.Draw(gameTime, vista, proyeccion, transformaciones);

  15: }

Y así es como se dibujaría el modelo en la clase base, en ModeloExtendido:

   1: public virtual void Draw(GameTime gameTime, Matrix vista, Matrix proyeccion, List<Transformaciones> transformacionesHijas) 

   2: {

   3:     modelo.CopyAbsoluteBoneTransformsTo(transformaciones);

   4:  

   5:     foreach(ModelMesh mesh in modelo.Meshes)

   6:     {

   7:         foreach (BasicEffect efecto in mesh.Effects) 

   8:         {

   9:             efecto.EnableDefaultLighting();

  10:  

  11:             if (transformacionesHijas != null && transformacionesHijas.Find(n => n.NombreMesh != string.Empty && n.NombreMesh == mesh.Name) != null)

  12:             {

  13:                 efecto.World = transformacionesHijas.Find(n => n.NombreMesh == mesh.Name).ObtenerMundo() * transformaciones[mesh.ParentBone.Index] * transformacionRaiz.ObtenerMundo();

  14:             }

  15:             else

  16:                 efecto.World = transformaciones[mesh.ParentBone.Index] * transformacionRaiz.ObtenerMundo();

  17:  

  18:             efecto.View = vista;

  19:             efecto.Projection = proyeccion;

  20:         }

  21:  

  22:         mesh.Draw();

  23:     }

  24: }


Observad que utilizo una expresión lambda para aplicar las transformaciones enviadas por parámetro a los mesh que correspondan.

Este es el resultado:

Os dejo el código por si queréis juguetear con él. Enjoy!