Resource manager for open world game

Started by
19 comments, last by jmakitalo 9 years, 3 months ago


#B There are basic two ways of letting this not happening. First: you can have pointers to different dependencies; you need to connect the "CImageManager" to the "CTextureManager". Second: if you're using the static classes approach (not the singleton one) you can just include your header file in the class if you want to make things "simple". The same applies to the "CModelManager". The "CTextureManager" should be connected to it. The Factory Pattern approach it is a good way of creating resources that are inherited. But it is just an example; a good way of saying I'll give you an ID and filepath and you can return a resource for me.

Connecting the managers together seems more natural (than connecting a type of resource to another type of manager). I did this in my sample code: CMeshManager defined on line 267 stores a pointer to CMaterialManager. This is then used in the load() method.


#C I know what you're saying. Since you know that you're not mixing responsibilities - you have to have that cognitive ability of the classes responsibility - you can do whatever you want to load the resources. But if you're talking about optimizations, it is impossible to answer because we don't know the overall architeture of your engine.

There are certain aspects of resource manager that need to be fast and some aspects that don't. For example, getting the actual resource data, say texture id, in hot rendering code should be fast. That's why I think that "single manager containing all type of resources" is potentially slow due to required dynamic type casts. One thing that does not need to be that fast is obtaining a handle to the resource by resource name/hash (my sample code line 148), because this is usualy done at load time. In my sample code, I experimented with the idea that at load time, handles to resources are retrieved. At active game time, handles are used to access the actual data. This data should always be usable, but it may not be the actual resource data, if some error occured or the data is still being loaded. This is why I invoked the idea of "dummy" data: each container would have to have one resource that is used in case of error. The engine would have to ensure that such resource is always added before anything else.


DATA_STRUCTURE RENDER_ITEM {
       HANDLE Texture;
       HANDLE Shader;
};

DATA_STRUCTURE RENDER_ITEM_QUEUE {
      RETURN_VALUE Render();
      Queue<AA_RENDER_ITEM> RenderQueue;
};

/* 
Now it is time to render.
*/

for each RenderItem {
CTexture* Texture = TextureContainer.Get( RenderItem.Texture );
Texture->Activate( TextureSlot ); //This is virtual. The TextureSlot depends of other things such the "object" you have. Assumming a slot based engine.
}

The pseudo-code above is very high-level and just an example. But you got the idea. As with the reference counter and as your should know you must find a way of avoiding hash collision. Actually since you're talking about loading time, this is not a problem to compute because you're loading all resources. If you was doing this while playing the game you could have into problems.

Advertisement

The pseudo-code above is very high-level and just an example. But you got the idea. As with the reference counter and as your should know you must find a way of avoiding hash collision. Actually since you're talking about loading time, this is not a problem to compute because you're loading all resources. If you was doing this while playing the game you could have into problems.

Thanks for the example. Your approach of invoking TextureContainer to get the texture seems fine. In my code attempt, the handles store pointers to managers that created the handles, and then the rendering code can call handle.getData(). There are probably pros and cons in both approaches.

I'm not sure why you mention hash collisions at this point. Anyway, I'm not worried about those. In fact, I'm not convinced I should even use hashes. I could just use directly the names of the resources instead, since seeking the handles by name does not have to be that fast. With "one manager per resource type" there is no fear that sound "fire" is mixed up with particle system "fire".

I don't think that loading resources during game play will be a problem if utilizing handles. In your code, Get() method would just return a dummy texture until the actual texture has been loaded. I also tried to sketch this in my code. Of cource in practice is not very nice if an ogre mesh right in front of the camera is shown as a dummy teapot mesh while the ogre is still loading, but it's better than access violation. The game or higher level engine routine should take care that resources are loaded sensibly.




- Your code clearly takes the path that there'll be one manager per resource type. That's already a major desicion. I would like to hear more experiences whether this tends to work or not in games that have many types of resources.

It depends on your loader, but for me having seperate managers for different resource types is a valid approach and allows you tight control over budgets.

If you allocate 500 meg of ram to textures and the graphic artists use 512 meg, you know about it. The same for meshes and sound effects. Being able to say "the game crashes because Fred the painter used 4k textures for that tank" saves you a hell of a lot of heartache in crunch.


- Your manager directly returns the asset data, not a handle. Another big desicion, which I think won't work well if the manager decides to move that data in memory or the data is destroyed and a dangling pointer remains.

Don't hold onto the pointer then. Call the manager every time you want to use the resource.

    spriteBatch.Draw(Game1.Instance.TextureManager.GetTexture(backdrop_hash, full_screen, Color.White);

Works just as well, the performance hit of a function call is minimal and the manager can do what it likes with the data with out causing issues to the display code.

- There seems to be no way to actually load the resource. Or is the idea that after invoking AddAsset(), the asset is loaded? There would have to be some way then to know if it was already loaded.

It's high level example code, the actual details of loading the data would be hidden away in the new AssetType() call.

The already loaded check is the hashmap.Contains call.

- It does not seem robust that to remove a resource, one has to call a method of the manager. If some game object using a resource is destroyed, it will have to signal the manager that the resource is not used anymore. Injecting manager dependency to game objects does not seem natural.

May not seem natural, but it's damn effective.

If the game object is destroyed, you call the manager. Simple, robust, not the fastest in the world but if you are worried about the overhead of a few function calls at this stage you are really going to shit your pants later smile.png



    class Content {

    public:
        Content();

        ~Content();



        template <class T>

        std::shared_ptr<T> Load(const std::string& File);



        template <>

        std::shared_ptr<Texture> Load(const std::string& File);

    protected:

    private:

        YourCacheGoesHere<std::shared_ptr<Texture>> Textures;



        Content(const Content&);

        Content(Content&&);

        Content& operator=(const Content&);

        Content& operator=(Content&&);

    };



    template <class T>

    std::shared_ptr<T> Content::Load(const std::string& File) {

        static_assert(false, "Content::Load<T>() must be specialized.");

        return std::shared_ptr<T>();

    }


    template <>

    std::shared_ptr<Texture> Content::Load(const std::string& File) {
        // Check if the item is already cached.  If it is, return it.
       // Otherwise load the resource, add it to the cache, and return a shared_ptr to it.
      // If the resource initialization fails, you can always return a std::shared_ptr<T>(nullptr)

    }

Use: // note, it is not enforced that only one instance of this class can be created, but you can easily modify it as such to do so.

Content content;

std::shared_ptr<MyType> myType = content.Load<MyType>("MyTypeFileName.ext");

Pros:

-Easy to use

-Easy to code

-Easy to maintain

-Easy to extend functionality for additional resource types

-Resource release is simple. If the shared ptr for a given resource inside the cache has a reference count of 1, that resource is no longer being used externally and can be cleaned up when it's appropriate to do so.

-No dynamic casts.

-No potential memory leaks.

-No inheritance, only composition.

Cons:

-Requires an additional cache per resource type.

-Requires an additional function per resource type.

The way I have done my resources can cover at least some of your concerns. The answers already given I think show this is primarily up to personal preference.

Phil's suggestion is closest to what I did.

I created a few templates for caching my resources:

CacheItem<Type>

The CacheItem wraps a pointer to the allocated resource and a reference count.

I started with a shared_ptr but had some issues and found it simpler to make my own object. I also wanted to be able to track data other than just a reference count, so if I used a shared_ptr, I would still end up having to wrap that shared_ptr in an object with my extra data.

This sort of matches your desire for a handle. This could point at a dummy object until the real data was loaded if you wanted to.

Other things you might want to track depending on how you intend to flush your cache: last time requested, maybe a sector id (from your original post)

CacheRef<Type>

The CacheRef is what my game uses external to the cache. This is what the resource manager returns. It takes a reference to a CacheItem. This object is responsible for incrementing/decrementing the reference counter in the CacheItem and providing the pointer to the data.

When this object goes out of scope it will decrement the reference count. Similar to using a shared_ptr. This prevents me from having to worry about letting the resource manager know when I am done with a resource.

Cache<TKey, Type>

Implemented with a std::map. The stored type for the map is a "CacheItem<TData>*".

Relatively simple has functions for adding, finding, flushing the cache. Could be tailored to flush by whatever you want to put into the CacheItem. Sectors, reference counts, last request time, etc...

I have a single resource manager that uses instances of the Cache<Key,Type> with custom load functions for the various types. The declarations get a little cumbersome, so some TypeDefs are used to make life a little easier. With the templates there is very little duplicated code, just the specifics needed for the various resource loading/creation.

My resource requests are something like:

1) auto* item = m_whicheverCache.Find(name);

2) if (!item) -> Load data, create resource, add to cache.

3) return item

What gets returned is a CacheRef which can be queried to see if it is empty/null which indicates an error somewhere. Hang onto the CacheRef for as long as you need it which keeps the reference count up.

This also provides another safety check for me in regards to whether I cleaned up what I thought I did. When my cache goes out of scope it will check the reference counts on all of the items in the cache; they should be zero if the cache is about to be destroyed. I have had times where I thought all my resources were cleaned up, but they were not.

General pattern of usage is pretty simple:


auto nameRef = resourceManager.GetString("login_name");
if (nameRef.IsEmtpy())
{
   DoSomethingAboutIt();
   return;
}

Print(nameRef.Data());

I have a .Data() and .DataPtr() which are just a reference and pointer to the underlying data. With the templates, everything is nice and type safe, no casting required.


Don't hold onto the pointer then. Call the manager every time you want to use the resource.

In your example code, you would search the pointer to the resource by hash each time the resource is used. I'm skeptic about the performance in hot code, say accessing textures when rendering all visible meshes. I guess the hash would refer to some map. Ok, traversing the map is only O(logN), but I would prefer not doing any unnecessary lookups in hot code.

After several thoughts and coding attempts, I'm feeling that a handle has the most pros of the alternatives.


Use: // note, it is not enforced that only one instance of this class can be created, but you can easily modify it as such to do so.

Content content;

std::shared_ptr myType = content.Load("MyTypeFileName.ext");

So you're after a "monolithic singleton" pattern. I don't see any reason to make a resource manager singleton, or any other class for that matter (well, maybe a logging class).

I don't understand how exactly you are utilizing templates here. If you just specialize it for each resource, you could as well write loadTexture, loadMesh, loadWhatever methods.

I think that if resources are stored in separate arrays, it makes sense to make a templated base manager class and derive it for each resource. Then much basic resource handling functionality can be implemented in the base class, while all the resource-specific functionality is implemented in separated derived classes.


The CacheItem wraps a pointer to the allocated resource and a reference count.

I started with a shared_ptr but had some issues and found it simpler to make my own object. I also wanted to be able to track data other than just a reference count, so if I used a shared_ptr, I would still end up having to wrap that shared_ptr in an object with my extra data.

This sort of matches your desire for a handle. This could point at a dummy object until the real data was loaded if you wanted to.

I would put the reference couter to the resource and not to the handle. In my attempt, when a handle is created, it accesses the manager class to increment the resource's use count.

hi

you can create your resource manager like this:

class resource
{
public:
virtual void* load(std::string filename);
void unload();
};

then create your texture resource like this:

class texture: public resource
{
public:
texture();
virtual ~texture();
void* load(std::string filename);
};

for every of your resource, create a class like that, sound, mesh, etc

then:

template <class t>
class resourceManager
{
public:
resourceManager();
virtual ~resourceManager();
void add(t& item);
void add(t&item, int pos);
void remove (t& item);
void remove(int pos);
void deleteAll();
int getPosition(t& item);
t& getItem(int pos);
private:
std::vector<t> resourceItems;
typedef resourceItems::iterator it;
};

then you can use shared_ptr<resourceManager<resource> > and then add your resource items from XML file (you can use pugiXML), and manage it

when you can't see well like me, you can't test your applications and you can't read something

Github


then you can use shared_ptr > and then add your resource items from XML file (you can use pugiXML), and manage it

Thanks for your input. I like the idea of having a resource base class as in your example. Then I can add a reference counter and possibly other metadata shared by all resources to this base class. This is an acceptable use of inheritance IMHO. However, I'm not convinced that the resource should implement its loader. The loader may have dependencies to various things and then it might be better that the resource manager implements the loader.

I think it is a good idea to implement iterator for the resources as in your implementation. The manager should implement begin() and end() functions, so that the routines in STL could be used if needed. Then in C++11 one could iterate over resources as for(auto &res : resourceManager) {...}.

I'm not sure if you read the whole posting as you boldly suggest to just return a reference to a resource instead of a handle or a smart pointer. To some extent this may be a matter of taste, but I think that a handle will be most versatile.

I'm thankful for all the replies to this topic. I'm sure that there is no best model for a resource manager, as different games have different requirements. It's good to read how others have approached the problem and most of all I want to learn which patterns are not good in most cases.

I think my experimentation in

https://gist.github.com/jmakitalo/93102107bb44d66f83d5

is on the right track. It would be nice to get some feedback on it. I tried to comment as much as possible, but I realize that some parts may be difficult to interpret. The resource manager stores resources by value in a vector and then a map that can be used to quickly associate a hash value with an index to the vector. This way data locality is imposed for the resources. One thing not discussed here that I tried to impose is that handles and resources cannot be created ouside the manager (private constructors). This way one can be sure that all resources and references to them can be found inside managers. I'm not sure if this restriction is necessary, but it might make the resource management more controlled.

I'm not entirely sure if I should store resources by value. The pros and cons I think about:

+ data locality

- slow vector resizing if resources are bulky

- requires that the resurces implement proper copy constructors if necessary

I'm not sure if data locality will actually be utilized, because usually one draws objects that are visible to viewer, and these may use only a small subset of all the reources. There is no guarantee that these few resources are located within overlapping cache lines.

The slow resize can be mitigated by preallocating a sufficiently large vector.

The third point is tricky. If some resource manages dynamically allocated memory, there is a chance that proper copy constructors are forgotten and may lead to errors that are difficult to trace. The safest solution would be to store pointers in the manager.

I went with a handle type of approach in my engine but it can be turned on or off. I can turn it off and just work with the reference counted version.

The main reason i went with handles was i wanted to be able to swap out the underlying data and be able to do dynamic reloads of content.

The entities in the scene have the handle but the data the handle points too has now changed. The hot reloading is nice for shaders and textures.

I can compile this out as well for a release type of build where I don't need the reloading and can save the extra level of indirection.

I also have a single resource manager for managing resource types using hashes/guids. Its based loosely off of unitys type of asset library. I also just have functions for LoadTexture etc instead of templates. Either way though you shouldn't have to worry about dynamic casts. I don't see a point in asking for a resource not knowing what type it is.

So specifying the type of resource based on a specialized template or a function shouldn't be an issue, in general the loader should fail to load a mesh as a texture and the need for a dynamic cast is avoided. There should be checks to catch issues like this during testing that shouldn't need to carry over into a final release.

I also allow the resource types to create/load them selves. I have the base resource types in the my core library and then in a platform specific version library, which is where my resource manager is, there are custom types of the resources that know how to load themselves off of disk and then pass the raw data to the base class that takes care of the rendering specific creation dx11/gl etc. I switch between rendering implementations based on build flags instead of dynamic loading.

My resource other than knowing how to create/bind/unbind their gpu versions (buffer/textures/etc) are mostly dumb.

Just what I chose to do.

hi again,

in my idea, it is better to use smart pointers

because you may get memory leeks

many resource managers in games have memory leeks

about stl, i have to say that stl in my idea have everything to create your resource manager

that example that i've written is a very very simple resource manager, but with stl you can create more flexible and better resource manager, loader and use it

inside stl, you can use boost that is a good option

when you can't see well like me, you can't test your applications and you can't read something

Github

This topic is closed to new replies.

Advertisement