Resources and ResourceManagers         Outlines in detail the process by which resources are loaded, unloaded, reloaded and destroyed. Shows how to create a new resource type, and a manager to go with it

This first part of this article will outline in detail the process by which resources are loaded, unloaded, reloaded and destroyed. In the second half, we will create a new resource type, and a manager to go with it.

Any problems you encounter during working with this tutorial should be posted in the Help Forum(external link).


Note: 24th Sep 2013 - The code as it stands does not compile with the very latest revisions of Ogre v1-9 from bitbucket.

The Lifecycle of a Resource

The OGRE API documentation explains the basic lifecycle of a resource, but is slightly washy with some of the concepts. Hopefully, things will be made clearer here.

Terminology

The following terms will be used to distinguish the various stages of loading that a resource may be in:

Unknown: OGRE is not aware of the resource. Its filename is stored in a ResourceGroup, but OGRE has no idea what to do with it.

Declared: The resource has been flagged for creation, either directly or as a side-effect of some other action. Ogre knows what type of resource it is, and what to do with it when the time comes to create it.

Created: OGRE has created an empty instance of the resource, and added it to the relevant ResourceManager.

Loaded: The created instance has been fully loaded, and the resource's full data now resides in memory. This is typically the stage at which the resource's file is actually accessed. You do not want to access the file in the Creation stage.

Resources do not take much space until they are loaded, therefore there is no harm in adding all of your resources to the resource manager first and then loading/unloading them as needed in your application.

Resource Creation, from the very beginning

  1. OGRE's native ResourceManagers are created in Root::Root.
  2. The first thing that needs to be done is to specify resource locations. This is done by calling ResourceGroupManager::addResourceLocation. This function does several things:
    1. Creates the specified ResourceGroup if this hasn't been done already.
    2. Creates a new Archive instance of the type specified
    3. Creates a new ResourceLocation, adds the Archive to it, and then adds the ResourceLocation to the ResourceGroup.
    4. The final step is to get a list of all the files in the Archive, and add them to a list in the ResourceGroup. After this step has completed, resources are in the Unknown stage.
  3. The next step is to manually declare resources. No resources have been declared as of yet, though many of them will be when the ResourceManager starts parsing scripts. If you wish to manually declare resources, call the ResourceGroupManager::declareResource function. At this point, any resources that have been manually declared are in the Declared stage. The rest are still Unknown.
  4. Next the ResourceGroups are initialized. This is done with ResourceGroupManager::initialiseResourceGroup or ResourceGroupManager::initialiseAllResourceGroups, the latter just calling the former for all ResourceGroups. This does the following:
    • Parses any scripts in the ResourceGroup. Scripts are defined by ResourceManagers which inherit from ScriptLoader. This may cause some resources to be Declared.
    • Creates any Declared resources.
    • The relevant ResourceManager creates a new instance of the resource, and adds it to itself. All resources are stored in ResourceManagers.
    • The resource is also inserted into an "ordered loading list". This allows resources to be loaded in a specific order, if you want to load an entire ResourceGroup at once. The load order for a resource is specified by its ResourceManager.
    • At this point, any Declared resources are now in the Created stage.
  5. Finally, we're done with initialization. No resources are loaded yet, but that's fine, because we're not using any. The final step - the transition from Created to Loaded - can come about in the following ways:
    • The resource was used. For example, an Entity was created which needed a specific mesh. Obviously, once a resource has been loaded in this way, it will not be loaded again if another Entity needs it. If the resource is currently in the Unknown state, it will be created and fully loaded.
    • ResourceGroupManager::loadResourceGroup is called - any Created resources are loaded.
    • The relevant ResourceManager's load method is called. This can be used to load resources which have not yet been Created, because it will automatically Create them for you first, if necessary.
    • The resource is loaded directly, by obtaining a pointer to it, and calling its load method. You can only do this if the resource is in the Created stage, of course.
    • At this point, Loaded resources are ready to use straight away.


Note: If you have created any custom ResourceManagers, you must initialize them before manually declaring resources or some of them may not be found.

In your application, this generally boils down to the following sequence of events:

  1. Create the Root object.
  2. Call ResourceGroupManager::addResourceLocation repeatedly until you have added all resource locations.
  3. Create any custom ResourceManager objects you have made and register them using ResourceGroupManager::_registerResourceManager. You should also register any ScriptLoader objects with the ResourceGroupManager::_registerScriptLoader function.
  4. Manually declare any resources you require with the ResourceGroupManager::declareResource function.
  5. Call the appropriate initialization function for your resource groups. Either use ResourceGroupManager::initialiseResourceGroup for a single group or call ResourceGroupManager::initialiseAllResourceGroups to initialize everything at once.

Resource Unloading and Destruction

  • ResourceManager::unload reverts a resource from Loaded to Created.
  • To completely remove a resource, call ResourceManager::remove. This returns the resource all the way back to the Unknown stage, from whichever stage it was in previously. You can get a pointer to the resource with ResourceManager::getByName and unload or remove it manually, if you wish.
  • Any existing resources are removed when a ResourceManager is destructed.

Reloading Resources

Reloading resources is a very useful feature for editors. Essentially, the resource is unloaded, and then loaded. It moves from Loaded, to Created and then back to Loaded again. Resources must be in the Loaded stage to be reloaded.

  • ResourceManager::reloadAll reloads all resources of one type.
  • Resources can be individually reloaded with Resource::reload

Creating a new Resource type, and the accompanying ResourceManager

Now that we know how OGRE's resource system works, creating a new resource type is actually pretty easy. Your application will almost certainly use extra resources, be they sound files, XML or just plain text. In this example, we'll create a simple text file loader. The code is neatly compartmentalized, and easy to extend to any file type - the only thing that will need to change is the Resource::load method, and the TextFile resource's public interface to access the data we have loaded.

There are two caveats to this: script resources, and manual resource loaders. This example will use neither, but they will be explained.

The first file to create is TextFile.h. This declares our resource, TextFile, and creates a shared pointer implementation for it. This is what it looks like:

#ifndef __TEXTFILE_H__
 #define __TEXTFILE_H__
 
 #include <OgreResourceManager.h>
 
 class TextFile : public Ogre::Resource
 {
     Ogre::String mString;
 
 protected:
 
     // must implement these from the Ogre::Resource interface
     void loadImpl();
     void unloadImpl();
     size_t calculateSize() const;
 
 public:
 
     TextFile(Ogre::ResourceManager *creator, const Ogre::String &name, 
         Ogre::ResourceHandle handle, const Ogre::String &group, bool isManual = false, 
         Ogre::ManualResourceLoader *loader = 0);
 
     virtual ~TextFile();
 
     void setString(const Ogre::String &str);
     const Ogre::String &getString() const;
 };
 
 class TextFilePtr : public Ogre::SharedPtr<TextFile> 
 {
 public:
     TextFilePtr() : Ogre::SharedPtr<TextFile>() {}
     explicit TextFilePtr(TextFile *rep) : Ogre::SharedPtr<TextFile>(rep) {}
     TextFilePtr(const TextFilePtr &r) : Ogre::SharedPtr<TextFile>(r) {} 
     TextFilePtr(const Ogre::ResourcePtr &r) : Ogre::SharedPtr<TextFile>()
     {
         if( r.isNull() )
             return;
         // lock & copy other mutex pointer
         OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
         OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
         pRep = static_cast<TextFile*>(r.getPointer());
         pUseCount = r.useCountPointer();
         useFreeMethod = r.freeMethod();
         if (pUseCount)
         {
             ++(*pUseCount);
         }
     }
 
     /// Operator used to convert a ResourcePtr to a TextFilePtr
     TextFilePtr& operator=(const Ogre::ResourcePtr& r)
     {
         if(pRep == static_cast<TextFile*>(r.getPointer()))
             return *this;
         release();
         if( r.isNull() )
             return *this; // resource ptr is null, so the call to release above has done all we need to do.
         // lock & copy other mutex pointer
         OGRE_LOCK_MUTEX(*r.OGRE_AUTO_MUTEX_NAME)
         OGRE_COPY_AUTO_SHARED_MUTEX(r.OGRE_AUTO_MUTEX_NAME)
         pRep = static_cast<TextFile*>(r.getPointer());
         pUseCount = r.useCountPointer();
         useFreeMethod = r.freeMethod();
         if (pUseCount)
         {
             ++(*pUseCount);
         }
         return *this;
     }
 };
 
 #endif


Here is the accompanying .cpp file. We are using a simple string to store our data, and as such it needs no special initialisation. If you are using more complex objects, they must be initialised appropriately.

#include "TextFile.h"
 #include "TextFileSerializer.h"
 
 TextFile::TextFile(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)
 {
     /* If you were storing a pointer to an object, then you would set that pointer to NULL here.
     */
 
     /* For consistency with StringInterface, but we don't add any parameters here
     That's because the Resource implementation of StringInterface is to
     list all the options that need to be set before loading, of which 
     we have none as such. Full details can be set through scripts.
     */ 
     createParamDictionary("TextFile");
 }
 
 TextFile::~TextFile()
 {
     unload();
 }
 
 // farm out to TextFileSerializer
 void TextFile::loadImpl()
 {
     TextFileSerializer serializer;
     Ogre::DataStreamPtr stream = Ogre::ResourceGroupManager::getSingleton().openResource(mName, mGroup, true, this);
     serializer.importTextFile(stream, this);
 }
 
 void TextFile::unloadImpl()
 {
     /* If you were storing a pointer to an object, then you would check the pointer here,
     and if it is not NULL, you would destruct the object and set its pointer to NULL again.
     */
 
     mString.clear();
 }
 
 size_t TextFile::calculateSize() const
 {
     return mString.length();
 }
 
 void TextFile::setString(const Ogre::String &str)
 {
     mString = str;
 }
 
 const Ogre::String &TextFile::getString() const
 {
     return mString;
 }

You will have noticed the reference to "TextFileSerializer" in the includes. This is a helper class which does the actual loading. It is not vital, especially for a resource this simple, but it allows us to serialize an object without having to wrap a Resource around it, should we want to. The Serializer base class contains lots of useful utility functions. We won't use them, but we'll subclass from it anyway.

TextFileSerializer.h:

#ifndef __TEXTSERIALIZER_H__
 #define __TEXTSERIALIZER_H__
 
 #include <OgreSerializer.h>
 
 class TextFile; // forward declaration
 
 class TextFileSerializer : public Ogre::Serializer
 {
 public:
     TextFileSerializer();
     virtual ~TextFileSerializer();
 
     void exportTextFile(const TextFile *pText, const Ogre::String &fileName);
     void importTextFile(Ogre::DataStreamPtr &stream, TextFile *pDest);
 };
 
 #endif

TextFileSerializer.cpp:

#include "TextFileSerializer.h"
 #include "TextFile.h"
 
 TextFileSerializer::TextFileSerializer()
 {
 }
 
 TextFileSerializer::~TextFileSerializer()
 {
 }
 
 void TextFileSerializer::exportTextFile(const TextFile *pText, const Ogre::String &fileName)
 {
     std::ofstream outFile;
     outFile.open(fileName.c_str(), std::ios::out);
     outFile << pText->getString();
     outFile.close();
 }
 
 void TextFileSerializer::importTextFile(Ogre::DataStreamPtr &stream, TextFile *pDest)
 {
     pDest->setString(stream->getAsString());
 }

The last class we need to write is of course the TextFileManager.

TextFileManager.h:

#ifndef __TEXTFILEMANAGER_H__
 #define __TEXTFILEMANAGER_H__
 
 #include <OgreResourceManager.h>
 #include "TextFile.h"
 
 class TextFileManager : public Ogre::ResourceManager, public Ogre::Singleton<TextFileManager>
 {
 protected:
 
     // must implement this from ResourceManager's interface
     Ogre::Resource *createImpl(const Ogre::String &name, Ogre::ResourceHandle handle, 
         const Ogre::String &group, bool isManual, Ogre::ManualResourceLoader *loader, 
         const Ogre::NameValuePairList *createParams);
 
 public:
 
     TextFileManager();
     virtual ~TextFileManager();
 
     virtual TextFilePtr load(const Ogre::String &name, const Ogre::String &group);
 
     static TextFileManager &getSingleton();
     static TextFileManager *getSingletonPtr();
 };
 
 #endif

And finally, TextFileManager.cpp

#include "TextFileManager.h"
 
 template<> TextFileManager *Ogre::Singleton<TextFileManager>::ms_Singleton = 0;
 
 TextFileManager *TextFileManager::getSingletonPtr()
 {
     return ms_Singleton;
 }
 
 TextFileManager &TextFileManager::getSingleton()
 {  
     assert(ms_Singleton);  
     return(*ms_Singleton);
 }
 
 TextFileManager::TextFileManager()
 {
     mResourceType = "TextFile";
 
     // low, because it will likely reference other resources
     mLoadOrder = 30.0f;
 
     // this is how we register the ResourceManager with OGRE
     Ogre::ResourceGroupManager::getSingleton()._registerResourceManager(mResourceType, this);
 }
 
 TextFileManager::~TextFileManager()
 {
     // and this is how we unregister it
     Ogre::ResourceGroupManager::getSingleton()._unregisterResourceManager(mResourceType);
 }
 
 TextFilePtr TextFileManager::load(const Ogre::String &name, const Ogre::String &group)
 {
     TextFilePtr textf = getByName(name);
 
     if (textf.isNull())
         textf = create(name, group);
 
     textf->load();
     return textf;
 }
 
 Ogre::Resource *TextFileManager::createImpl(const Ogre::String &name, Ogre::ResourceHandle handle, 
                                             const Ogre::String &group, bool isManual, Ogre::ManualResourceLoader *loader, 
                                             const Ogre::NameValuePairList *createParams)
 {
     return new TextFile(this, name, handle, group, isManual, loader);
 }

ScriptLoader

Some resources, such as materials, are loaded from scripts, and in these cases, the ResourceManager will derive from ScriptLoader. It will then set up its 'script patterns' - the filetypes that it considers as scripts (*.material, *.compositor, etc) - in the constructor. It will also call
ResourceGroupManager::_registerScriptLoader at this point to register itself as a script loading ResourceManager. Later, when ResourceGroupManager::initialiseResourceGroup is called, any registered script files will be parsed.

If you wish to write a script-loading ResourceManager, you will need to derive from ScriptLoader, and implement the parseScript method. It is parseScript that gets called on the resources during ResourceGroupManager::initialiseResourceGroup, and this is where you should declare any resources that you want created.

ManualResourceLoader

When a resource is declared with ResourceGroupManager::declareResource, it may be given an optional ManualResourceLoader. ManualResourceLoaders can be used for resources which are not loaded from file - they may be created programmatically. Here is a simple example, using TextFile:

// Do not add this to the project
 class ManualTextFileLoader : public Ogre::ManualResourceLoader
 {
 public:
 
    ManualTextFileLoader() {}
    virtual ~ManualTextFileLoader() {}
 
    void loadResource(Ogre::Resource *resource)
    {
        TextFile *tf = static_cast<TextFile *>(resource);
        tf->setString("manually loaded");
    }
 };

The TextFile is then declared as follows:

// Do not add this to the project
 ManualTextFileLoader *mtfl = new ManualTextFileLoader;
 Ogre::ResourceGroupManager::getSingleton ().declareResource("hello.txt", "TextFile", "General", mtfl);

Usage

To use our new resource manager, we create an instance before Root::initialise or any ResourceGroups have been initialized.

TextFileManager *tfm = new TextFileManager();

And we destruct it when we shutdown - before we destroy the Ogre::Root object, of course. OGRE does not destruct it itself, it's your responsibility:

delete Ogre::ResourceGroupManager::getSingleton()._getResourceManager("TextFile");


The last thing we will do is look at an example of what we have created in action. Create a file called "hello.txt" and place it in a media directory where Ogre can find it. Since there really isn't a location for custom scripts, I suggest putting the file in "media/materials/scripts". Add the following to that text file:

Hello world!

Now create a file called main.cpp and add the following code to it. Be sure to read through the createScene function to see some of the things we can do with this:

#include <ExampleApplication.h>
 
 #include "TextFileManager.h"
 #include "TextFileSerializer.h"
 
 class TutorialApplication : public ExampleApplication
 {
 private:
     TextFileManager *mTFM;
 public:
     TutorialApplication()
         : mTFM(0)
     {
     }
 
     ~TutorialApplication()
     {
         delete mTFM;
     }
 
     void setupResources()
     {
         mTFM = new TextFileManager();
 
         // hello.txt will be created when initialiseResourceGroup is called
         ResourceGroupManager::getSingleton().declareResource("hello.txt", "TextFile");
         ExampleApplication::setupResources();
     }
 
     void createScene(void)
     {
         // Load the file, get the data
         TextFilePtr textfile = mTFM->load("hello.txt", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
         String str = textfile->getString();
 
         // Reload the file
         textfile->reload();
 
         // export the file
         TextFileSerializer serializer;
         serializer.exportTextFile(static_cast<TextFile *>(textfile.getPointer()), "hello.out.txt");
 
         // unload/remove the file
         mTFM->unload("hello.txt");
         mTFM->remove("hello.txt");
     }
 };
 
 #if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32
 #define WIN32_LEAN_AND_MEAN
 #include "windows.h"
 
 INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)
 #else
 int main(int argc, char **argv)
 #endif
 {
     // Create application object
     TutorialApplication app;
 
     try {
         app.go();
     } catch(Exception& e) {
 #if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32
         MessageBoxA(NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
 #else
         fprintf(stderr, "An exception has occurred: %s\n",
             e.getFullDescription().c_str());
 #endif
     }
 
     return 0;
 }


If you step through this function with a debugger, you will see that see that calling getString will indeed return the contents of hello.txt. While this may not be the most useful thing in its current state, you can easily expand this to create your own resources and resource loaders.


Alias: Advanced Tutorial 1
Alias: Advanced_Tutorial_1