Extender la gestión de recursos, audio

De IberOgre

Tras comprender los mecanismos por los que se rige la gestión de recursos en Ogre es probable que nos veamos en la necesidad de añadir nuevos tipos de recursos. No es extraño que en un videojuegos contemos con recursos adicionales a los que ofrece inicialmente Ogre, por ejemplo: efectos de sonido, niveles del juego o scripts de inteligencia artificial. En este texto aprenderemos a hacer que Ogre trabaje con nuevos recursos y ofreceremos una aplicación práctica integrando un sistema de audio con el resto del motor.

Para el sistema de audio nos valdremos de la popular biblioteca 2D libSDL y de su extensión mixer. Su integración es sencilla, ligera y podremos reproducir tanto efectos de sonido como música de fondo.


Contenido



Requisitos previos

En este texto no vamos a trabajar con el grafo de la escena por lo que el número de dependencias se ve reducido bastante. En cualquier caso, es altamente recomendable haber leído y dominar de forma práctica los conocimientos tratados en:

  • Inicialización y cierre de Ogre: es importante conocer la secuencia de inicialización de Ogre ya que tendremos que hacer ligeras modificaciones para integrar el sistema de audio.
  • Gestión de recursos: nos disponemos a extender la funcionalidad del subsistema de gestión de recursos de Ogre, así que es imprescindible dominar a la perfección los contenidos expuestos en este artículo.


Instalación de SDL

Logo de la biblioteca libSDL

libSDL es una biblioteca de desarrollo de videojuegos en 2D multiplataforma, de bajo nivel y compatible con los lenguajes C/C++. Se apoya en pequeñas bibliotecas que la extienden y cuenta con varios subsistemas: gráfico, eventos, red, audio, control de tiempo, etc. En caso de que no la conozcas y estés interesado, puedes acudir a Wikijuegos, instancia de Wikimedia con una amplísima documentación en castellano al respecto.

Nosotros estamos interesados principalmente en el subsistema de audio de libSDL y de su extensión mixer, la cual proporciona funciones adicionales para trabajar con distintos formatos. En esta sección instalaremos SDL junto a mixer en sistemas GNU/Linux y Windows. Finalmente actualizaremos nuestro makefile para trabajar con las nuevas bibliotecas.

Instalación de SDL en GNU/Linux

Suponiendo que estemos ante un sistema basado en Debian, simplemente debemos instalar los siguientes dos paquetes:

sudo apt-get install libsdl1.2-dev libsdl-mixer1.2-dev

En caso de utilizar una distribución no basada en Debian y, por tanto, que no cuente con apt, será necesario emplear el sistema de instalación de paquetes del que dispongas. Si los paquetes se encuentran en los repositorios de la distribución, es probable que tengan el mismo nombre.

Instalación de SDL en Windows

Instalaremos SDL y mixer en Windows suponiendo que trabajas con el compilador MinGW tal y como explicamos en "Creación de un entorno de trabajo multiplataforma". Supondremos que instalaste el compilador en C:\MinGW, si lo hiciste en otro directorio deberás cambiar las rutas según corresponda. Para proceder con la instalación, debes seguir los siguientes pasos:

  1. Descargar las bibliotecas de ejecución de libSDL.
  2. Descomprimir el paquete y copiar el fichero SDL.dll en el directorio C:\windows\system.
  3. Descargar las bibliotecas de desarrollo de libSDL para MinGW.
  4. Descomprimir el fichero, copiar el contenido de la carpeta lib en C:\MinGW\lib y copiar el contenido de include en C:\MinGW\include de forma que quede C:\MinGW\include\SDL.
  5. Descargar las bibliotecas de ejecución de mixer.
  6. Descomprimir el fichero y copiar las bibliotecas dinámicas .dll en C:\windows\system
  7. Descargar las bibliotecas de desarrollo de mixer.
  8. Descomprimir el paquete y copiar el contenido del directorio lib en C:\MinGW\lib y el contenido de include en C:\MinGW\include\SDL.

El nuevo Makefile

La jerarquía de directorios y el proceso de compilación tanto en GNU/Linux como en Windows que se explicó en "Creación de un entorno de trabajo multiplataforma" permanece inalterado. No obstante, ahora es necesario añadir SDL y mixer a la lista de bibliotecas con las que hay que enlazar. Como sabes, esta lista está indicada por la variable LDFLAGS.

Antes de comenzar con los objetivos de compilación, en el makefile de GNU/Linux añadimos:

LDFLAGS += -lSDL -lSDL_mixer

Por su parte, en el de Windows es necesario añadir:

LDFLAGS += -lSDLmain -lSDL -lSDL_mixer


Inicialización y cierre de SDL

Este artículo no pretende ser un manual sobre libSDL porque no es el objetivo de IberOgre y porque ya existe Wikijuegos. No obstante, sería complicado continuar sin dar ciertas pinceladas sobre el uso de la biblioteca, al menos únicamente las funciones que vamos a utilizar.

Antes de empezar a utilizar los recursos de sonido que desarrollaremos dentro de nuestro juego será necesario inicializar SDL y mixer. Lo usual es que este proceso esté encapsulado en la clase principal del sistema, la que se encarga de inicializar cada aspecto del motor aunque en última instancia es decisión del programador. En cualquier caso, es imprescindible preparar las bibliotecas antes de reproducir o cargar audio.

En primer lugar, debemos incluir el fichero de cabecera <SDL/SDL.h> y <SDL/SDL_mixer.h> allá donde deseemos utilizar funciones o tipos de las respectivas bibliotecas. Para inicializar SDL, haremos uso de la función SDL_Init:

int SDL_Init(Uint32 flags);
  • Uint32 flags: lista de valores indicando los subsistemas a inicializar que podemos combinar entre sí mediante el operador lógico |. En nuestro caso, sólo necesitamos el subsistema de audio representado mediante SDL_INIT_AUDIO. Para la lista completa de opciones lo mejor es acudir a la documentación oficial.

Cuando SDL se inicia de forma correcta devuelve 0 y en caso de error -1. Posteriormente, deberíamos inicializar la extensión mixer mediante la función Mix_OpenAudio tal y como se indica a continuación:

int Mix_OpenAudio(int frequency, Uint16 format, int channels, int chunksize)
  • int frequency: frecuencia de salida para las pistas medida en hercios. Lo mejor es utilizar el valor por defecto, MIX_DEFAULT_FREQUENCY.
  • Uint16 format: formato de salida de las pistas, es recomendable emplear MIX_DEFAULT_FORMAT.
  • int channels: número de canales para los efectos de sonido, puedes especificarlo manualmente o hacer uso del valor MIX_DEFAULT_CHANNELS.
  • int chunksize: número de bytes utilizado por pista de salida, lo usual es utilizar 4096 (2MiB).

De la misma manera, cuando mixer se inicia de forma correcta, la función anterior devuelve 0 y en caso contrario devuelve -1. A la hora de cerrar la aplicación y liberar los recursos el orden sería inverso, en primer lugar desconectamos mixer para después cerrar SDL. Para cerrar la biblioteca mixer hacemos uso de la función Mix_CloseAudio.

void Mix_CloseAudio(void);

Cuando hayamos llamado a la anterior, podemos pasar a cerrar SDL con SDL_Quit.

void SDL_Quit(void);

Ambas son funciones que no reciben ni devuelven nada por lo que podríamos hacer que se llamasen automáticamente al cierre de la aplicación gracias a la función atexit de la biblioteca estándar de C. A continuación, se expone un ejemplo completo de inicialización y programación del cierre de SDL y mixer dentro de una supuesta clase Juego.

#include <cstdlib>
 
#include <SDL/SDL.h>
#include <SDL/SDL_mixer.h>
 
bool Juego::inicializarSDL() {
    // Inicializamos el sistema de audio de SDL
    if (SDL_Init(SDL_INIT_AUDIO) < 0)
        return false;
 
    // Al cerrar, llamamos automáticamente a SDL_Quit
    atexit(SDL_Quit);
 
    // Inicializamos SDL mixer
    if(Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,MIX_DEFAULT_CHANNELS, 4096) < 0)
        return false;
 
    // Al cerrar, llamamos automáticamente a Mix_CloseAudio
    atexit(Mix_CloseAudio);
 
    return true;
}


Conceptos sobre la extensión del sistema de recursos

Como vimos en el artículo "Gestión de recursos", Ogre cuenta con una jerarquía de clases determinada para gestionar cada tipo de recurso que implementan ciertas interfaces. Para añadir un recurso nuevo basta con desarrollar las correspondientes clases que implementen dichas interfaces y, por tanto, incluyan los métodos necesarios para gestionar el nuevo recurso. El siguiente diagrama ilustra de forma general lo necesario para extender la gestión de recursos de Ogre. Es un ejemplo sencillo en el que añadiríamos el recurso NuevoRecurso.

Diagrama de clases para extender la gestión de recursos de Ogre

En las siguientes subsecciones haremos un recorrido por cada uno de los elementos que componen este diagrama explicando cada uno de los métodos que deben implementar. Tras este apartado, deberías ser capaz de crear nuevos recursos por ti mismo. De todos modos, más adelante incluiremos un ejemplo completo explicando cómo debe implementarse cada uno de los métodos.

El nuevo recurso

Cada recurso debe heredar de la clase abstracta Ogre::Resource e implementar los métodos que se indican en el diagrama previo. Por supuesto, según el tipo de recurso se añadirán métodos adicionales para trabajar con él. Por ejemplo, un efecto de sonido podría tener métodos de reproducción, parada y pausa.

El constructor del nuevo recurso cuenta con la siguiente forma:

NuevoRecurso::NuevoRecurso(Ogre::ResourceManager* creator,
                           const Ogre::String& name,
                           Ogre::ResourceHandle handle,
                           const Ogre::String& group,
                           bool isManual = false,
                           Ogre::ManualResourceLoader* loader = 0);
  • Ogre::ResourceManager* creator: puntero al gestor de recursos que ha creado este recurso.
  • const Ogre::String& name: nombre del recurso.
  • Ogre::ResourceHandle handle: manejador del recurso.
  • const Ogre::String& group: grupo al que pertenece el recurso.
  • bool isManual: true si el recurso se ha cargado de forma manual.
  • Ogre::ManualResourceLoader* loader: en caso de que el recurso se haya cargado de forma manual, puntero al cargador de recursos manual.

Por supuesto, debemos adjuntar un destructor:

NuevoRecurso::~NuevoRecurso();

En la sección protegida del nuevo recurso debemos añadir el método que, efectivamente carga el recurso en memoria. Recuerda que el ciclo de vida de los recursos hace posible que un recurso exista pero no esté cargado o preparado para su uso. El método loadImpl es el responsable de preparar el recurso para su uso.

void NuevoRecurso::loadImpl();

De la misma manera, podemos liberar la memoria que consume el recurso sin necesidad de destruirlo gracias al método protegido unloadImpl.

void NuevoRecurso::unloadImpl();

Es posible asignarles a los gestores de recursos de Ogre un presupuesto de memoria. Esto significa que los recursos bajo el control de dicho gestor no pueden superar la cuota determinada de consumo de memoria. Esto resulta útil en sistemas muy complejos en los que la memoria sea un bien preciado y haya que aprovecharlo al máximo (consolas de sobremesa o dispositivos móviles, por ejemplo). Para ello, cada recurso debe conocer lo que ocuparía en memoria (al menos una aproximación) antes de ser cargado para informar a su gestor. A tal efecto existe el método calculateSize.

size_t NuevoRecurso::calculateSize();

Punteros inteligentes

Ya hemos visto que una de las funcionalidades de los sistemas de gestión de recursos de cualquier motor es evitar duplicidades en los recursos. No hay necesidad de que un mismo recurso esté instanciado dos veces en memoria, sería un desperdicio. Ogre utiliza los punteros inteligentes compartidos Ogre::SharedPtr para conseguir este efecto. Se trata de una clase paramétrica que toma el recurso que hayamos creado y guarda un contador de referencias a dicho recurso de forma interna. Cuando copiemos el recurso, se incrementa y cuando destruyamos alguna referencia, se decrementa.

Esto permite al sistema liberar recursos cuando no se estén utilizando y compartir el mismo recurso entre distintos elementos sin duplicarlo. Sólo debemos crear una clase hija de Ogre::SharedPtr e implementar los siguientes constructores junto al operador de asignación.

NuevoRecursoPtr::RecursoPtr();
explicit NuevoRecursoPtr::NuevoRecursoPtr(NuevoRecurso* s);
NuevoRecursoPtr::NuevoRecursoPtr(const NuevoRecursoPtr& s);
NuevoRecursoPtr::NuevoRecursoPtr(const Ogre::ResourcePtr& r);
NuevoRecursoPtr& NuevoRecursoPtr::operator= (const Ogre::ResourcePtr& r);

En la sección práctica veremos qué deben contener estos métodos.

El nuevo gestor de recursos

Al añadir un recurso nuevo debemos implementar un gestor que se haga responsable de ese tipo de recursos. La nueva clase debe hacer uso de la herencia múltiple heredando de Ogre::ResourceManager y Ogre::Singleton. Como ya sabes, los gestores de recursos siguen el patrón de diseño Singleton, una sóla instancia accesible desde todo el sistema.

En primer lugar, debemos implementar su constructor, en el cual debe registrarse como un nuevo tipo de gestor de recursos en Ogre y, si corresponde, iniciar algún subsistema.

NuevoRecursoManager::NuevoRecursoManager();

En su destructor debe borrarse de la lista de gestores de recursos de Ogre, lo veremos detenidamente en el ejemplo práctico. Por supuesto, debe liberar la memoria que haya podido reservar.

NuevoRecursoManager::~NuevoRecursoManager();

Para permitir a los usuarios cargar nuevos recursos debe proporcionar un método load con la siguiente forma:

NuevoRecursoPtr NuevoRecursoManager::load(const Ogre::String& name,
                                          const Ogre::String& group = Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
  • const Ogre::String& name: nombre del recurso a cargar.
  • const Ogre::String& group: nombre del grupo al que pertenece el recurso.

Con el objetivo de acceder a la única instancia desde todo el sistema, debe implementar los siguientes métodos estáticos:

static NuevoRecursoManager& NuevoRecursoManager::getSingleton();
static NuevoRecursoManager* NuevoRecursoManager::getSingletonPtr();

Por último, a través del método protegico createImpl, creará los recursos pertinentes:

Ogre::Resource* NuevoRecursoManager::createImpl(const Ogre::String& name,
                                                Ogre::ResourceHandle handle,
                                                const Ogre::String& group,
                                                bool isManual,
                                                Ogre::ManualResourceLoader* loader,
                                                const Ogre::NameValuePairList* createParams);

Los parámetros son idénticos a los de NuevoRecurso::NuevoRecurso().


Ejemplo: música

En este primer ejemplo vamos a crear un nuevo recurso para reproducir música dentro de nuestro juego. Debemos tener en cuenta que, por limitaciones de la biblioteca SDL mixer, sólo se podrá reproducir una canción en un momento dado. Las pistas que cargaremos vendrán en formato OGG el cual está libre de patentes, al contrario que MP3. A continuación se exponen las clases necesarias junto a su implementación y la pertinente explicación.

La clase Song

En primer lugar nos centraremos en el propio recurso, la clase Song. Los métodos que debe implementar al heredar de la clase Resource ya han sido detallados y nos limitaremos a explicarlos en el momento de su implementación. Como es lógico, se añaden algunos métodos para utilizar el propio recurso:

  • play: reproduce la canción y detiene cualquier pista que se esté reproduciendo. Es posible indicarle el número de veces que debe repetirse, -1 para que se reproduzca de forma indefinida.
  • pause: pausa la canción, cuando se llame de nuevo a play, se reanudará la reproducción.
  • stop: detiene por completo la canción, cuando se llame a play comenzará desde el principio.
  • fadeIn: reproduce la canción con un efecto de entrada, subiendo poco a poco el volumen. Es posible indicarle el tiempo que tarda en alcanzar el volumen máximo.
  • fadeOut: detiene la reproducción con un efecto de salida, bajando poco a poco el volumen. Podemos indicarle el tiempo de duración del efecto.
  • isPlaying: un método estático para conocer si se está reproducciendo alguna canción en el sistema en un momento dado.

Para ello, cuenta con los siguientes atributos privados:

  • Mix_Music* _song: estructura de SDL mixer que guarda información sobre la canción.
  • Ogre::String _path: ruta completa al recurso.
  • size_t _size: tamaño del recurso.

A continuación se muestra el fichero song.h al completo. Para detalles adicionales sobre las funciones de SDL y SDL mixer lo mejor es acudir a la documentación oficial.

class Song: public Ogre::Resource {
    public:
 
        Song(Ogre::ResourceManager* creator,
             const Ogre::String& name,
             Ogre::ResourceHandle handle,
             const Ogre::String& group,
             bool isManual = false,
             Ogre::ManualResourceLoader* loader = 0);
 
        ~Song();
 
        void play(int loop = -1);
        void pause();
        void stop();
        void fadeIn(int ms, int loop = -1);
        void fadeOut(int ms);
        static bool isPlaying();
 
    protected:
        void loadImpl();
        void unloadImpl();
        size_t calculateSize() const;
 
    private:
        Mix_Music* _song;
        Ogre::String _path;
        size_t _size;
};

En primer lugar examinaremos la implementación de los métodos heredados de Resource. En el constructor simplemente delegamos en el constructor de Resource e inicializamos los atributos de la clase. Con el método createParamDictionary informamos a Ogre de la existencia de un recurso de tipo "Song".

Song::Song(Ogre::ResourceManager* creator,
               const Ogre::String& name,
               Ogre::ResourceHandle handle,
               const Ogre::String& group,
               bool isManual,
               Ogre::ManualResourceLoader* loader):
               Ogre::Resource(creator, name, handle, group, isManual, loader) {
    // Creamos el tipo de recurso si no existía ya
    createParamDictionary("Song");
 
    // Puntero a Song no apunta a nada
    _song = 0;
 
    // Tamaño inicial
    _size = 0;
}

En el destructor llamamos al método unload que internamente llamará a unloadImpl. Toda la liberación de memoria necesaria se llevará a cabo en el segundo método.

Song::~Song() {
    unload();
}

El método loadImpl es el encargado de cargar el recurso. Como ya sabrás, Ogre no utiliza la ruta completa para identificar a sus recursos. Si tenemos una textura "cesped.png" dentro del directorio "Texturas" no tendremos que referirnos a ella por su nombre completo "Texturas/cesped.png", sólo por el nombre del fichero. No obstante, cuando tenemos que cargar nuestro nuevo recurso necesitamos conocer su ruta completa. Al comienzo de este método buscamos la ruta completa de la canción a cargar.

Una vez tenemos la ruta llamamos a la función de SDL mixer Mix_LoadMUS para cargar la canción pasándole la ruta y capturamos los posibles errores. Por último, para ofrecer una aproximación sobre el tamaño de la música, abrimos el fichero de forma binaria y lo recorremos.

void Song::loadImpl() {
    _path = "";
 
    // Buscar la ruta del fichero
    Ogre::FileInfoListPtr info;
    info = Ogre::ResourceGroupManager::getSingleton().findResourceFileInfo(mGroup, mName);
    for (Ogre::FileInfoList::iterator i = info->begin(); i != info->end(); ++i) {
        _path = i->archive->getName() + "/" + i->filename;
    }
 
    if (_path == "") {
            Ogre::LogManager::getSingleton().logMessage("Song::loadImpl(): no se encuentra el recurso");
            throw (Ogre::Exception(Ogre::Exception::ERR_FILE_NOT_FOUND,
                                   "No se encuentra el fichero de la música",
                                   "Song::loadImpl()"));
    }
 
    // Cargar la música
    if ((_song = Mix_LoadMUS(_path.c_str())) == NULL) {
            Ogre::LogManager::getSingleton().logMessage("Song::loadImpl(): no se puede cargar la música");
            throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                                   "No se puede cargar la música",
                                   "Song::loadImpl()"));
    }
 
    // Calculamos el tamaño
    std::ifstream stream;
    char byteBuffer;
 
    stream.open(_path.c_str(), std::ios_base::binary);
 
    while (stream >> byteBuffer)
        ++_size;
 
    stream.close();
}

En el método unloadImpl simplemente hemos de llamar a la función Mix_FreeMusic. No es necesario nada más ya que no hemos reservado memoria adicional.

void Song::unloadImpl() {
    if (_song)
        Mix_FreeMusic(_song);
}

A la hora de informar del tamaño del recurso en el método calculateSize simplemente devolvemos el atributo privado _size.

size_t Song::calculateSize() const {
    return _size;
}

En el método play consultamos si la música estaba pausada para reanudarla con la función Mix_PausedMusic. En caso negativo reproducimos desde el principio la canción con Mix_PlayMusic indicándole el número de repeticiones y comprobando los posibles errores.

void Song::play(int loop) {
    if(Mix_PausedMusic())
            Mix_ResumeMusic();
    else{
        if(Mix_PlayMusic(_song, loop) == -1){
            Ogre::LogManager::getSingleton().logMessage("Song::play(): error al reproducir");
            throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                                   "No se puede reproducir la música",
                                   "Song::play()"));
        }
    }
}

Para implementar el método pause simplemente añadimos una llamada a la función Mix_PauseMusic.

void Song::pause() {
    Mix_PauseMusic();
}

Si, en cambio, deseamos detener la reproducción por completo, llamaríamos a stop que internamente hace una llamada a Mix_HaltMusic.

void Song::stop() {
    Mix_HaltMusic();
}

El método fadeIn es muy similar a play, simplemente hace una llamada a Mix_FadeInMusic indicándole la canción, la duración del efecto y el número de iteraciones. Por supuesto, captura los posibles errores e informa de ellos.

void Song::fadeIn(int ms, int loop) {
    if (Mix_FadeInMusic(_song, ms, loop) == -1) {
            Ogre::LogManager::getSingleton().logMessage("Song::fadeIn(): error al entrar suavizado");
            throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                                   "No se puede reproducir la música con efecto suavizado",
                                   "Song::fadeIn()"));
    }
}

Por su parte, el método fadeOut hace algo muy similar al anterior aunque en esta ocasión se llama a Mix_FadeOutMusic.

void Song::fadeOut(int ms) {
    if (Mix_FadeOutMusic(ms) == -1) {
            Ogre::LogManager::getSingleton().logMessage("Song::fadeOut(): error al salir suavizado");
            throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                                   "No se puede parar la música con efecto suavizado",
                                   "Song::fadeOut()"));
    }
}

Por último, el método estático isPlaying hace una llamada interna a Mix_PlayingMusic.

bool Song::isPlaying() {
    return Mix_PlayingMusic()? true : false;
}

La clase SongPtr

El puntero inteligente SongPtr hereda de SharedPtr<Song> y sólo debe implementar varios constructores y su operador de asignación. El constructor predeterminado está vacío mientras que el que toma un puntero a Song y el de copia delegan en el de la clase padre.

class SongPtr: public Ogre::SharedPtr<Song> {
    public:
        SongPtr(): Ogre::SharedPtr<Song>() {}
        explicit SongPtr(Song* m): Ogre::SharedPtr<Song>(m) {}
        SongPtr(const SongPtr &m): Ogre::SharedPtr<Song>(m) {}
        SongPtr(const Ogre::ResourcePtr &r);
        SongPtr& operator= (const Ogre::ResourcePtr& r);
};

El constructor que toma un puntero inteligente a un recurso genérico puede parecer complejo pero simplemente se encarga de convertir tipos (casting), copiar ciertos parámetros e incrementar el contador de referencias pUseCount. Se han de incluir las directivas de Ogre para garantizar la exclusión mutua.

SongPtr::SongPtr(const Ogre::ResourcePtr &r): Ogre::SharedPtr<Song>() {
    // Si r no es un recurso válido, no hacemos nada
    if (r.isNull())
        return;
 
    OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
    OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
 
    pRep = static_cast<Song*>(r.getPointer());
    pUseCount = r.useCountPointer();
    useFreeMethod = r.freeMethod();
 
    if (pUseCount)
        ++(*pUseCount);
 
}

El operador de asignación es prácticamente idéntico al constructor a partir de un recurso genérico

SongPtr& SongPtr::operator= (const Ogre::ResourcePtr& r) {
    if (pRep == static_cast<Song*>(r.getPointer()))
        return *this;
 
    release();
 
    if (r.isNull())
        return *this;
 
    OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
    OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
 
    pRep = static_cast<Song*>(r.getPointer());
    pRep = static_cast<Song*>(r.getPointer());
    pUseCount = r.useCountPointer();
    useFreeMethod = r.freeMethod();
 
    if (pUseCount)
        ++(*pUseCount);
 
    return *this;
}

La clase SongManager

La clase SongManager será la encargada de gestionar los recursos de tipo Song en nuestro sistema. No añade más métodos de los imprescindibles al heredar de ResourceManager y Singleton que ya comentamos en la sección anterior. Iremos detallando la implementación de dichos métodos a continuación.

class SongManager: public Ogre::ResourceManager,
                   public Ogre::Singleton<SongManager> {
    public:
        SongManager();
        virtual ~SongManager();
        virtual SongPtr load(const Ogre::String& name, const Ogre::String& group = Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
        static SongManager& getSingleton();
        static SongManager* getSingletonPtr();
 
    protected:
        Ogre::Resource* createImpl(const Ogre::String& name,
                                   Ogre::ResourceHandle handle,
                                   const Ogre::String& group,
                                   bool isManual,
                                   Ogre::ManualResourceLoader* loader,
                                   const Ogre::NameValuePairList* createParams);
};

El constructor de SongManager es bastante sencillo, en primer lugar define el tipo de recurso que gestiona ("Song"), establece su prioridad con respecto a otros gestores (le hemos dado una prioridad baja) y se registra en el sistema como gestor de recursos para dicho tipo.

SongManager::SongManager() {
    // Tipo de recurso a gestionar
    mResourceType = "Song";
 
    // Prioridad
    mLoadOrder = 30.f;
 
    // Registramos el gestor como gestor de "Song"
    Ogre::ResourceGroupManager::getSingleton()._registerResourceManager(mResourceType, this);
}

En el destructor lo único necesario es informar a Ogre que ya no existe el gestor de recursos para el tipo "Song".

SongManager::~SongManager() {
    // Eliminamos el registro del gestor
    Ogre::ResourceGroupManager::getSingleton()._unregisterResourceManager(mResourceType);
}

El método load es el que más utilizarán los programadores que deban cargar canciones. En primer lugar trata de recuperar el recurso con el nombre dado, si es nulo llama al método create que internamente utilizará createImpl. Como el usuario ha pedido el recurso para utilizarlo debemos cargarlo antes de devolverlo.

SongPtr SongManager::load(const Ogre::String& name, const Ogre::String& group) {
    SongPtr songPtr = getByName(name);
 
    if (songPtr.isNull()) 
        songPtr = create(name, group);
 
    songPtr->load();
 
    return songPtr;
}

Los métodos getSingleton y getSingletonPtr son muy sencillos, simplemente comprueban si la instancia es válida y la devuelven en forma de referencia o puntero respectivamente.

SongManager& SongManager::getSingleton() {
    assert(ms_Singleton);
    return (*ms_Singleton);
}
 
SongManager* SongManager::getSingletonPtr() {
    assert(ms_Singleton);
    return ms_Singleton;
}

El método createImpl es el que realmente se encarga de crear un nuevo recurso y devolverlo en forma de puntero. En este caso utilizamos el operador new para crear un objeto de tipo Song con los parámetros dados. En este momento no se cargaría definitivamente el recurso, para ello habría que llamar a load.

Ogre::Resource* SongManager::createImpl(const Ogre::String& name,
                                          Ogre::ResourceHandle handle,
                                          const Ogre::String& group,
                                          bool isManual,
                                          Ogre::ManualResourceLoader* loader,
                                          const Ogre::NameValuePairList* createParams) {
    return new Song(this, name, handle, group, isManual, loader);
}

Con esto hemos conseguido integrar la reproducción de pistas de audio que nos proporciona SDL mixer con el sistema de gestión de recursos. Ahora podremos cargar y utilizar objetos Song de forma sencilla y rápida tal y como veremos en el ejemplo final.


Ejemplo: efectos de sonido

En esta sección crearemos un nuevo tipo de recurso que llamaremos SoundFX para poder reproducir efectos de sonido: explosiones, disparos, aplausos, etc. En definitiva estamos hablando de pistas de audio de corta duración. Por supuesto, internamente haremos uso de la biblioteca SDL mixer, la que nos permitirá reproducir efectos en formato WAV entre otros. Al igual que con Song, iremos repasando las clases y métodos necesarios aunque esta vez no será necesario pararse en cada detalle, ya conoces el procedimiento.

La clase SoundFX

La clase SoundFX hereda de Resource y representa el nuevo tipo de recurso. Es bastante más sencilla que Song ya que sólo cuenta con un método si no tenemos en cuenta los que derivan de Resource:

  • play: reproduce el efecto de sonido, es posible indicarle el número de veces que queremos reproducirlo.

Como atributos privados, tenemos:

  • Mix_Chunk* _sound: puntero a la estructura de mixer que guarda la información sobre el efecto de sonido.
  • Ogre::String _path: ruta completa al efecto de sonido.
  • size_t _size: tamaño en bytes del efecto de sonido.
class SoundFX: public Ogre::Resource {
    public:
        SoundFX(Ogre::ResourceManager* creator,
               const Ogre::String& name,
               Ogre::ResourceHandle handle,
               const Ogre::String& group,
               bool isManual = false,
               Ogre::ManualResourceLoader* loader = 0);
 
        ~SoundFX();
        int play(int loop = 0);
 
    protected:
        void loadImpl();
        void unloadImpl();
        size_t calculateSize() const;
 
    private:
        Mix_Chunk* _sound;
        Ogre::String _path;
        size_t _size;
};

Al igual que el equivalente de Song, en el constructor simplemente le indicamos a Ogre que existe un nuevo tipo de recurso (si no existía ya) del tipo "SoundFX" e inicializamos los parámetros.

SoundFX::SoundFX(Ogre::ResourceManager* creator,
                 const Ogre::String& name,
                 Ogre::ResourceHandle handle,
                 const Ogre::String& group,
                 bool isManual,
                 Ogre::ManualResourceLoader* loader):
                 Ogre::Resource(creator, name, handle, group, isManual, loader){
 
    // Creamos el tipo de recurso si no existía ya
    createParamDictionary("SoundFX");
 
    // Damos valor a los atributos
    _sound = 0;
    _path = "";
    _size = 0;
}

En el destructor hacemos una llamada al método unload que, si recuerdas, debe llamar a su vez al método unloadImpl que se implementa más abajo.

SoundFX::~SoundFX() {
    unload();
}

En el método loadImpl buscamos la ruta completa hacia el recurso y lanzamos una excepción acompañada de un mensaje de log en caso de fracasar. Posteriormente cargamos el efecto de sonido gracias al método Mix_LoadWav y finalmente estimamos su tamaño recorriendo el fichero binario de audio.

void SoundFX::loadImpl() {
    // Buscar la ruta del fichero
    Ogre::FileInfoListPtr info;
    info = Ogre::ResourceGroupManager::getSingleton().findResourceFileInfo(mGroup, mName);
    for (Ogre::FileInfoList::iterator i = info->begin(); i != info->end(); ++i) {
        _path = i->archive->getName() + "/" + i->filename;
    }
 
    if (_path == "") {
            Ogre::LogManager::getSingleton().logMessage("SoundFX::loadImpl(): no se encuentra el recurso");
            throw (Ogre::Exception(Ogre::Exception::ERR_FILE_NOT_FOUND,
                                   "No se encuentra el fichero del efecto",
                                   "SoundFX::loadImpl()"));
    }
 
    // Cargar el efecto
    if ((_sound = Mix_LoadWAV(_path.c_str())) == NULL) {
            Ogre::LogManager::getSingleton().logMessage("SoundFX::loadImpl(): no se ha podido cargar el efecto");
            throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                                   "No se ha podido cargar el efecto",
                                   "SoundFX::loadImpl()"));
    }
 
    // Calculamos el tamaño
    std::ifstream stream;
    char byteBuffer;
 
    stream.open(_path.c_str(), std::ios_base::binary);
 
    while (stream >> byteBuffer)
        ++_size;
 
    stream.close();
}

Cuando llegue la hora de liberar la memoria del efecto, simplemente empleamos la función Mix_FreeChunk de SDL mixer. No es necesario hacer nada más ya que no reservamos memoria adicional.

void SoundFX::unloadImpl() {
    if (_sound)
        Mix_FreeChunk(_sound);
}

Al igual que en la versión de Song, cuando se desee conocer una estimación del tamaño, devolvemos el atributo privado _size.

size_t SoundFX::calculateSize() const {
    return _size;
}

El método play es el único que añade SoundFX con respecto a Resource. La función Mix_PlayChannel reproducimos el efecto en el primer canal de audio disponible y, si se produce un error, lanzamos una excepción acompañada de mensaje de log.

int SoundFX::play(int loop) {
    int channel;
 
    if ((channel = Mix_PlayChannel(-1, _sound, loop)) == -1) {
        Ogre::LogManager::getSingleton().logMessage("SoundFX::play(): error al reproducir el efecto");
        throw (Ogre::Exception(Ogre::Exception::ERR_INTERNAL_ERROR,
                               "No se puede reproducir el efecto",
                               "SoundFX::play()"));
    }
 
    return channel;
}

La clase SoundFXPtr

De nuevo, es el momento de implementar el puntero inteligente para los efectos de sonido. En esta ocasión hereda de SharedPtr<SoundFX> y también cuenta con constructor predeterminado, de copia, a partir de un recurso genérico y operador de asignación. El primero está vacío, y los dos siguientes delegan en el constructor de la clase padre.

class SoundFXPtr: public Ogre::SharedPtr<SoundFX> {
    public:
        SoundFXPtr(): Ogre::SharedPtr<SoundFX>() {}
        explicit SoundFXPtr(SoundFX* s): Ogre::SharedPtr<SoundFX>(s) {}
        SoundFXPtr(const SoundFXPtr& s): Ogre::SharedPtr<SoundFX>(s) {}
        SoundFXPtr(const Ogre::ResourcePtr& r);
        SoundFXPtr& operator= (const Ogre::ResourcePtr& r);
};

En el constructor que toma un puntero inteligente a un recurso genérico se hace una conversión de tipos, se copian los atributos del recurso y se incrementa el contador de referencias. Al igual que en Song, se requiere el uso de los mecanismos de exclusión mutua de Ogre.

SoundFXPtr::SoundFXPtr(const Ogre::ResourcePtr& r) {
    // Si r no es un recurso válido, no hacemos nada
    if (r.isNull())
        return;
 
    OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
    OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
 
    pRep = static_cast<SoundFX*>(r.getPointer());
    pUseCount = r.useCountPointer();
    useFreeMethod = r.freeMethod();
 
    if (pUseCount)
        ++(*pUseCount);
}

El operador de asignación utiliza el mismo mecanismo que el constructor a partir de un puntero inteligente a un recurso genérico.

SoundFXPtr& SoundFXPtr::operator= (const Ogre::ResourcePtr& r) {
    if (pRep == static_cast<SoundFX*>(r.getPointer()))
        return *this;
 
    release();
 
    if (r.isNull())
        return *this;
 
    OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
    OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
 
    pRep = static_cast<SoundFX*>(r.getPointer());
    pUseCount = r.useCountPointer();
    useFreeMethod = r.freeMethod();
 
    if (pUseCount)
        ++(*pUseCount);
 
    return *this;
}

La clase SoundFXManager

Para gestionar recursos SoundFX emplearemos el gestor SoundFXManager el cual, además de incluir los métodos heredados de ResourceManager y Singleton, añade la gestión de canales disponibles. A continuación, ofrecemos detalles sobre la implementación de cada método.

class SoundFXManager: public Ogre::ResourceManager,
                      public Ogre::Singleton<SoundFXManager> {
    public:
        SoundFXManager();
        virtual ~SoundFXManager();
        virtual SoundFXPtr load(const Ogre::String& name,
                                const Ogre::String& group = Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
        static SoundFXManager& getSingleton();
        static SoundFXManager* getSingletonPtr();
        static int getAvailableChannels();
 
    protected:
        Ogre::Resource* createImpl(const Ogre::String& name,
                                   Ogre::ResourceHandle handle,
                                   const Ogre::String& group,
                                   bool isManual,
                                   Ogre::ManualResourceLoader* loader,
                                   const Ogre::NameValuePairList* createParams);
 
    private:
        static int numChannels;
};

En el constructor registramos al gestor como el encargado de los recursos del tipo "SoundFX". Le asignamos una prioridad de 30 con respecto al resto de gestores y reservamos canales de reproducción mediante el método Mix_AllocateChannels.

SoundFXManager::SoundFXManager() {
    // Tipo de recursos que gestiona
    mResourceType = "SoundFX";
 
    // Prioridad de carga
    mLoadOrder = 30.f;
 
    // Registramos el nuevo ResourceManager
    Ogre::ResourceGroupManager::getSingleton()._registerResourceManager(mResourceType, this);
 
    // Reservamos 32 canales de audio
    Mix_AllocateChannels(numChannels);
}

En el destructor, como ya hemos hecho anteriormente, debemos indicarle al sistema que no existe un gestor para el tipo "SoundFX".

SoundFXManager::~SoundFXManager() {
    // Borramos el gestor de la lista de ResourceManager
    Ogre::ResourceGroupManager::getSingleton()._unregisterResourceManager(mResourceType);
}

El método load será el empleado por los usuarios que deseen cargar efectos de sonido. Simplemente toma el puntero inteligente según el nombre del recurso Si no está completamente listo para su uso, llama a su método create que internamente hace uso de createImpl. Posteriormente, cargamos el recurso y devolvemos el puntero inteligente.

SoundFXPtr SoundFXManager::load(const Ogre::String& name, const Ogre::String& group) {
    // Obtenemos el SoundFXPtr
    SoundFXPtr soundFXPtr = getByName(name);
 
    // Si no está creado, lo creamos
    if (soundFXPtr.isNull())
        soundFXPtr = create(name, group);
 
    // Cargamos el recurso
    soundFXPtr->load();
 
    return soundFXPtr;
}

Los métodos getSingleton y getSingletonPtr nos permiten acceder a la referencia o del gestor respectivamente desde cualquier punto del sistema. Su implementación es muy similar.

SoundFXManager& SoundFXManager::getSingleton() {
    assert(ms_Singleton);
    return (*ms_Singleton);
}
 
SoundFXManager* SoundFXManager::getSingletonPtr() {
    assert(ms_Singleton);
    return ms_Singleton;
}

El método estático getAvailableChannels nos devuelve el número de canales que hemos reservado para la reproducción de efectos de sonido.

int SoundFXManager::getAvailableChannels() {
    return numChannels;
}

Por último, el método createImpl se llama automáticamente por el sistema al tratar de cargar un recurso no creado. Hacemos uso del operador new para crear un nuevo efecto de sonido.

Ogre::Resource* SoundFXManager::createImpl(const Ogre::String& name,
                                           Ogre::ResourceHandle handle,
                                           const Ogre::String& group,
                                           bool isManual,
                                           Ogre::ManualResourceLoader* loader,
                                           const Ogre::NameValuePairList* createParams) {
    return new SoundFX(this, name, handle, group, isManual, loader);
}


Ejemplo final

Diagrama de clases del ejemplo de sonido
Ejemplo de extensión del sistema de recursos, audio
Aplicación que carga un escenario de monólogos y simula el papel del regidor reoproduciendo efectos de sonido.

En este ejemplo vamos a desarrollar una aplicación que carga una escena iluminada, compuesta de varios elementos: un escenario, unas paredes, una silla, un micrófono y un cartel. Simula una especie de programa de monólogos y el cartel contiene las instrucciones para el regidor. Si pulsamos las teclas indicadas reproduciremos una u otra música o efecto de sonido:

  • 1: sintonía de comienzo del programa.
  • 2: sintonía del contenido del programa.
  • 3: sintonía del final del programa.
  • 4: aplausos de un público ficticio.
  • 5: risas enlatadas.
  • 6: abucheos del público ficticio.

El diagrama adjunto muestra las clases que participan en la aplicación. Al conjunto de clases del sistema de sonido que hemos desarrollado anteriormente se unen las clases AplicacionOgre y EscenaSimple. La primera se encarga de inicializar Ogre, OIS y SDL junto a su extensión mixer. Además, es la encargada de crear al inicio y destruir al cierre los gestores de efectos de sonido y música. La clase EscenaSimple hereda de AplicacionOgre y es la encargada de configurar la cámara, los elementos de la escena, la iluminación y el audio. Contiene la lógica de juego es la encargada de reproducir la pista indicada en función de las pulsaciones del teclado.

Recuerda que debes preparar los plugins (pues no vienen incluidos en el paquete). Puedes encontrar más información sobre esto en "Creación de un entorno de trabajo multiplataforma". Los plugins necesarios son:

  • RenderSystem_GL
Ejemplo de extensión del sistema de recursos y sonido en ejecución

Conclusiones

Tras este extenso artículo habrás aprendido a extender el sistema de gestión de recursos de Ogre de forma efectiva. Además, los ejemplos de música y efectos de sonido te deben haber servido para conocer a fondo el procedimiento. Si no conocías SDL, ahora eres consciente de su utilidad para el desarrollo de videojuegos y su sencillez a la hora de integrarse con otros sistemas de manera auxiliar. Cuando trabajes en un proyecto, deberías valorar integrar los recursos adicionales (niveles, audio, eventos) dentro de la gestión de recursos de Ogre.

Herramientas personales