jueves, 12 de febrero de 2009

XNA Y METODOS DE EXTENSION - PARTE 1

El artículo original fue escrito el 03-Nov-08.

Con la liberación de XNA GS 3, dos nuevos tutoriales fueron incluidos en la sección "What's New In This Release" en el archivo de ayuda: los juegos "TopDownShooter" y "FuelCell". En lo que sigue, nos vamos a referir al primer juego de ejemplo: TopDownShooter.

A veces precisamos obtener una instancia de un efecto de sonido sin ejecutarlo. La buena noticia es que puede hacerse. La mala noticia es que no se ha provisto ningún método por defecto para obtener dicha instancia en un sólo paso.

Y bien, que sigue entonces? Abre el archivo AudioManager.cs del ejemplo TopDownShooter. Encontrarás el siguiente código en el método LoadContent():

// Podemos ejecutar silenciosamente un sonido para obtener
// una instancia específica para luego utilizar con llamadas
// "
Pause" y "Play"
SeekerInstance = Seeker.Play(0, 0.75f, 0, true); SeekerInstance.Stop();
SeekerInstance.Volume = FXVolume; // restablece el volúmen

Como puedes ver, el código anterior da una idea de qué deberías usar para obtener una instancia de un efecto de sonido "sin ejecutarlo".

Pero, que si quierer obtener dicha funcionalidad en un sólo método (como si fuera provisto por defecto) sin extender la clase "SoundEffect"? Bueno, en ese caso dado que apuntamos al .NET Framework 3.5 podemos usar la magia de los métodos de extensión ("Extension Methods").

No voy a explicar que son los llamados métodos de extensión (para hacer una primera lectura del tema, por favor visita esta página), sino cómo poder beneficiarnos de los mismos para este caso en particular.

Nuestra metar es extender la clase "SoundEffect" de forma tal que:

  1. No creamos una especificación de la clase,
  2. Obtengamos una instancia de un efecto de sonido ("SoundEffectInstance") sin que lo ejecutemos o escuchemos,
  3. No generemos resíduos innecesarios al obtener dicha instancia, y
  4. Nuestro método se ejecute efficientemente en el "Compact Framework" de .NET.

Por ello, nuestra solución debería ser la siguiente:

  1. Usaremos la técnica métodos de extensión,
  2. Imitarémos el código de nuestro juego de ejemplo "TopDownShooter",
  3. No podrémos declarar una referncia local del tipo "SoundEffectInstance" dentro de nuestro método, y
  4. Deberíamos pasar un parámetro por referencia del tipo "SoundEffectInstance" que no sea nulo.

Por lo tanto, nuestra implementación final debería lucir algo así:

using System;
using Microsoft.Xna.Framework.Audio;

namespace MyNamespace.Audio
{
public static class AudioExtensions
{
private static float VolumeLevelByDefault = .5f;

public static void GetCue(this SoundEffect 
              soundEffect, out SoundEffectInstance  
              soundEffectInstance)
 {
GetCue(soundEffect, out soundEffectInstance,
                   VolumeLevelByDefault, 0f, 0f, false);
  }

public static void GetCue(this SoundEffect
              soundEffect, out SoundEffectInstance
              soundEffectInstance, float volume, bool loop)
{
GetCue(soundEffect, out soundEffectInstance,
                   volume, 0f, 0f, loop);
}

public static void GetCue(this SoundEffect
              soundEffect, out SoundEffectInstance
              soundEffectInstance, float volume, float pitch,
              float pan, bool loop)
{
// Ejecuta el sonido silenciosamente y
               // deténlo inmediatamente.
soundEffectInstance = soundEffect.Play(0f,
                   pitch, pan, loop);
soundEffectInstance.Stop();
// Configura el nivel de volúmen indicado. soundEffectInstance.Volume = volume;
}
}
}

Como puedes ver, estoy nombrando el método estático "GetCue" pero puedes llamarlo si es que lo prefieres, digamos, "GetInstance". También, puesto que no podemos usar aún parámetro opcionales -tendrémos que esperar a C# 4.0 para eso- tendrás que crear tantas sobrecargas como precises.

Ahora bien, in el método "LoadContent()" del archivo "AudioManager.cs", todo lo que debemos usar es:

// Obtiene la instancia del efecto de sonido "Seeker". Seeker.GetCue(out SeekerInstance, FXVolume, 0.75f, 0,
    true);

No olvides agregar la sentencia "Using" apropiada en AudioManager.cs, la cual refiera al espacio de nombres ("namespace") donde el método de extensión fue declarado e implementado (en mi ejemplo de arriba es "MyNamespace.Audio"), o tu código no se compilará.

Además, deberás agregar manualmente en tu proyecto la referencia al ensamblado de 3.5 llamado "System.Core" -en caso que no esté ya referenciado.

Bien, ya está. Ahora sabes cómo extender la clase SoundEffect a fin de obtener una nueva instancia de un efecto de sonido en tan sólo una única llamada a un método, virtualmente hablando ;)

Una nota final: al usar "Extension Methods" no estamos rompiendo ningún principio de diseño, puesto a que no hay acceso directo a miembros privados del tipo "extendido" (siendo esto último, en el ejemplo, la clase "SoundEffect").

Que lo disfutes!
~Pete

GESTORES DE CONTENIDO "101"

El artículo original fue escrito el 15-Oct-08.

Si has estado utilizando XNA GS desde hace un tiempo, entonces conocerás de memoria los beneficios del ducto de contenidos. Si no, sólo para mencionar unos pocos, aquí está una breve losta para que cheques antes de seguir leyendo esta publicación:

  • Mejora el proceso de importación de activos a tus propios juegos basados en XNA,
  • Tiempo de carga más rápido para activos pre-construidos (léase, compilados a un formato binario), y
  • Fácil de extender para importar contenido personalizado en nuestras creaciones basadas en XNA.

Ok, ahora continuemos.

I. LO BASICO

Durante el proceso de creación de tu juego, una vez construidos todos tus activos en tiempo de compilación, precisarás entonces un Gestor de Contenido ("Content Manager") para cargarlos en tiempo de ejecución. Afortunadamente, como quizás ya puedas saber, XNA ya provée uno para ti. Lo que es más, la plantilla de proyecto de juego define una referencia y crea una instancia de la misma en la clase "Game".

El gestor se encargará de ambos, los procesos de carga y descarga de activos pre-construidos. Esto significa que detrás de cámaras, no sólo gestionará la instanciación de los activos que están siendo cargados sino también, cuando se requiera apropiadamente, desechará por ti aquellos que sean desechables ("disposable").

Para cargas, tendrás que usar una sentencia tal como "this.Content.Load<...>(...);" o también como ser "this.Game.Content.Load<...>(...);" y el lugar usual para incluir algúna de ellas es dentro del método suplantado "LoadContent" de tu clase "Game" y/o componentes de juego dibujables.

Y para descargas, todo lo que tienes que hacer es llamar al método "Unload()" de tu gestor. De nuevo, el lugar usual para inluir dicha sentencia es dentro del método suplantado "UnloadContent" de tu clase "Game" y/o componentes de juego dibujables.

II. DETRAS DE LA PANTALLA

Ahora bien, si has estado leyendo atentamente notarás que puedes cargar tus activos de forma individual pero no hay forma de descargarlos individualmente. Al principio puedes pensar que esto no es más que un restricción desagradable, pero como puedes llegar a concluir luego de leer este artículo -o al menos como espero que suceda, en realidad no lo es tanto. De hecho, en cierta forma contribuye a gestionar tus activos de manera prolija.

En general, por cuestiones de performance, de asignación eficiente de memoria y mejoras en la experiencia de usuario, diseñas tu juego de forma tal que todos los activos requeridos sean cargados de "una sola vez", digamos, al comienzo de cada nivel. E incluso si cargas tus activos de forma individual, ese comportamiento desde la óptica de "un período de tiempo compartido" -para llamarlo de alguna manera, muestra que has programado la lógica de tu juego a los efectos de tratar a todos los activos cargados como un grupo.

El razonamiento antedicho puede pués extenderse al proceso de descarga de activos. No hay necesidad de asumir a priori que cada activo sea descargado individualmente en distintos momentos durante el juego. Y por lo tanto, los activos serán tratados como un grupo, en este caso durante el nivel de juego respectivo, y descargados en forma conjunta, otra vez, en "un período de tiempo compartido", cuando ello sea requerido.

Puedes cuestionarte: "Ok, pero que si quiero desechar cierto activos en tiempo de ejecución y, al mismo tiempo, mantener otros en memoria?". La respuesta a esa inquietud en verdad es simple: crea más de un gestor de contenido.

Dentro de instantes vamos a volver a este punto, pero primero veamos otro aspecto importante del gestor de contenido.

III. LO QUE NO SE DEBE HACER

Evita desechar manualmente tus activos!!! Al gestor de contenido no le gusta que manejes la destrucción de activos a través del uso de sentencias como "this.myAsset.Dispose();". De hecho, el gestor NO monitorea si lo haces, y por ende, podría llegar a confundirse cuando ello sucede.

Veamos un ejemplo. Digamos que tienes las clases "Screen1" y "Screen2" tal que:

  1. Ambas comparten el gestor de contenido de todo el juego,
  2. Ambas cargan la misma texture pre-construida en sus respectivos campos locales "Texture2D",
  3. Screen1 se crea y muestra primero, y
  4. Screen2 se crea y muestra únicamente luego que Screen1 es desechada de forma manual.

Si Screen1 manualmente desechara la textura (ya sea mediante el uso del patrón de diseño "Dispose" o llamando al método Dispose dentro del método UnloadContent) sin llamar a "this.Game.Content.Unload();" primero, cuando Screen2 intente dibujar dicha textura en pantalla obtendrás una excepción indicando que la texura ha sido desechada, incluso si Screen2 cargó dicha textura. Como dije antes, el gestor de contenido se confunde en situaciones como ésta.

Por lo tanto, evita estas implementaciones:

protected override void Dispose(bool disposing) {
if (disposing) {
if (this._myTexture != null) {
this._myTexture.Dispose(); this._myTexture = null;
}
} base.Dispose(disposing);
}

... y ...

protected override void UnloadContent() {
if (this._myTexture != null) {
this._myTexture.Dispose(); this._myTexture = null;
} base.UnloadContent();
}

Siendo la forma apropiada de desechar activos:

protected override void UnloadContent() {
this.Game.Content.Unload(); // O el gestor que uses. base.UnloadContent();
}

Nota por favor que el gestor de contenido también permite lo siguiente:

protected override void Dispose(bool disposing) {
base.Dispose(disposing); if (disposing) {
if (this._myTexture != null) {
this._myTexture.Dispose(); this._myTexture = null;
}
}
} protected override void UnloadContent() {
this.Game.Content.Unload(); // O el gestor que uses. base.UnloadContent();
}

Cúmple con lo antedicho, y lo harás bien.

IV. QUE HACER

Volviendo a la cuestión de cómo descargar activos en diferentes momentos durante el tiempo de ejecución, hay una práctica que ayudará a entender porqué es sano y por ello, recomendable, tener más de un gestor de contenido en tus juegos creados con XNA.

Cuando creas un programa declaras variables globales y locales. Esta práctica muy común incorporada a nuestras tareas de programación diarias es la clave que nos conducirá a pensar en activos globales y locales.

Puedes considerar a un activo como "global" si su tiempo de vida dura todo el juego, o al menos, la mayoría del mismo. En este sentido, los activos deben ser considerados "locales" en cambio si se espera y pretende descargarlos al terminar un nivel del juego pero cuando el juego mismo aún no ha finalizado y no la hará, al menos, por algún tiempo más.

Por ende, cuando estes creando un juego con XNA usa: 1) Un gestor de contenido "global": que es instanciado pro defecto dentro de la clase "Game" (al cual puedes acceder usando la propiedad Content), y 2) Uno (o más) gestor(es) de contenido para activos "locales": digamos un gestor de contenido por nivel, pantalla, o por cualquier categoría que satisfaga al diseño de tu juego.

De nuevo, agrupa tus activos en conjuntos ("clusters") basados en su tiempo de vida, y sabrás cuáles son globales y cuáles son locales. Si todas tus pantallas usarán un mismo "spritefont" en común, entonces no hay motivo para cargarlo y descargarlo para cada pantalla; simplemente cárgalo una vez y mantenlo en la colección de activos globales. Si una modelo de nave será usado en un único nivel, adminístralo de manera local a dicho nivel.

Como puedes ver, usar más de un gestor de contenido es una práctica sana: fácil de aplicar, fácil de depurar, de fácil mantenimiento y fácil de extender.

A veces no hay necesidad de contar con más de un gestor de contenido para todo el juego, lo cual depende del juego mismo por supuesto, pero aún es una buena práctica sana de la cual estar al tanto y a la cual acostumbrarse, dado que al final ayudará a administrar nuestros activos en tiempo de ejecución de una manera más ordenada y conveniente.

Ergo, mi recomendación es: acostúmbrate. Cuanto antes, mejor.

V. CONCLUSION

El ducto de contenido ("Content Pipeline") es una gran herramienta para importar activos a nuestros juegos creados con XNA, lo cual se complementa con el gestor apripiado para manejar el contenido en tiempo real: la clase "ContentManager".

Mediante el uso de instancias de esta clase de forma sabia y apropiada, podríamos obtener (más) eficiencia en materia de: implementación, depuración, mantenibilidad y extensibilidad.

A efectos de lograr esta meta, ciertas prácticas deben aplicarse:

  1. Para activos globales, usa una instancia de la clase "ContentManager",
  2. Para activos locales, usa al menos una instancia de la clase "ContentManager", y
  3. No deseches lo activos manualmente; en cambio, llama al método "Unload()" del gestor de contenido.

Espero que encuentres este artículo útil. Tus comentarios y sugerencias son bienvenidas.

Hasta la próxima!
~Pete