Jump to content
  • Advertisement
Sign in to follow this  
hymerman

Another resource manager thread

This topic is 4449 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Howdy again, folks. I'm starting to write a resource manager, but unfortunately I'm being let down by my lack of C++ knowledge. I've searched high and low for articles on resource managers, and have found a fair few, but none of them seem quite 'right'. I'd like it to do the things a resource manager usually does; create resources, free resources when no longer needed, and keep track of what's been created already. If something's already been loaded, a pointer to the first instance can be returned rather than a new copy. All standard fare, really. The resources are all derived from an abstract base class 'IResource', which contains pure virtual methods for loading and unloading. All resources load given a path to a file, and are responsible for all loading and processing of the file they're given. The bit where my design differs from the currently existing resource managers I can find is that I'd like it to be type-safe, and to do as little casting as possible. I figure I'll accomplish this by using templated methods within the manager, though anyone who wishes to shoot this idea down go ahead! At the moment, I have something like this:
class ResourceManager {
class ResourceManager {
public:
	typedef IResource base_type;

public:

	ResourceManager() {}
	~ResourceManager() {}

	template<typename T>
	void support() {
		// Somehow allow support for resources of type T
		// make sure T is a subclass of base_type
	}

	template<typename T>
	boost::shared_ptr<T> create(const std::string& path) {
		boost::weak_ptr<base_type> res;
		if(res = createdResources.find(path)) {
			return res;
		} else {
			res = boost::shared_ptr<T>(new T(path));
			createdResources[path] = res;
			return res;
			// How can res be guaranteed to be a base_type? Only resources can be created!
		}
	}

private:
	std::map<std::string, boost::weak_ptr<base_type>> createdResources;
	// Will need some way of storing what resources can be created
};


I can probably fiddle it so it works vaguely, but it will be horribly hackish and very susceptible to abuse and breakage. Now, my question to you fellas is; you can see what I'm trying to do, what do I need to do to actually accomplish this?

Share this post


Link to post
Share on other sites
Advertisement
Quote:
Original post by hymerman
// How can res be guaranteed to be a base_type? Only resources can be created!


template<typename T>
boost::shared_ptr<T>
create(
const std::string& path,
typename boost::enable_if
<
typename boost::is_base_and_derived<base_type,T>::value
>::type* fake = 0)
{
// ...
};


Didn't really tried to compile, may contain some bugs.

-------------
And a few more notes (still on topic, I hope):

Quote:
Original post by hymerman
The resources are all derived from an abstract base class 'IResource', which contains pure virtual methods for loading and unloading.


How are you planning to use that polymorphic behaviour? You are going to use a particular resource as its real type (BaseModel, BaseTexture, BaseSound, etc.), not as IResource. So the only place to use the IResource would be in the manager itself. What for?

IMO it would be more convenient to write a separate manager for each resource type. Each with its own map. You'd want to write separate loading/unloading routines anyway, and most likely enclose them in separate classes and/or even modules. Also, with templates and resource created by new Res(whatever) you'd are dropping the possiblility of subclassing the resources, unless th end-user knows about it. For example, the user wants to load a BaseSound from "sound.mid", so you return SoundMid (SoundMid : public BaseSound), or SoundWav for "sound.wav". Just a possibility, but that is why people are using factories (loaders) in the first place.

Share this post


Link to post
Share on other sites
I would go a totally different direction myself. Note that I don't require any special base class. In fact, it can store anything that can be constructed from a const std::string.


class ResourceManager
{
class a_Resource
{
public:
a_Resource() : ref_count(0) {}
virtual ~a_Resource() {}
int ref_count;
friend void intrusive_ptr_add_ref(a_Resource* e) { ++e->ref_count; }
friend void intrusive_ptr_release(a_Resource* e) { if (--e->ref_count == 0) delete e; }
};

template <typename ValueType>
class T_Resource: public a_Resource
{
public:
ValueType* value;
T_Resource(ValueType* val) : value(val) {}
virtual ~T_Resource() { delete value; }
};

typedef boost::intrusive_ptr<a_Resource> Resource;

typedef std::map<std::string,Resource> filename_to_resource;

filename_to_resource resources;

typedef filename_to_resource::iterator iterator;
iterator begin() { return resources.begin(); }
iterator end() { return resources.end(); }
iterator find(const std::string& filename) { return resources.find(filename); }

public:

class LoadFailed: public std::runtime_error
{
public:
virtual const char * what() const throw() { return "Resource load failure"; }
virtual ~LoadFailed() throw() {}
};

template<typename ResourceType>
ResourceType* load(const std::string& filename)
{
try
{
Resource r = new T_Resource<ResourceType>( new ResourceType(filename) );
resources[filename] = r;
return dynamic_cast<T_Resource<ResourceType>*>(r.get())->value;
}
catch (LoadFailed)
{
return 0;
}
}

template<typename ResourceType>
ResourceType* get(const std::string& filename)
{
iterator I = find(filename);
if (I == end()) return load<ResourceType>(filename);
T_Resource<ResourceType>* t_r = dynamic_cast<T_Resource<ResourceType>*>(I->second.get());
if (!t_r) return 0;
return t_r->value;
}
};



One little gotcha: If you load another resource with the same filename (or call load twice with the same file) you invalidate any pointers to the original resource.

Also, it is the responsibility of the resource class to do the cleanup. Example..


class SampleResource
{
public:
SampleResource(const std::string& filename)
{
//load resource...
//if load failed; throw ResourceManager::LoadFailed().
}

~SampleResource()
{
//unload resource...
}
};



This requires you to use all the features most uninformed C++ users turn off. That is, exceptions and RTTI.

Share this post


Link to post
Share on other sites
I just recently implemented mine in C#. You could probably figure out how to translate it quite easily. Anyways, if anyone has any criticisms of how I am doing it now, please feel free to point them out, as I pretty much just wrote this off the top of my head.

Anyways, some of the nifty features include the ability to autmatically refresh the contents of the resources every few seconds, which means I can have my game running, open up one of my scripts or art files, make changes and hit save, and they will instantly show up in-game.

IResource interface, which all resources must derive from and implement:

// <summary>
/// Represents an interface from which all resources managed by a
/// ResourceManager class must derive.
/// </summary>
public interface IResource
{
/// <summary>
/// When implemented, gets the resource's filename.
/// </summary>
string Filename
{
get;
}

/// <summary>
/// When implemented, creates a new blank resource.
/// </summary>
void Create();

/// <summary>
/// When implemented, loads a resource from file.
/// </summary>
/// <param name="filename">The name of the file from which the resource
/// will be loaded.</param>
/// <exception cref="Exception">Should throw an Exception if the
/// file cannot be loaded.
/// </exception>
void Load(string filename);

/// <summary>
/// When implemented, releases the resource and its memory.
/// </summary>
void Release();
}




And the actual resource manager itself, which uses C# generics to do the trick quite nicely:

/// <summary>
/// Manages resources loaded by an application.
/// </summary>
/// <typeparam name="T">The resource that will be handled
/// by the instance of the ResourceManager.
/// </typeparam>
public class ResourceManager<T> where T : IResource, new()
{
// variables
Dictionary<string, T> resources = null; // the resource table
Dictionary<string, DateTime> lastWriteTable; // the last write table
int updateInterval = 0; // updating interval
Timer timer; // update timer

/// <summary>
/// Creates a new ResourceManager class.
/// </summary>
public ResourceManager()
{
// create a new resource table
resources = new Dictionary<string, T>();
lastWriteTable = new Dictionary<string, DateTime>();
timer = new Timer();
timer.Stop();
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
}

/// <summary>
/// Occurs when the timer event meets the required interval.
/// </summary>
/// <param name="sender">Object sender.</param>
/// <param name="e">Event arguments.</param>
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
// call the refresh function
Refresh();
}

/// <summary>
/// Creates a blank resource and adds it to the manager.
/// </summary>
/// <param name="name">The name that will be associated with the resource.</param>
/// <returns>The newly created item.</returns>
/// <remarks>
/// If a resource with the requested name already exists, it will be
/// returned instead of creating the new item.
/// </remarks>
public T Create(string name)
{
// if the item already exists then return it
if (resources.ContainsKey(name))
return resources[name];

// otherwise, create the new item and return it
T item = new T();
item.Create();
resources.Add(name, item);
lastWriteTable.Add(name, DateTime.Now);
return resources[name];
}

/// <summary>
/// Adds a new resource to the list.
/// </summary>
/// <param name="filename">The name of the file to load.</param>
/// <param name="name">The name that will be associated with the given resource.</param>
/// <returns>The newly added item, or null if it cannot be loaded.</returns>
/// <exception cref="Exception">Throws an Exception if the resource cannot
/// be loaded.</exception>
/// <remarks>
/// If the requested file has previously been loaded by this manager,
/// it will return the previous item instead of loading it again.
/// </remarks>
public T Add(string filename, string name)
{
// call the modified version
return Add(filename, name, false);
}

/// <summary>
/// Adds a new resource to the list.
/// </summary>
/// <param name="filename">The name of the file to load.</param>
/// <param name="name">The name that will be associated with the given resource.</param>
/// <param name="forceReload">True to force a reload, false to load normally.</param>
/// <returns>The newly added item, or null if it cannot be loaded.</returns>
/// <exception cref="Exception">Throws an Exception if the resource
/// cannot be loaded.
/// </exception>
/// <remarks>
/// If the requested file has previously been loaded by this manager,
/// and forceReload is false, this function will return the previous item
/// instead of loading it again. If forceReload is true, then this function
/// will load a fresh copy from file. If names conflict, then this function
/// will modify the name by adding _1, _2, etc. to the end of the name,
/// so that all the names will be unique.
/// </remarks>
public T Add(string filename, string name, bool forceReload)
{
// check if we aren't forcing a reload
if (!forceReload)
{
// check if this item has already been loaded
if (resources.ContainsKey(name))
return resources[name];

// otherwise load a new copy
T item;
try
{
// create the item and add it to the list
item = new T();
item.Load(filename);
resources.Add(name, item);
lastWriteTable.Add(name, File.GetLastWriteTime(filename));

return resources[name];
}
catch
{
// an error occured!
throw new Exception("Could not load resource " + name + " from file " + filename + "!");
}
}
else
{
// if the item has already been loaded change the name
// until we find a good new valid one
string tempname = name;
int counter = 1;
while (resources.ContainsKey(tempname))
{
// remove the last two and add the new ending
tempname = name + "_" + counter.ToString();
counter++;
}

// load the new copy
T item;
try
{
// create the item and add it to the list
item = new T();
item.Load(filename);
resources.Add(tempname, item);
lastWriteTable.Add(tempname, File.GetLastWriteTime(filename));
return resources[name];
}
catch
{
// an error occured!
throw new Exception("Could not load resource " + tempname + " from file " + filename + "!");
}
}
}

/// <summary>
/// Unloads the specified resource from the manager.
/// </summary>
/// <param name="name">The name of the resource that will be removed
/// from the manager.</param>
/// <exception cref="Exception">Throws an Exception if the specified
/// resource has not been loaded by this manager.
/// </exception>
public void Remove(string name)
{
// if we have an item associated with the given name, then
// release it
if (resources.ContainsKey(name))
{
// release the resource
resources[name].Release();
resources.Remove(name);
if (lastWriteTable.ContainsKey(name))
lastWriteTable.Remove(name);
}
else
throw new Exception("The resource " + name + " has not been loaded by this manager!");
}

/// <summary>
/// Unloads all files from the manager.
/// </summary>
public void Clear()
{
// unload each resource
foreach (KeyValuePair<string, T> pair in resources)
pair.Value.Release();

// clear the list
resources.Clear();
lastWriteTable.Clear();
}

/// <summary>
/// Reopens each file in the list and refreshes its contents.
/// </summary>
/// <remarks>
/// This can be a very time consuming process, depending on how many
/// files need to be updated. Be careful of calling this in a processor
/// intensive area of your program.
/// </remarks>
public void Refresh()
{
// refresh each resource
foreach (KeyValuePair<string, T> pair in resources)
{
// check the write times and see if we need to reload
if (File.GetLastWriteTime(pair.Value.Filename) != lastWriteTable[pair.Key])
{
// reload
pair.Value.Release();
pair.Value.Load(pair.Value.Filename);
}
}
}

/// <summary>
/// Gets the resource with the specified name.
/// </summary>
/// <param name="name">The name of the item to retrieve.</param>
/// <returns>The item with the given filename, or null if it cannot be found.</returns>
public T this[string name]
{
get
{
// if the resource exists in our list, then return it. Otherwise,
// return null.
if (resources.ContainsKey(name))
return resources[name];
else
return default(T);
}
}

/// <summary>
/// Gets or sets the auto update interval.
/// </summary>
/// <remarks>
/// If the interval is set to be greater than zero, the manager will
/// refresh each of its files at every interval. This value is expressed
/// in milliseconds.
/// </remarks>
public int AutoUpdate
{
get { return updateInterval; }
set
{
// if the new value is different, start the timer
if (value != updateInterval)
{
updateInterval = value;
timer.Interval = updateInterval;

if (updateInterval == 0)
timer.Stop();
else
timer.Start();
}
}
}
}


Share this post


Link to post
Share on other sites
Quote:

The bit where my design differs from the currently existing resource managers I can find is that I'd like it to be type-safe, and to do as little casting as possible. I figure I'll accomplish this by using templated methods within the manager, though anyone who wishes to shoot this idea down go ahead!


Why not template the manager itself? base_type then becomes T.

It allows you to work directly with the type. It allows you to extend or override the default behavior via inheritance. It allows for any variable type use to be done once (in the manager) rather than many times (for each instantiation). It allows you to specify special deletors per type for when the shared_ptr's die (such as Texture::Release()), keeping that responsibility from the programmer.

On the downside, it makes for many managers rather than one. Still, those are then easily collected in a factory or similar container if the variety of managers is too annoying to deal with.

Share this post


Link to post
Share on other sites
Thanks for your replies, everyone, I shall rate you up once I've written this.

Quote:
Original post by defferHow are you planning to use that polymorphic behaviour? You are going to use a particular resource as its real type (BaseModel, BaseTexture, BaseSound, etc.), not as IResource. So the only place to use the IResource would be in the manager itself. What for?


Yeah, I was thinking this while writing the classes. IResource is purely for the benefit of the manager, and I'm not entirely sure if this is a good enough reason for it to exist; I could easily factor it out of the design.

Quote:
Original post by DeyjaI would go a totally different direction myself. Note that I don't require any special base class. In fact, it can store anything that can be constructed from a const std::string.

...



I like your approach, though it looks like it could do with a little tidying up. I may well end up adapting this depending on what people say about the next idea I'm about to put forward!

@ussnewjersey4: I also like your approach, it seems like a fixed and more elegant (thanks to C#) version of what I was trying to do.

@Telastyn: I'd thought about templating the whole manager too, but figured this would quickly get messy; I'm not quite experienced enough to figure out how to do this properly!

Now, I'm beginning to think that actually a centralised manager isn't totally necessary. I'm also thinking that a manager for each resource type is a bit pointless; there will be a 1:1 mapping between manager and resource, which seems a waste. I may as well simply implement the manager functionality (no duplicate resources etc) into the resource classes themselves. So, TextureResource would have a static map<string, shared_ptr<TextureResource>>, which it would use to check each time it is instantiated. This is a massively simpler solution, but I'm sure it has some drawbacks, can anybody point them out to me?

Share this post


Link to post
Share on other sites
Like [edit: most] every static solution, it ties you to the design. You cannot then, for example have a resource manager per map or per theme; you'd have to unload all the old and load all the new (rather than loading all the new and replacing the old manager). It also I'd imagine make variable loading targets (things not from files), or threaded loading fairly difficult.

[Edited by - Telastyn on August 11, 2006 8:35:38 AM]

Share this post


Link to post
Share on other sites
Fair enough, I'd like this to be fairly flexible and extendable. Okay, looks like I shall be basing it loosely (thanks, copy&paste!) on Deyja's code, thanks again for your help people :)

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!