domingo, 26 de julio de 2009

COMO ARREGLAR PROBLEMAS LOCALES DE CONEXION DE TU XBOX 360 A LOS SERVIDORES DE XBOX LIVE

Unas semanas atrás, tuve que resetear mi router porque Windows Vista me estaba dando problemas con las conecciones cableadas LAN & WAN de mi PC de escritorio (nota: problema resuelto al pasar a Windows 7).

La cosa es que cuando reconfiguré mi router utilizando el archivo con la configuración grabada por última vez, mi XBox estaba teniendo problemas al intenar conectarse a los canales de XBox Live’s (tanto arcade como indie). De hecho, de vez en cuando solía recibir mensajes que decían que mi conexión se había perdido debido a errores como el 80072741 y demás (como luego verán en unos instantes, simplemente olvidé persistir a disco los valores de configuración adecuados para mi router. Uff!).

Cuando eso sucede, y suponiendo que el Equipo de XBox Live no está trabajando en los servidores (pruebas, actualizaciones, etc.), entonces algo errado debe estar sucediendo de lado suyo (como en mi caso).

Así que, qué podría estar mal?

  1. Tu router está roto,
  2. Tu router tiene un firmware defectuoso o corrupto,
  3. Estás conectando tu XBox 360 con un cable defectuoso,
  4. Tu 360 no puede obtener un dirección IP local del router,
  5. El cortafuegos (firewall) de tu router no permite que tu 360 se conecte a Internet,
  6. Tu router está realizando tareas de "Network Address Translation" (NAT) estrictas o moderadas,
  7. Obtienes algúnos mensajes extraños de error y/o pérdida de conexión al jugar algúnos videojuegos en línea en modo multijugador, y
  8. Otros problemas de conexión.

Si uno, unos pocos o algúnos de estos les pasó, entonces tal vez los siguientes consejos podrían ayudarles a resolver problemas de conexión. Lo cual significa? Que no hay garantía de solución.

POR LO QUE USEN ESTOS CONSEJOS BAJO SU PROPIO RIESGO!

Ahora que están avisados, continúen leyendo con atención …

Tu router está roto.

Compren uno nuevo en caso que no sea fácil o valga la pena repararlo. mientras tanto pueden intentar conectarse directamente a través del model (DSL).

Tu router tiene un firmware defectuoso o corrupto.

Vayan a la página de soporte del fabricante, descarguen el firmware más reciente para el modelo de router, y actualícenlo (primero, lean el manual del router para saber cómo proceder con la actualización).

Estás conectando tu XBox 360 con un cable defectuoso.

Sólo cambialo e intenta nuevamente.

Pero que si estoy utilizando una conexión inalámbrica? Entonces comprueben la configuración inalámbrica de su router, como, digamos el método de seguridad y la contraseña.

Tu XBox 360 no puede obtener un dirección IP local del router.

Primero verifiquen la configuración de red en la XBox 360: si desean obtenr una dirección IP estática o dinámica, los valores de dirección primario y secundario del “Sistema de Nombres de Dominio” (DNS), las direcciones IP de la puerta de enlace (gateway), etc.

Ahora bien, miren en el router el número máximo de conexiones permitidas al mismo tiempo. Talvéz las estén utilizando todas.

Si el router provée un servidio de “Protocolo de Configuración Dinámica de Alojamiento" (DHCP), entonces cualquier computer, consola y/o dispositivo LAN/WAN puede ser configurado para intentar obtener una dirección IP del router, dinámicamente.

Ello debería funcionar bien con sus consolas XBox 360 para los routers soportados, pero en caso que no sea así, sólo configuren la consola para obtener una dirección IP estática.

Cuál? Bueno, dicho de manera simple, una dirección IP que Uds. sepan que sus sistmeas normalmente no van a usar (por ejemplo, si tienen dos computadores y una consola y están permitiendo, digamos 8 conexiones, entonces establezcan la última como la dirección IP estática de la consola y probablemente les vaya bien).

A efectos de establecer una dirección IP estática, en la pizarra de la 360 naveguen hacia “System –> Network Settings –> Edit Settings”, y luego ingresen:

  • La dirección IP estática para la consola,
  • La máscara de subred (la misma que encontrarán en el router),
  • La dirección IP de la puerta de enlace (Gateway),
  • Las direcciones de los DNS primario y secundario.

Prueben la conexión de nuevo, y si todo sale bien, su 360 ahora debería tener acceso a la red local.

El cortafuegos (firewall) de tu router no permite que tu 360 se conecte a Internet.

Tener acceso a la red local no significa que el router les conceda acceso a Internet. A veces, el cortafuegos (firewall) del router frena cualquier intento de un dispositivo y/o de una serie de direcciones IP de alcanzar la Internet.

Si ese es el caso, entonces verifiquen las reglas de seguridad establecidas en el router. La mayoría de los routers les permite especificar cada IP para la cual quieran controlar su acceso a Internet, un rango de direcciones IP e incluso las direcciones de “Control de Acceso a Medios” (MAC) de sus dispoditivos.

Puesto que en caso, en mi caso, el número de dispositivos que se conectan a Internet es bajo, sólo especifico cada dirección MAC y listo!

también puede suceder que el acceso a Internet esté únicamente disponible durante ciertos días y horas por día. Por lo que también deberían ver esas reglas.

Tu router está realizando tareas de "Network Address Translation" (NAT) estrictas o moderadas.

Este es uno de los problemas más populares a cuando una console XBox 360 intenta establecer una conexión segura con los servidores de XBox Live.

En resúmen, no todos los puertos y protocolos necesarios para establecer una comunicación óptima están (debidamente) configurados.

Ok … ehh … qué?

Hagan lo siguiente: prueben sus conexiones de la 360 a la red local, a la Internet y finalmente a los servidores de Live, y si obtiene como resultado que dos de tres funcionan bien, siendo la última prueba la que “parcialmente” falla, entonces la función NAT de su router no es “Abierta”.

De hecho, si esa es la situación, pueden conectarse a Xbox Live pero dicha conexión no es óptima para casos donde quieran jugar, chatear, hablar e incluso aceptar invitaciones de amigos, en línea.

Para configurar los puertos y protocolos requeridos a fin de establecer una conexión "sana" a los servicios de XBox Live, deben o bien:

  • Ubicar su consola dentro de la "zona desmilitarizada” (DMZ), o
  • Configurar manualmente los valores específicos utilizando “Reenvío de puertos”.

Nota: para realizar cualquiera de las dos, deben primero establecer una dirección IP estática en la consola.

DMZ significa, en breve, que abren todos los puertos y protocolos en pro de comunicarse con un determinado dispositivo con una dirección IP específica. Así que dicho dispositivo se sitúa dentro de una zona desprotegida o si lo prefieren una área no restringida. DMZ es demasiado riesgoso!

La alternativa (la que prefiero): es configurar manualmente sólo el par de puertos & protocolos que verdaderamente se necesitan para la conexión para un determminado dispositivo con una dirección IP estática (para el caso que nos ocupa, la dirección IP de la 360).

Todo lo que tienen que hacer en la última opción, es seleccionar la lengüeta “Port Forwarding” en el router y configurar algo similar a:

  • Nombre de la aplicación (digamos, “XBox360Live”),
  • Cada rango de puertos (“de 80 a 80”, y así sucesivamente),
  • Los protocolos permitidos (UDP/TCP/Ambos),
  • La dirección IP local (esto es, el valor de la dirección IP estática de tu consola 360), y
  • Marcar “Habilitar” (o cualquier otra opción necesaria para activar dicha regla).

Lean este artículo para saber cuáles deben configurar.

Como mencioné al comienzo de este artículo, el problema antedicho fue el que no permitía que mi consola 360 se conectara a Xbox Live, apropiadamente. Para resumir, el archivo de configuración que había salvado tiempo atrás como respaldo no incluía estos valores. Solucionado!

Ahora bien, volviendo al tema …

Opcional: algúnos recomiendan (yo no) que cuando reciban un código de error como el 8007274c, deseleccionar una opción similar a “bloquear peticiones anónimas desde Internet” en el cortafuegos del router puede ayudar. Y agregan, en algúnos casos, limipiar la memoria chache de la consola (atención: hacerlo borrará todas las actualziaciones de software! Deberán descargarlas nuevamente) y/o verificar que los valores de DNS debidos estén configurados.

Por cierto, el reenvío de puertos sólo funciona para una aplicación por vez, lo cual significa que si dos aplicaciones en la red local intentan obtener acceso, digamos, a Internet usando el mismo puerto, un conficto ocurre y si el router no lo puede resolver, la conectividad sufre … talvez sean sus consolas 360 uno de los dispositivos en conflicto!

Si el cortafuegos de sus router tiene una vitácora (log), léanla para ver cual dispositivo y aplicación es la fuente del conflicto. también pueden intentar ver el log de los respecivos cortafuegos de los dispositivos, si tienen.

Si no pueden identificar qué es lo que está causando el conflicto en el puerto, supongo que desconectar todos los dispositivos, a excepción de la XBox 360, debería solucionar el problema de conexión, de manera que puedan jugar en línea nuevamente a algúnos videojuegos.

Obtienes algúnos mensajes extraños de error y/o pérdida de conexión al jugar algúnos videojuegos en línea en modo multijugador.

Si tienen una membresía Live Dorada vigente, entonces esto está emparentado con el tema de los puertos de conexión.

Algúnos juegos precisan que unos pocos puertos estén abiertos para ciertos protocolos (TCP/UDP/Both), los cuales difieren de los listados aquí.

De nuevo, si no desean establecer (la dirección IP de) sus consolas dentro del DMZ, entonces deberían configurar manualmente ambos, puertos más protocolos, para el juego.

Ok, cómo puedo saber qué par debería configurar? Bien, o bien pueden hacer una b´squeda en Internet para averiguarlo o visitar sitios como http://www.portforward.com/, los cuales tienen mucha información al respecto, para una lista de routers y servicios (incluyendo conexiones a la 360).

Otros problemas de conexión … más sus soluciones?

A modo de ejemplo, sus routers deben soportar una MTU mínima (“Unidad Máxima de Transmisión"”); la cual en el caso de XBox Live es: 1364. O el modem DSL de sus proveedores de Internet no es lo suficientemente bueno (soliciten su cambio).

Sería genial saber tanto el problema que enfrentaron al conectar su 360 a XBox Live como, por supuesto, la solución.

Bien, eso es todo. Espero que la información les sea de utilidad.

Dsifruten sus juegos!
~Pete

> Vínculo a la versión en inglés.

viernes, 24 de julio de 2009

MODERNIZACION DEL CLUB EN LINEA DE CREADORES DE XNA

Guau! Cuando desperté esta mañana y encendí mi computador de escritorio, encontré una muy grata novedad en el sitio de creadores: fue modernizado!

La página principal ha tenido algúnos cambios de aspecto: las noticias, la publicidad y la info en general han sido reorganizadas todas de forma más atractiva. Simplemente compruébenlo.

Además, hay nuevos paneles como “Top Movers” … esperen! Qué es un “top mover? De ahora en más, todo lo que hagan en XNA CCO les dará reputación, tanto en forma positiva como negativa:

  • Buena contribución: clink, caja! Su reputación crece. Felicitaciones!
  • Mala contribución: bam! Compórtense …

Por favor lean este artículo para obtener datos detallados sobre cómo funciona el sistema de reputación.

Pero esperen! Hay más:

  • Nuevos Países: Japón y Alemania (también actualizaciones para Singapur y Suecia),
  • Fichas: son miembros premium de XNA CCO, crean y envían sus juegos, reciben 50 fichas por título! Sean listos: úsenlas a los efectos de “incrementar el grado de conocimiento de sus videojuegos”,
  • Actualizaciones de Juegos: cuando envíen y publiquen una nueva versión de sus juegos para la XBox 360, una nota aparecerá frente a los clientes existentes preguntando si desean descargar la versión más reciente,
  • Envío de Juegos “3.1”: ahora pueden enviar juegos creados con XNA GS 3.1, pero los verán en la tienda de la 360 (luego de pasar por la revisión de pares) a partir de August 11, 2009,
  • Nueva Estructura de Precios: 80, 240 y 400 puntos! Lean atentamente como esta nueva estructura va a afectar a los videojuegos ya publicados en la tienda de la 360, y
  • XBox Live Indie Games: sí, finalmente! El nuevo nombre para nuestros videojuegos está comenzando a mostrarse en casi todos lados. El cambio de nombre se completará más adelante en el verano norteamericano.

Buena, gente!
~Pete

> Vínculo a la versión en inglés.

martes, 21 de julio de 2009

EJEMPLO DE ANIMACIONES PERSONALIZADAS DE AVATARS, ESQUELETO & MAS ANIMACIONES

Tres nuevos recursos se encentran disponibles para miembros premium en el Club de Creadores de XNA:

Hurra!
~Pete

> Vínculo a la versión en inglés.

viernes, 17 de julio de 2009

ADELANTO DE LA ACTUALIZACION DE LA PIZARRA DE XBOX LIVE

Muchos sitios web están reportando detalles sobre la “Actualización de Otroño” que se viene para la pizarra de la XBox 360.

Realmente, en la actualización se están agregando caracterísiticas notables, entre otros:

  • Calificaciones de Usuarios,
  • Juegos a Demanda, y
  • XBox Live “Indie” Games!!!

Pueden leer este artículo por una lista completa de las nuevas características.

No puedo esperar!
~Pete

> Vínculo a la versión en inglés.

domingo, 5 de julio de 2009

“XNAVATARES” - PARTE 2 – ¿SIN SOMBRAS?

Como prometí, aquí presento la segunda parte de esta serie de artículos sobre el uso de Avatares con XNA GS para la XBox 360.

Si recuerdan, en mi primer artículo mostré como dibujar en pantalla un avatar animado teniendo en cuenta transiciones entre dos animaciones.

En esta parte, hablaré de un factor que ayudará a mejorar un poco el “dulce visual” en sus juegos cuando utilicen avatares: sombras.

Como deben saber, el sistema de renderizado de avatares no nos permite usar efectos de shaders personalizados para dibujar avatares en panatalla; en cambio, debemos únicamente usar el sistema incluido por defecto para lograr esta tarea.

Por un lado, simplifica las cosas pero por otro, limita un poco nuestras posibilidades.

La emisión de sombras es un ejemplo de dicha limitante. Como voy a mostrar en un minuto o dos, una solución alternativa “barata” puede ser utilizada, para juegos simples.

La técnica que usaré se conoce como “Sombras Planas” (el framework de XNA incluye todo lo que precisamos para esto). Es un sustituo muy básico de sombras “reales”, pero va a lograr el truco con lo justo para proyectos que no requieran effectos de sombreado “puntillozos”.

Utilizaremos el proyecto que había incluído la vez anterior como punto de partida y enfocándonos principalmente en el código que se agregará y/o modificará.

1. Campos a agregar:

private Matrix[] transitionTransforms, shadowTransforms;
...
private Plane plane;
private float lightRotation;
private Model floor;

Qué hay de nuevo? El arreglo de ‘transformación de sombras’ guardará las matrices que vamos a necesitar para achatar el modelo en base a un plano de referencia, el cual también definimos.

2. El constructor:

...
// Create the array of matrices that will hold bone transforms for shadows.
this.shadowTransforms = new Matrix[ 71 ];
...

No hay nada extravagante aquí. Sólo creamos el arreglo que almacenará la colección de matrices con las que se achatará el modelo.

3. Inicializando el juego:

/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content.  Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
    this.plane = new Plane( Vector3.Up, 0 );
 
    // As usual, initialize all compoenents.
    base.Initialize();
}

Unicamente creamos el plano de referencia con una normal mirando hacia arriba y sin movernos sobre dicha normal (por lo que es un plano XZ donde la coordenada Y es cero, inicialmente).

4. Cargando contenido:

...
// Set the "World" value wit a rotarion of 180º.
this.avatarRenderer.World = Matrix.CreateTranslation(
    Vector3.Right * -1 + Vector3.Up * 0 + Vector3.Forward * -1 ) *
    Matrix.CreateRotationY( MathHelper.Pi );
...

Sólo modificamos la línea que ubica al avatar en el mundo.

5. Actualizando el mundo:

Sólo agregaremos esta línea:

...
// Update the value used to rotate the light.
this.lightRotation += .5f * (float)gameTime.ElapsedGameTime.TotalSeconds;
...

Así que la luz rotará para mostrar el efecto.

6. Dibujando el avatar:

/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw( GameTime gameTime )
{
    // As usual, clear the backbuffer (or the current render target).
    GraphicsDevice.Clear( Color.CornflowerBlue );
 
    // Create the array of bone transforms for the floor and populate it.
    ModelBone[] transforms = new ModelBone[ this.floor.Bones.Count ];
    this.floor.Bones.CopyTo( transforms, 0 );
 
    // For each mesh in the floor model.
    foreach(var mesh in this.floor.Meshes)
    {
        // Get the basic effect.
        foreach ( BasicEffect effect in mesh.Effects )
        {
            // Set values and commit changes.
            effect.DiffuseColor = Color.LightSteelBlue.ToVector3();
            effect.View = this.avatarRenderer.View;
            effect.Projection = this.avatarRenderer.Projection;
            effect.World = transforms[ mesh.ParentBone.Index ].Transform;
            effect.CommitChanges();
        }
 
        // Finally, draw the mesh.
        mesh.Draw();
    }
 
    // Can we draw the avatar?
    if ( avatarRenderer != null && currentAnimation != null )
    {
        // If we can, is the animation in transition?
        if ( this.isInTransition )
        {
            // If so, draw it with the interpolated transforms.
            this.avatarRenderer.Draw(
                this.transitionTransforms,
                currentAnimation.Expression );
        }
        else
        {
            // If not, draw it with the actual transforms.
            this.avatarRenderer.Draw(
                this.currentAnimation.BoneTransforms,
                currentAnimation.Expression );
        }
 
        // Make the light sources of the avatar dark.
        Vector3 ambientColor = this.avatarRenderer.AmbientLightColor;
        Vector3 lightColor = this.avatarRenderer.LightColor;
        this.avatarRenderer.AmbientLightColor =
            this.avatarRenderer.LightColor =
                -10 * Vector3.One;
 
        // Enable alpha blending.
        GraphicsDevice.RenderState.AlphaBlendEnable = true;
        GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
        GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
 
        // Change the depth bias just a bit to avoid z-fighting.
        float sourceDepthBias = GraphicsDevice.RenderState.DepthBias;
        GraphicsDevice.RenderState.DepthBias = -0.0001f;
 
        // Set the new light direction.
        this.avatarRenderer.LightDirection = Vector3.Normalize(
            Vector3.Right * 7.5f * (float)Math.Cos( lightRotation ) +
            Vector3.Forward * 15.0f * (float)Math.Sin( lightRotation ) +
            Vector3.Up * 10.0f );
 
        // If the avatar is stepping over the floor, then move the plane 
        // according to the "altitude" of the avatar in the world so as
        // to calculate and cast shadows in the correct world position
        // (also, take into account that in case of a "jump" movement, in a 
        // "complete" shadow system you must reposition the shadow along the 
        // floor taking into account the place where the light-ray hits the 
        // floor while it points to the avatar; otherwise, it will stand still 
        // as if the avatar never jumped in the first place).
        this.plane.D = -this.avatarRenderer.World.Translation.Y;
 
        // Calculate and set the world transform that will flatten the 
        // avatar's geometry, taking into account the original rotation,
        // scale and translation factors.
        Matrix world = this.avatarRenderer.World;
        this.avatarRenderer.World *= Matrix.CreateShadow(
               this.avatarRenderer.LightDirection,
               this.plane );
 
        // Is the animation in transition?
        if ( this.isInTransition )
        {
            // If so, draw it with the interpolated transforms.
            this.avatarRenderer.Draw(
                this.transitionTransforms,
                currentAnimation.Expression );
        }
        else
        {
            // If not, draw it with the actual transforms.
            this.avatarRenderer.Draw(
                this.currentAnimation.BoneTransforms,
                currentAnimation.Expression );
        }
 
        // Reset all affected values.
        this.avatarRenderer.World = world;
        this.avatarRenderer.AmbientLightColor = ambientColor;
        this.avatarRenderer.LightColor = lightColor;
        GraphicsDevice.RenderState.DepthBias = sourceDepthBias;
        GraphicsDevice.RenderState.AlphaBlendEnable = false;
    }
 
    // The following is used to show some statistics and other info
    // on screen. It can be omitted (or optimized).
    this.spriteBatch.Begin();
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'A' to force changing animations or 'Back' to exit.",
        new Vector2( 50, 25 ),
        Color.White );
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'B' to change the type of selection : " +
        ( this.moveRandomly ? "RANDOMLY" : "IN ASCENDING ORDER" )
        + ".",
        new Vector2( 50, 55 ),
        Color.White );
 
    // Draw the animation pointer, whether we are processing a transition and
    // the current transition time. Please notice that in this implementation
    // when the current animation is about to end (that is, 1 second or less),
    // the pointer "currentAnimationId" will change even if the animation is still
    // the same, so you will see a different number and name during 1 second or so.
    this.spriteBatch.DrawString(
        this.font,
        this.currentAnimationId + " : " +
            ( (AvatarAnimationPreset)this.currentAnimationId ).ToString() +
            " (" +
            ( !this.isInTransition ? "no transition" : this.transitionProgress.ToString() + " processed" ) +
            ").",
        new Vector2( 50, 85 ),
        Color.White );
 
    // Draw the current position and length of the animation being rendered.
    if ( currentAnimation != null )
    {
        this.spriteBatch.DrawString(
            this.font,
            "Processed " +
            this.currentAnimation.CurrentPosition.ToString() +
                " of " +
                this.currentAnimation.Length.ToString() +
                ".",
            new Vector2( 50, 115 ),
            Color.White );
    }
 
    // Flush the batch.
    this.spriteBatch.End();
 
    // As usual, call the base method.
    base.Draw( gameTime );
}

Aquí es donde ocurren la mayoría de los cambios; el juego ...:

  1. ... dibuja el avatar tal cual lo hiciera en mi ejemplo previo,
  2. ... cambia los valores de iluminación que afectan al avatar.
  3. ... modifica el ajuste de profundidad a efectos de evitar cualquier disputa de profundidad eventual (z-fighting),
  4. ... rota la luz y ajusta la altitud del plano de referencia antes de achatar el modelo,
  5. ... actualiza la posición del modelo aplanado usando el método estático del estructurado del tipo matríz, al cual llamamos “CreateShadow”,
  6. ... dibuja la sombra falsa, y finalmente ...
  7. ... restaura los valores de las luces y también de la posición de las mayas del modelo del avatar.

7: Cambios al código de transición: ninguno.

Si todo sale bien verán algo como esto:

Cosas a tener en cuenta, no obstante:

  1. Las sombras se dibujarán aun cuando no hayan elementos donde plasmar sombras (vean como parte de la sombra  se dibuja más allá del “suelo” por un breve lapso de tiempo),
  2. Tendrán que modificar la ubicación de las sombras cuando se cambia la altura en la posición del avatar (i.e.: si salta),
  3. Las mallas del modelo se achatan en un plano de referencia, por lo que únicamente funcionará para objetos situados en dicho plano específico (como, en mi ejemplo, un piso), y
  4. Por ende, no hay auto-sombreado.

Un enfoque más preciso sería extender el ejemplo tratando de utilizar la “memoria cliché” (stencil buffer) y datos de profundidad, como se explica al final de esta discusión.

Bien, eso es todo por hoy. Aquí pueden encontrar el código fuente de este ejemplo.

Salúd!
~Pete

> Vínculo a la versión en inglés.

jueves, 2 de julio de 2009

“XNAVATARES” - PARTE 1

Al final de mi artículo "Avatars 101" mencioné que publicaría un ejemplo con un enfoque un tanto diferente a fin de dibujar las transiciones de los avatares.

Bueno, he decidido extender el ejemplo y partir el resultado final en una serie de, posiblemente, 3 o 4 artículos.

Para empezar, en este artículo nos centraremos en el código para las transiciones que prometí.

Para simplificar un poco la explicación, todo el código para dibujar los avatares se encuentra implementado en la clase Game (pero esto va a cambiar en la tercera parte de la serie).

Ok, comencemos ...

1. Creen un proyecto de juego de XNA GS en Visual Studio y llámenlo, digamos, "AvatarGame".

2. Incluyan los siguientes campos (en adición a los que son automáticamente generados para Uds. por XNA GS):

// We will use a font to draw statistics.
private SpriteFont font;
 
// These are needed to handle our avatar.
private AvatarDescription avatarDesc;
private AvatarRenderer avatarRenderer;
private AvatarAnimation currentAnimation, targetAnimation;
 
// Holds an array of all animations available for our avatar.
private AvatarAnimation[] animations;
 
// These are needed to handle transitions between animations.
private bool isInTransition, moveToNextAnimation, moveRandomly;
private float transitionProgress, transitionStep;
private Matrix[] transitionTransforms;
private int currentAnimationId;
private Random randomizer;
 
// These will help to detected pressed buttons.
private GamePadState currentGamePadState, lastGamePadState;

El código se explica solo. Básicamente, estamos agregando los campos básicos que necesitamos para dibujar un avatar más los de ayuda para manejar las transiciones.

3. El constructor:

/// <summary>
/// Initializes a new instance of the <see cref="AvatarGame"/> class.
/// </summary>
public AvatarGame()
{
    // These are implemented for you.
    graphics = new GraphicsDeviceManager( this );
    Content.RootDirectory = "Content";
 
    // Create and add the GamerServices component.
    Components.Add( new GamerServicesComponent( this ) );
 
    // Create the array that will hold the collection of animations.
    this.animations = new AvatarAnimation[ 30 ];
 
    // Create the array of matrices that will hold bone transforms for transitions.
    this.transitionTransforms = new Matrix[ 71 ];
 
    // Create the random-number generator.
    this.randomizer = new Random( DateTime.Now.Millisecond );
}

Como es habitual, Uds. deben crear los gestores y servicios que utilizarán para dibujar los avatares.

Pero por qué necesitamos también dos arreglos y un generador de números al azar? El último, en caso que quiéramos cambiar las animaciones sin un órden en particular.

Y los arreglos? Sigan leyendo …

4. Cargando contenido:

/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch( GraphicsDevice );
 
    // Load a spritefont.
    this.font = this.Content.Load<SpriteFont>( "MainFont" );
 
    // For each position in the array.
    for ( int i = 0; i < this.animations.Length; i++ )
    {
        // Create and store the corresponding animation.
        this.animations[ i ] = new AvatarAnimation( (AvatarAnimationPreset)i );
    }
 
    // Create a random description (instead, you can try to get the 
    // description of a signedIn gamer).
    this.avatarDesc = AvatarDescription.CreateRandom();
 
    // Create the renderer with a standard loading effect.
    this.avatarRenderer = new AvatarRenderer( avatarDesc, true );
 
    // Just for fun, set the current animation, randomly.
    this.currentAnimationId = this.randomizer.Next( this.animations.Length );
    this.currentAnimation = this.animations[ this.currentAnimationId ];
 
    // Set the "World" value.
    this.avatarRenderer.World =
        Matrix.CreateRotationY( MathHelper.ToRadians( 180.0f ) );
 
    // Set the "Projection" value.
    this.avatarRenderer.Projection =
        Matrix.CreatePerspectiveFieldOfView(
            MathHelper.ToRadians( 45.0f ),
            this.GraphicsDevice.Viewport.AspectRatio,
            .01f,
            200.0f );
 
    // Set the "View" value.
    this.avatarRenderer.View =
        Matrix.CreateLookAt(
            new Vector3( 0, 1, 3 ),
            new Vector3( 0, 1, 0 ),
            Vector3.Up );
}

Como pueden ver, en este método poblamos el arreglo de animaciones con cada una de las animaciones por defecto para los avatares. La razón? Usamos este arreglo como una memoria intermedia a efectos de agilizar el proceso de búsqueda al momento de pasar de una animación a la siguiente.

Entonces, configuramos la transición inicial y establecemos los campos de visión y proyección (nota: ambas matrices están siempre “fijas” en este ejemplo).

No hay código significativo para los métodos Initialize y Unload, y por ende avancemos al método Update.

5. Actualizando el juego:

/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update( GameTime gameTime )
{
    // Update the gamepad state for player one.
    this.lastGamePadState = this.currentGamePadState;
    this.currentGamePadState = GamePad.GetState( PlayerIndex.One );
 
    // Allow the game to exit.
    if ( this.currentGamePadState.Buttons.Back == ButtonState.Pressed )
    {
        this.Exit();
    }
 
    // Force moving to the next animation without waiting for the 
    // current animation to end.
    if ( this.currentGamePadState.Buttons.A == ButtonState.Pressed &&
        this.lastGamePadState.Buttons.A == ButtonState.Released &&
        !this.isInTransition )
    {
        this.moveToNextAnimation = true;
    }
 
    // Change the type of selection: ascending order or randomly.
    if ( this.currentGamePadState.Buttons.B == ButtonState.Pressed &&
        this.lastGamePadState.Buttons.B == ButtonState.Released )
    {
        this.moveRandomly = !this.moveRandomly;
    }
 
    // Is there any animation to update?
    if ( currentAnimation != null )
    {
        // Is the current animation just about to finish or forced to end?
        if ( !this.isInTransition &&
            this.targetAnimation == null &&
            ( this.currentAnimation.RemainingTime().TotalSeconds <= 1 ||
                this.moveToNextAnimation ) )
        {
            // Are we moving randomly?
            if ( this.moveRandomly )
            {
                // If so, select a new animation at random.
                this.currentAnimationId = this.randomizer.Next( this.animations.Length );
            }
            else
            {
                // If not, point to the next animation.
                this.currentAnimationId++;
 
                // Keep the id pointing to a valid position in the array.
                this.currentAnimationId %= this.animations.Length;
            }
 
            // Set the corresponding target animation.
            this.targetAnimation = this.animations[ this.currentAnimationId ];
        }
 
        // Has the animation reached its last frame? Or is it forced 
        // to change by the user?
        if ( this.currentAnimation.LastFrameReached() || this.moveToNextAnimation )
        {
            // If so, start by resetting this marker flag to false.
            this.moveToNextAnimation = false;
 
            // State that we will process a transition.
            this.isInTransition = true;
 
            // Make a copy of the readonly transforms to the transition array.
            this.currentAnimation.BoneTransforms.CopyTo( this.transitionTransforms, 0 );
 
            // Reset the current animation.
            this.currentAnimation.CurrentPosition = TimeSpan.Zero;
 
            // Set the target one as the current one.
            this.currentAnimation = this.targetAnimation;
            this.targetAnimation = null;
 
            // You can tweak this value to meet your game's need,
            // considering the lenght of your animations (it could vary).
            this.transitionStep = .5f;
        }
 
        // Update the current animation (in this example, there is no looping).
        this.currentAnimation.Update( gameTime.ElapsedGameTime, false );
 
        // Are we processing a transition?
        if ( this.isInTransition )
        {
            // If so, update the progress value.
            this.transitionProgress += this.transitionStep * (float)gameTime.ElapsedGameTime.TotalSeconds;
 
            // Is the progress below 100%?
            if ( transitionProgress < 1f )
            {
                // Calculate the proper bone transforms.
                this.CalculateTransition();
            }
            else
            {
                // When the progress reaches 100%, reset everything.
                this.isInTransition = false;
                this.transitionProgress = 0;
            }
        }
    }
 
    // As usual, call the base method.
    base.Update( gameTime );
}

En tiempo de ejecución, si pulsan el botón ‘A’ y no hay ningúna transición ejecutándose, obligan “al avatar” a comenzar la transición hacia la siguiente animación.

Pulsando el botón ‘B’, cambian la forma en la que se selecciona la próxima animación: en orden ascendente o aleatoriamente.

Noten que cuando la animación actual está por finalizar, entonces el juego selecciona la próxima animación, en el método de ordenamiento que el usuario tiene seleccionado actualmente.

Al alcanzase el último cuadro de la animación que se ejecuta (sin repetición) o el usuario fuerza la transición, entonces copiamos las matrices de transfomación de los huesos para la posición en la animación, retornamos la posición a cero y luego cambiamos la animaicón por la “objetivo”.

Uds. pueden preguntarse cuál es el propósito del campo “transitionStep”. Este factor establece el tiempo que dedicaremos para movernos de “la última” posición de la animación que finaliza a la posición “corriente” de la animaición que incia.

Por qué hacia la “corriente”? El comportamiento que decidí elegir para crear una sensación creíble para la transición fue evitar ir de “la última a la primera” y en cambio de "la última a la actual” (noten que la animación que inicia comenzará desde el momento que ejecutamos la transición de salida, y por ello la posición actual o corriente se modificará).

Por supuesto que pueden cambiar el mencionado comportamiento si prefieren un comportamiento que vaya “desde la posición final a la inicial” antes de ejecutar la animación entrante.

Llamamos al método “CalculateTransition”, por último, el cual explicaré luego del método “Draw”.

6. Dibujando al avatar:

/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw( GameTime gameTime )
{
    // As usual, clear the backbuffer (or the current render target).
    GraphicsDevice.Clear( Color.CornflowerBlue );
 
    // Can we draw the avatar?
    if ( avatarRenderer != null && currentAnimation != null )
    {
        // If we can, is the animation in transition?
        if ( this.isInTransition )
        {
            // If so, draw it with the interpolated transforms.
            avatarRenderer.Draw(
                this.transitionTransforms,
                currentAnimation.Expression );
        }
        else
        {
            // If not, draw it with the actual transforms.
            avatarRenderer.Draw(
                this.currentAnimation.BoneTransforms,
                currentAnimation.Expression );
        }
    }
 
    // The following is used to show some statistics and other info
    // on screen. It can be omitted (or optimized).
    this.spriteBatch.Begin();
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'A' to force changing animations or 'Back' to exit.",
        new Vector2( 50, 25 ),
        Color.White );
 
    // No need for further explanation.
    this.spriteBatch.DrawString(
        this.font,
        "Press 'B' to change the type of selection : " +
        ( this.moveRandomly ? "RANDOMLY" : "IN ASCENDING ORDER" )
        + ".",
        new Vector2( 50, 55 ),
        Color.White );
 
    // Draw the animation pointer, whether we are processing a transition and
    // the current transition time. Please notice that in this implementation
    // when the current animation is about to end (that is, 1 second or less),
    // the pointer "currentAnimationId" will change even if the animation is still
    // the same, so you will see a different number and name during 1 second or so.
    this.spriteBatch.DrawString(
        this.font,
        this.currentAnimationId + " : " +
            ( (AvatarAnimationPreset)this.currentAnimationId ).ToString() +
            " (" +
            ( !this.isInTransition ? "no transition" : this.transitionProgress.ToString() + " processed" ) +
            ").",
        new Vector2( 50, 85 ),
        Color.White );
 
    // Draw the current position and length of the animation being rendered.
    if ( currentAnimation != null )
    {
        this.spriteBatch.DrawString(
            this.font,
            "Processed " +
            this.currentAnimation.CurrentPosition.ToString() +
                " of " +
                this.currentAnimation.Length.ToString() +
                ".",
            new Vector2( 50, 115 ),
            Color.White );
    }
 
    // Flush the batch.
    this.spriteBatch.End();
 
    // As usual, call the base method.
    base.Draw( gameTime );
}

El único código de importancia aquí es el que está a cargo de dibujar o bien la animación tal cual es, o bien la transición calculada.

Puesto que el método Draw de AvatarAnimation espera una instancia del tipo “IList”, podemos pasar un arreglo de matrices directamente sin crear una colección “ReadOnlyCollection”.

7. Finalmente, la magia:

/// <summary>
/// Calculates the proper bone transoforms for the current transition.
/// </summary>
private unsafe void CalculateTransition()
{
    // If so, declare all needed helper primitives.
    // For debugging purposes you can use local fields marked as "final",
    // which in this example are commented out.
    Matrix currentMatrix, targetMatrix;
    Vector3 currentScale, targetScale; //, finalScale;
    Quaternion currentRotation, targetRotation; //, finalRotation;
    Vector3 currentTranslation, targetTranslation; //, finalTranslation;
 
    // For each transform's matrix.
    for ( int i = 0; i < this.transitionTransforms.Length; i++ )
    {
        // Since we are pointing to a managed struct we must use the 
        // reserved word "fixed" with an "unsafe" method declaration,
        // if we want to avoid traversing the array several times.
        fixed ( Matrix* matrixPointer = &this.transitionTransforms[ i ] )
        {
            // Get both, the current and target matrices.
            // Declaring these two local fields could be omitted 
            // and be used directly in the calcs below, but 
            // they are really useful when debugging.
            currentMatrix = *matrixPointer;
            targetMatrix = this.currentAnimation.BoneTransforms[ i ];
 
            // Get the components for the current matrix.
            currentMatrix.Decompose(
                out currentScale,
                out currentRotation,
                out currentTranslation );
 
            // Get the components for the target matrix.
            targetMatrix.Decompose(
                out targetScale,
                out targetRotation,
                out targetTranslation );
 
            // There's no need to calculate the blended scale factor, since we
            // are mantaining the current one, but I include it in the example
            // for learning purposes in case you need it.
            /*Vector3.Lerp(
                ref currentScale,
                ref targetScale,
                this.transitionProgress,
                out currentScale );*/
 
            // Interpolate a rotation value from the current an target ones,
            // taking into account the progress of the transition.
            Quaternion.Slerp(
                ref currentRotation,
                ref targetRotation,
                this.transitionProgress,
                out currentRotation );
 
            // Interpolate a translation value from the current an target ones,
            // taking into account the progress of the transition.
            Vector3.Lerp(
                ref currentTranslation,
                ref targetTranslation,
                this.transitionProgress,
                out currentTranslation );
 
            // Calculate the corresponding matrix with the final components.
            // Again, in this example, the creation of the scale matrix can be omitted
            // from the formula below (you may only use the rotation and tranlsation
            // factors obtaining the same result and save some processing power per loop).
            //this.transitionTransforms[ i ] =
            *matrixPointer =
                //Matrix.CreateScale( currentScale ) *
                Matrix.CreateFromQuaternion( currentRotation ) *
                Matrix.CreateTranslation( currentTranslation );
        }
    }
}

Cada vez que necesitemos calcular y actualizar las matrices de transformación interpoladas, vamos a tener que descomponer las matrices de la última posición de la animación que finaliza y la de la posición de la animación entrante.

Si la última posición de la animación más antigua está “fija”, por qué necesitamos descomponer sus matrices cada vez que llamamos a este método? Muy buena pregunta. Respuesta corta: no lo necesitamos. Podríamos calcular las matrices de transformación viejas una vez y almacenarlas en un campo privado para el juego, pero las incluyo en caso que desen cambiar el comportamiento por defecto (por ejemplo, si deciden mantener en ejecución la animación que está por finalizar así como también la de la que ingresa).

Qué pasa con las palabras “unsafe” y “fixed”? Debemos declarar ambas a los efectos de utilizar punteros a tipos por valor (y además marcar el proyecto para que permita código inseguro). Fijar el campo va a indicar al GC de no recolectarlo hasta que no lo precisemos más, y, en este caso, nos permitirá usar directamente el valor guardado en la pila de memoria sin tener que recorrer el arreglo de las matrices de transformación de las transiciones dos veces por bucle “for”, primero para obtener el valor y luego para actualizarlo.

Fiúuu! Esto es todo por ahora; cuando compilen y corran el programa verán como un avatar creado al azar realizan unas lindas transiciones o bien cuando una animación finaliza o cuando Uds. pulsan el botón ‘A’.

Pueden descargar el proyecto de ejemplo de este artículo desde aquí (van a encontrar una clase extra con dos métodos de extensión para la clase AvatarAnimation, llamados “LastFrameReached” y “RemainingTime”).

Pueden modificar el comportamiento y optimizar el código un poco (digamos la forma de dibujar, crear una clase específica para los avatares y demás), o esperar las partes de la serie que se vienen en breve …

Que lo disfruten!
~Pete

> Vínculo a la versión en inglés.