El XNA Content Pipeline

MUY AVANZADO

Que es el Content Pipeline?

El content pipeline es una API que permite a los desarrolladores y diseñadores incorporar contenidos multimedia en los proyectos creados con XNA framework, estos contenidos multimedia son por ejemplo:

  • imágenes
  • sonidos
  • contenido 3d
  • efectos

Por defecto el content pipeline soporta una amplia gama de formatos de archivo diferentes los cuales son usualmente usados en la industria de los videojuegos.

El content pipeline (en adelante CPL) facilita el acceso a estos archivos y brinda un interfaz de acceso unificada que permite acceder a dichos recursos desde cualquier objeto utilizado dentro del juego sin necesidad de hacer uso de múltiples referencias cruzadas, lo cual desde luego va en favor de la independencia de cada componente lo cual en un juego generalmente es una tarea muy difícil de hacer.

Sin embargo las capacidades del CPL podrían verse limitadas por la cantidad de archivos que soporta puesto que es muy común que en la industria de los videojuegos se usen formatos de archivo independientes de acuerdo a las necesidades particulares de cada proyecto, es aquí donde reside una de las mas importantes características del CPL… es extensible.

El CPL incorpora un marco de trabajo que permite fácilmente incorporar soporte a diferentes tipos de archivo e incluso extender la funcionalidad de un tipo de archivo ya soportado.

Como Funciona el Content PipeLine?

Lo primero ha tener en cuenta son los elementos con los cuales trabaja:

Namespace: Microsoft.Xna.Framework.Content.Pipeline
ContentImporter

ContentProcessor

Namespace: Microsoft.Xna.Framework.Content
ContentTypeReader

Namespace: Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compile
ContentTypeWriter

El camino comienza con el ContentImporter este es el encargado de realizar lectura física del archivo a importar y colocar la información en un objeto capaz de almacenarla.

Seguidamente la información colocada en el objeto es pasada a través de un ContentProcessor el cual se encarga de realizar transformaciones en la información cargada en el paso anterior.

El ContentTypeWriter se encarga de escribir ese objeto en un archivo con el formato del CPL. Todos los pasos anteriores suceden en tiempo de compilación, el ultimo paso sucede en tiempo de ejecución cuando el juego ya esta ejecutándose, es allí donde el ContentTypeReader lee los archivos creados por el CPL para cargar en memoria el recurso almacenado.

Debido a que se debe adicionar una referencia a estas clases en el CPL es necesario que los tres primeros objetos existan compilados en una librería dinámica, mientras que el ultimo puede hacer parte el juego directamente.

XNA Content Pipe Line

Cómo hacer que el Content Pipeline soporte un archivo diferente?

Normalmente con tan solo el ContentImportery el ContentProcessor es suficiente para cargar un tipo de archivo ya que basicamente lo que se hace es utilizar estos objetos para convertir los datos del archivo en uno de los tipos de dato ya soportados por el CPL.

Sin embargo si se requiere información adicional que no es contenida en el tipo soportado es entonces necesario que el desarrollador establezca la forma en que el CPL escribe esos datos en un archivo del CPL y la forma en que el CPL debe cargarlos una vez se solicite desde el juego. Para ello son las clases ContentTypeWriter y ContentTypeReader.

La buena noticia es que para adicionar soporte a un nuevo tipo de archivo estas clases proveen los mecanismos necesarios para implementarlo de manera relativamente fácil, la mala noticia es que todas son clases abstractas así que la mayoría del trabajo lo debe hacer uno mismo. :P

A continuación un ejemplo de Implementación de un nuevo tipo de archivo para el CPL.

Supongamos existe un tipo de arhivo bmp2 dicho archivo posee la información habitual de un archivo bmp más tres datos muy importantes que requerimos (todos los campos son un ejemplo):

  • Color de Máscara
  • Número de Colores
  • Prioridad

La tarea nro 1 crear un ContentImporter. Hay que recordar que ContentImporter es una clase abstracta asi que se debe crear una clase que herede de ContentImporter, adicionalmente esta clase es genérica por lo cual debemos pasar el parámetro del tipo de dato que se soportara para importar.

 public class BMP2Importer : ContentImporter 
{ }

Pero un momento… de donde salio BMP2Content ? bueno dado que se esta incorporando un nuevo tipo de archivo es necesario crear un objeto contenedor de los datos que se cargan desde ese archivo, teniendo en cuenta que el formato bmp2 lo usaremos para crear texturas 2D lo mejor que se puede hacer es crear una clase que herede de un contenedor existente, para este caso Texture2DContent :

 
using Microsoft.Xna.Framework; 
using Microsoft.Xna.Framework.Graphics; 
using Microsoft.Xna.Framework.Content.Pipeline; 
using Microsoft.Xna.Framework.Content.Pipeline.Graphics; 

public class BMP2Content : Texture2DContent 
{
    public BMP2Content(): base() 
    {} 

    private Color maskColor; 
    public Color MaskColor 
    { 
        get { return maskColor; } 
        set { maskColor = value; } 
    } 

    private int numColores; 
    public int NumColores 
    { 
        get { return numColores; } 
        set { numColores = value; } 
    } 

    private byte prioridad; 
    public byte Prioridad 
    { 
        get { return prioridad; } 
        set { prioridad = value; } 
    } 
}

Este contenedor permite manipular los datos desde su cargue hasta su transformacion y escritura en un archivo del content pipeline, sin embargo se requiere de una estructura adicional para manipularlo, porque?

Bueno el contenedor puede ser realmente cualquier estructura pero se requiere que sea liviana puesto que la cantidad de recursos que tengamos puede incrementar dramaticamente el tiempo que se invierta en la compilacion del proyecto ya que cada uno de los recursos pasará por un importer, un processor y un writer, ya cuando se va a utilizar esa informacion en un objeto util para el juego si debemos disponer de toda la infromacion necesaria.

Para este ejemplo, debido a que el archivo de tipo bmp2 va a ser usado como textura se pueden usar dos opciones, una es crear una clase de textura nueva (poco recomendable) y la otra es crear una clase que herede de un tipo de textura ya existente, para el caso lo que mas nos conviene es crear una clase que herede de Texture2D.

 public class BMP2Texture2D : Texture2D 
{ 
    public BMP2Texture2D(GraphicsDevice device, int height, 
                         int with, int numberLevels, 
                         ResourceUsage usage, SurfaceFormat format) : 
                            base(device, with, height, 
                                 numberLevels, usage, format) 
    {} 

    public BMP2Texture2D(GraphicsDevice device, int height, int with, 
                         int numberLevels, ResourceUsage usage, 
                         SurfaceFormat format, ResourceManagementMode resourceManagementMode) : 
                            base(device, with, height, 
                                 numberLevels, usage, 
                                 format,resourceManagementMode) 
    { } 

    private Color maskColor; 
    public Color MaskColor 
    { 
        get { return maskColor; }
        set { maskColor = value; } 
    } 

    private int numColores; 
    public int NumColores 
    { 
        get { return numColores; } 
        set { numColores = value; } 
    } 

    private byte prioridad; 
    public byte Prioridad 
    { 
        get { return prioridad; } 
        set { prioridad = value; } 
    } 
} 

Esta clase ya trae todo lo que necesita una textura 2d más las cosas propias del formato bmp2 de el ejemplo.

Ahora si se puede crear el ContentImporter indicando el tipo de contenido a almacenar.

 [ContentImporter(".bmp2", DisplayName = "Imagen BMP2", DefaultProcessor = "Procesador para imagenes BMP2")] 
public class BMP2Importer : ContentImporter<bmp2content> 
{ 
    public override BMP2Content Import(string filename, ContentImporterContext context) 
    { 
        BPM2 miBmp = new BMP2(filename); 
        //Este objeto es de ayuda para recuperar la informacion de color y llevarla facilmente al 
        //MipmapChain del objeto textureContent PixelBitmapContent<rgb24> pixelHelper; 
        //Array de textura a retornar BMP2Content textureContent; 
        //Array de bytes para capturar la informacion del archivo JKI byte[] byteArray; 
        //Inicializa un nuevo contenedor de datos de pixeles 
        pixelHelper = new PixelBitmapContent</rgb24><rgb24>(miBmp.Width,miBmp.Height); 
        //Cargar la informacion de bytes para el archivo 
        byteArray = miBmp.GetBytes(); 
        //Envia los datos de bytes al pixel helper 
        pixelHelper.SetPixelData(byteArray); 
        //Inicializa un nuevo contenedor para la textura 
        textureContent = new BMP2Content(); 
        //Carga la informacion de la imagen en la textura 
        textureContent.Mipmaps = new MipmapChain(pixelHelper); 
        //Guarda el color de mascara de la imagen 
        textureContent.MaskColor = new Color(miBmp.ColorMascara.R,miBmp.ColorMascara.G, miBmp.ColorMascara.B); 
        //Establecer el numero de colores 
        textureContent.NumColores = miBmp.Palette.Count; 
        textureContent.Prioridad = 0; 
        return textureContent; 
    } 
}

La primera linea es un atributo de la clase, la cual le permitira al Content Pipeline identificar que esta es un ContentImporter y le proporcionara informacion adicional acerca de que informacion debe mostrar desde el IDE de Visual Studio respecto a los archivos que este ContentImporter es capaz de utilizar asi como un nombre descriptivo de la funcionalidad.

Seguidamente lo que se hace cargar desde archvo un objeto BMP2 el cual desde luego es un objeto que ya es capaz de cargar un archivo bmp2, el objeto pixelHelper es utilizado como una 'Helper Class' ya que si bien no se necesita de manera directa, si es un excelente atajo para poder crear un MipmapChain el cual es requerido para cargar en un objeto Texture2DContent con la informacion de la imagen que se requiere, finalmente en la parte inferior del método se asignan la informacion adicional que se requiere.

Una vez se ha importado la informacion desde el archivo esta debe pasar por el processor, el procesor se debe hacer para que funcione igual que funciona un procesor para una imagen BMP normal asi que se puede reutilizar un processor que ya este creado en XNA y eso es todo ya que sobre el resto de la informacion no se requiere hacer ninguna modificacion.

El processor en este caso es bastante sencillo:

 [ContentProcessor(DisplayName = "Procesador para imagenes BMP2")] 
class BMP2Processor : ContentProcessor<bmp2content , BMP2Content> 
{ 
    public override BMP2Content Process(BMP2Content input, ContentProcessorContext context) 
    { 
        input[i] = (BMP2Content)context.Convert<texturecontent ,TextureContent>( 
                         (TextureContent)input[i], "SpriteTextureProcessor"); 
        return input; 
    } 
}

El atributo inicial tiene la misma funcionalidad que en el importer y es proveer una pequeña descripcion del ContentProcessor para que aparezca en el IDE de Visual Studio. Se llama al metodo context.Convert indicandole que debe usar el builting processor SpriteTextureProcessor, esto hara que la informacion inherente a una textura 2d sea procesada de la manera habitual . Finalmente se procede a crear el ContentTypeWriter.

 [ContentTypeWriter] 
public class BMP2ContentWriter : ContentTypeWriter<bmp2content> 
{
    protected override void Write(ContentWriter output, BMP2Content value) 
    { 
        //Buffer para la informacion de la imagen 
        byte[] pixelData; 
        //Escribe alto y ancho 
        output.Write(value.Mipmaps[0].Height); 
        output.Write(value.Mipmaps[0].Width); 
        //Obtiene el contenido de la imagen 
        pixelData = value.Mipmaps[0].GetPixelData(); 
        //Escribe el tamaño de datos de la imagen 
        output.Write(pixelData.Length); 
        //Escribe la informacion de la imagen 
        output.Write(pixelData); 
        //Escribe el color de mascara de la imagen 
        output.WriteObject<color>(value.MaskColor); 
    }
    public override string GetRuntimeReader(TargetPlatform targetPlatform) 
    { 
        return typeof(BMP2ContentReader).AssemblyQualifiedName; 
    } 
} 

El atributo inicial tiene la misma funcionalidad que en el importer.

El método write lo que hace es escribir en el archivo del CPL la informacion relacionada con el archivo ya previamente cargado en el BMP2Content, el objeto output recibido como parametro posee metodos para escribir a nivel de tipos nativos y soporta una amplia gama de tipos incluidos en el xna framework, el método GetRuntimeReader almacena informacion que le indica el CPL en tiempo dejecucion que clase debe instanciar para poder convertir un archivo del CPL en el objeto que se desea; es decir indica cual implementacion de ContentTypeReader debe utilizarce para tal fin.

Esta es la implementacion de ContentTypeReader para leer en tiempo de ejecucion lo que el ContentTypeWriter ha guardado en tiempo de compilacion, el objeto input recibido como parametro posee metodos para leer a nivel de tipos nativos y soporta una amplia gama de tipos incluidos en el xna framework.

 ContentManager content; 
… 
… 
… 
BMP2Texture myTexture = content.Load(@"myImage.bmp2");

Espero les sea de utilidad.

Saludos.