This is the first of my old “Code Samples” that I’m updating into this new more bloggy form.
There will probably be less information that other newer entries, but I’ll try and add more
than what was originally present in that post (which was basically just the comments in the file)
to talk about why things were designed the way they are, and what I would like to add to this in future.
To start with, lets take a look at the public interface for the Resource Manager class.
My primary goal with the public facing interface of this class was to minimize the complexity of
the interface, whilst maximizing the potential uses of this system. The two methods that users of
the class mainly touched were extremely simple to use, simply requesting a resource and getting
back a pointer of that type. I decided to use templates here to prevent the user from needing to
use messy and potentially error-prone casts on a generic Resource* type.
I learned a lot about priority queues from implementing this system. In part, what I learned is that the declarations for the standard libraries priority queue get pretty ugly pretty fast, but thats the price we pay in C++. The priority queue for loading resources made it very easy to ensure they were loaded in an order that respected the importance of the request.
I opted to use a map for the resources because fast random access time was my top priority. This may have been a situation where unordered map would have proved faster, and this will definitely be an area for investigation should I revisit this system again for a future project.
This section was actually simplified significantly by my job system. This was an extremely primitive job system, with all jobs sharing a single queue for the sake of simplicity of implementation, but it did enable this system to be implemented on multiple threads relatively quickly. I wrote this basic job system a long time ago when still had a lot of habits from C, hence the heavy usage of macros for declaring jobs. Believe me when I say you don’t want to see the definitions of those. Perhaps at some point in future I’ll attempt a dissection of that particular monstrosity now that I know all the ways in which it was wrong. Essentially, what the DECLARE_JOB macro does is declare a function that is immediately ready to be submitted to a set of worker threads instead of being executed on the main thread. This function will be defined later with a similar macro.
These two functions are the bulk of the interesting behavior of the system. As you can see, the primary purpose of the templates was to conceal a cast. I opted to use a dynamic_cast so that the system would smoothly return nullptr instead of either failing or returning something with the incorrect pointer type. This way if a user requested “image.png” as a lua script type object they would simply get nullptr back, which they could handle gracefully. I knew that dynamic_cast would be safe to use here, since all Resource types extended Resource, and had to override some pure virtual functions there and so I knew that the virtual function table must be present.
The first half of the cpp file for this class doesn’t really do anything unexpected. The constructor initializes everything, the destructor ensures everything is smoothly shut down and cleaned up, which is only slightly more interesting in a multi-threaded context. Some resource loading is handled here, but its only the things that cannot be handled on another thread. This was used in our game for compiling lua scripts and other things that were unsafe to handle on other threads due to the modification of global state loading them caused.
Here are those macro instances to match the DECLARE_JOBs from earlier.
CheckFilesForUpdate is designed to check to see if every resource it has already loaded has been modified since it was loaded. If the resource had been modified, it was added back into the queue to be updated. This effectively meant that we could modify the assets of the game while it was running, and see our changes immediately. Designers and artists took advantage of this feature, since it meant they didn’t even have to restart the game to see any changes they had made in engine. This was achieved by having the job, after checking all of the resources and adding them to the appropriate queues, add itself back into the job queue.
The LoadResource job is much simpler, and takes advantage of the polymorphic design of the Resource class to tell whatever type of resource has been requested to load itself correctly. Since this is done on another thread, care must be taken in the implementation of that load function to be thread-safe, but none of that effort is reflected here.
Overall, I’m quite happy with this system. It served its purpose well for the game it was built for. If I were to implement a similar system now, much of it would probably be pretty similar. My main issues are with the implementation of the job system, since I have spent a lot of time attempting to write better such systems since. There are also a few missing features that would present a significant problem in a larger game. The lack of ability for the system to unload resource from memory that are no longer required being the most important.