Asset Management!

posted in Lost Repo
Published March 05, 2015
Advertisement
It's been quite some time since I last posted on this site. In fact, so much time has past that I figured it's better to start a new journal than to continue the old one.

I've been getting back into C++ after a number of years away. I used to be decently skilled at the language... but I was a bit surprised at the new features it obtained since I left (there are still things about C++11 that I'm just discovering, and I haven't even looked at C++14 or the upcoming(?) C++17). However, one of the lovely new features the language had obtained was smart pointers (truthfully, I don't even think boost was around when I last worked with C++).

Looking at all the shiny new C++ toys... I decided to start a C++ game. Ok... actually... I started about 4 of them since I came back to the language, but my most recent project has come leagues ahead of the others (and all it does is display a splash screen, then a blank window... lol). I'm not going to talk about the game or the project, really. It's too infant at the moment to be worth mentioning. What I want to write about is the Asset/Resource handling system I implemented!

Asset System


No... I'm not about to go around claiming I invented anything. See... Asset management (keeping track of loading and using images, audio, meshes, etc) has always caused me to bog down in a project. I know what I want to do, but I always had trouble wrapping my brain around the issue (I'm slow tongue.png ) and every time I thought I grasped the problem, I'd get to a point where it didn't seem to work, whether that be because the system didn't work for all assets I wanted to use, or I couldn't figure out how to handling the loading of those assets, or... the biggest of them all... how to make the "manager" accessible to everything that would want to use it without making the "manager" itself, global.

The solution came with the following realizations (thank you to the GameDev forums)...

  1. One class CANNOT rules them all, in this problem.
  2. Really check the responsibility of the class.
  3. Singletons don't help this issue.

1. From one class to three classes... per resource 0_o
No matter how long I pondered the issue... no matter the coding tricks I tried to employ... I couldn't design a single, universal, "Asset Manager" class. Sure... I can stick "template" to the head of my class definition, but how do I load that "ASSET"? An image loads different than an Audio file. A Mesh needs an Image as part of it's data. I couldn't define one loader that loads them all.

Not only was loading the asset an issue, but not loading the asset was an issue! I didn't want to load an asset unless I needed it, so I only really wanted to store the description of the asset in the manager and have the manager load it when requested. With more complex assets (like the afore mentioned Mesh), it would need to load not just the mesh, but it's underlying data, such as texture and material information.

The next logical thought was to do something like this...class Asset{public: virtual void load(); virtual void release(); virtual bool is_loaded();};
... and then rig the actual asset class we want (like an SFML sf::Texture, for instance) with the Asset base class we just created.

But... this ends up confusing the real asset class interface with the interface defined in Asset. What if I have an asset class that actually has a method void load() ? Even worse, however, is that any object that obtains an instance of my asset has access to the ::release() method! WHY? Why would I want any other object, other than my asset manager, to release() ANY of my assets?! I don't!

My real solution...template class AssetDescriptor{public: friend class AssetCache; virtual std::string name() const=0; virtual bool loaded() const=0; virtual bool operator==(const AssetDescriptor &rhs) const=0; virtual bool operator!=(const AssetDescriptor &rhs) const=0; virtual operator bool() const=0;protected: virtual uint64 _referenceCount() const=0; virtual std::shared_ptr& _get()=0; virtual bool _load()=0; virtual void _release()=0;};template class AssetCache{public: virtual void add(const std::shared_ptr > &descriptor)=0; virtual std::shared_ptr& get(const std::string &name)=0; virtual bool exists(const std::string &name) const=0; virtual uint64 referenceCount(const std::string &name) const=0; virtual void unloadUnreferenced()=0; virtual void removedUnloaded()=0;protected: uint64 _getDescriptorReferenceCount(const std::shared_ptr > &desc) const{ return desc->_referenceCount(); } std::shared_ptr& _getDescriptorAsset(std::shared_ptr > &desc){ return desc->_get(); } bool _loadFromDescriptor(std::shared_ptr > &desc){ return desc->_load(); } void _releaseDescriptorAsset(std::shared_ptr > &desc){ desc->_release(); }};template class AssetPool{public: virtual void setPoolCache(const std::shared_ptr > &cache)=0; virtual void loadPool()=0; virtual void add(const std::string &name, bool autoLoad)=0; virtual void add(const std::shared_ptr > &desc, bool autoLoad)=0; virtual std::shared_ptr& get(const std::string &name)=0; virtual bool exists(const std::string &name) const=0; virtual bool existsInPool(const std::string &name) const=0; virtual uint64 referenceCount(const std::string &name) const=0; virtual operator bool()=0;};
That's it! Three template interface classes!

Class AssetDescriptor:
To the outside world, all we want to know about this class is it's name() and if it's currently loaded() or not. The virtual class has no methods for HOW to define any of that information. That's for the concrete classes (class TextureDescription : public AssetDescriptor{};) to define. The comparison operators allow for quick compares to see if two descriptors are describing the same thing (how ever the concrete classes want to do that), and an operator bool() which will allow the outside world to quickly check if the descriptor is valid (as in, it has information which can be used to load a resource).

It has some protected methods as well... such as the actual _load() and _release() methods. A method to obtain the current _referenceCount() and a method to obtain a shared_ptr to the actual asset.

The descriptor is the only place that holds the "description" for loading/creating the asset. It does the loading/creating when needed. It also is the source of the asset's reference count (once loaded).

Of course, the interface is such that the outside world can only describe the asset. It can't load or obtain the asset once loaded.

Class AssetCache:
Add the AssetDescriptors to the cache, and it'll keep track of them for you. Being a friend to the AssetDescriptor, it can call the loading and releasing as needed. The cache's publlic get() method will check if the asset is loaded, call the descriptor's _load method if it isn't, then return a smart_ptr to the asset if load was successful.

Of course, how the cache manages any of that is up to the concrete class. An std::unordered_map is my prefered way, and tends to be for most resources.

Notice, also, that the AssetCache class is the only class with actually defined methods (all protected). These are hooks to manage the limitation of C++ friends. A derived cache class does not inherit the friendship of it's parent, but it does inherit the protected methods defined by it's parent, so these defined, protected methods are the gateway to accessing the protected descriptor methods.

This is great! I have interfaces for describing assets and caching them! Two issues remained.

  1. How to access the cache globally
  2. Asset unloading/releasing is still EXPOSED!!

Class AssetPool:
AssetPool helps me with issue number two above, but also gives me one other benefit I'll explain in a second. In general, AssetPool has much of the same interface as AssetCache sans the unload/remove methods, but it adds a "setPoolCache" method. The intent is that I can create as many AssetPool instances as desired and, post creation, assign them to the cache the pool is going to work from. I then use the pool to add assets and obtain them.

The other benefit to the AssetPool (while not directly forced by the interface) is that I can store the loaded assets in the pool (as well as in the descriptor). Doing so ups the reference count on the asset because the asset is being held (internally) by the pool, but since the pool need to obtain the asset from the cache, I'm not reloading an already loaded resource.

If it's not clear, think of the pool as a "local" instance of the cache. Object A and Object B can both have their own pools pointing to the same cache. Object A then pools "imageA", "imageB", and "imageD". Object B pools "imageB", "imageC", and "imageD". The cache, which both pools are associated, only loads up four assets with assets "imageB" and "imageD" having a reference count of 2 (one per pool) while "imageA" and "imageC" have a reference count of 1 (as each is only being requested by one pool).

I know what you're saying... "Ok, but the AssetPool still needs to be given an AssetCache. How do you do that without passing the cache around?"

Locator classes:
This doesn't have an interface class because you can't really interface a static class and that's what the Locator class is.

NO! I'm not trading in a singleton for a static class. Let me show you how I use the locator...

Let's look at the following...class TextureCache : public AssetCache{...}; // My project used SFMLclass TexturePool : public AssetPool{...};// Here's my locatorclass TextureCacheLocator{public: static void mapPoolToCache(std::shared_ptr > &pool); static void setAssetCache(const std::shared_ptr > & cache);private: static std::shared_ptr > m_assetCache;};std::shared_ptr > TextureCacheLocator::m_assetCache = nullptr;void mapPoolToCache(std::shared_ptr > &pool){ pool->setPoolCache(TextureCacheLocator::m_assetCache);}void setAssetCache(const std::shared_ptr > & cache){ if (TextureCacheLocator::m_assetCache == nullptr){ TextureCacheLocator::m_assetCache = cache; } // Notice how the cache can only be set once? Yeah... it's my little cheat.}
... now, above, I define a concrete cache and pool class for my Textures (assume I did the same for the descriptor), but notice how the TextureLocator only expects pointers to the interface AssetCache and AssetPool? This means that I can define TextureCache and TexturePool any way I want, as long as it conforms to the interface (obviously), but I can also do the following...class TextureCacheEx : public TextureCach{...};
... and the locator still works! Also, even though I extended TextureCache, I don't have to extend TexturePool in order to use the locator class as is.



Conclusion:
Even though I'm using a static Locator class, no objects (other than the one that creates the initial asset cache) have any way of touching the cache except through AssetPools. Any number of AssetPools can be created by any object that needs them, and these objects can use the locator to assign the cache to the pool. As far as the object is concerned, they have their own asset system via the pools, but in reality, they're all sharing through the cache, and the cache handles all of it's loading through the descriptors.



Anyway... this is probably not new to anyone, but I hope someone finds it useful. At the very least, this is a (very large) reminder note to me about this system, in case my repository explodes and I loose the original code.

If anyone did find it useful, feel free to use or borrow any of the above code and use it as a jumping off point.

If anyone has any suggestions for me on how I'm using this, I'm all ears. It's working great for me so far, but I may be missing a shortfall.

Also... I want to thank the forums on this site from which I pieced together the bits of information that lead me to this system.

Ok... I'm done typing now...

Seriously

I mean it

Right this moment.

No more talking from m[END OF LINE]
2 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement