jueves, 12 de febrero de 2009

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