C++ Resource Manager
Accomplishments and Techniques:
Created a resoure manager and associated cache holder to quickly load game object resources for a mockup roleplaying game.
Created a custom freelist-style memory allocator to load unique object pointers from the cache.
Created a custom buddy-style memory allocator to load shared object pointers from the cache.
Utilized multi-threading and garbage collection to remove unused objects from the cache.
Used polymorphism to implement and store a family of serializeable game resource classes, including text file objects, weapon data, and player spell inventories.
Implemented thorough exception handling to mitigate loading errors.
Project Overview:
For this project, I created a resource manager system responsible for loading, owning, caching, unloading, and providing access to various game resources for a mock roleplaying game. The resource manager acts as both a loader and cache, which stores objects such as text assets or textures and then provides either shared or unique pointers to objects on request. I created this system as my final submission for a C++ programming Master’s course, with the desire to better understand custom memory allocation.
The below snippet showcases the central ResourceManager class, which acts as an interface for the CachHolder struct. When requesting an object to be loaded, objects will call one of two templated functions contained in the ResourceManager class:
shared_load
unique_load
Shared load returns a shared pointer to the requested object stored in the cache. This pointed object is const and intended to be used for data reading rather than writing (for example, if an object wanted to copy some canned dialogue from a text file). Unique_load returns a unique pointer to a copy of the object stored in the cache. This pointed object can be edited as the requester sees fit (for example, a weapon object could be requested, and later have its damage stats increased).
Language: C++
IDE: Visual Studio 2022
Production Time: 2 weeks
Link To Github: Resource Manager on Github
Cache Holder:
The CacheHolder struct is responsible for managing access to resources currently being held in memory. These resources are stored in an unordered map in the below format:
std::unorderedmap<std::string, std::pair<timet, std::shared_ptr<const Resource>>>
The above map takes a string filename as the key to a paired value. These values include the time that the specified resource was last accessed, as well as a shared pointer to the resource in question. Note that the pointer specifies a const resource, ensuring that shared pointers cannot alter the resource being accessed witout creating a unique pointer copy.
The CacheHolder has three primary functions:
Load objects from .txt files to be stored in memory for later access
Create shared pointers to load const resources (such as texture data or NPC dialogue).
Create unique pointers to load editable resources (such as weapons whose stats can change over time).
Featured below is a snippet of the CacheHolder struct and its primary functions.
Freelist Allocator:
Rather than relying on default memory allocation, I felt that this project was a great time to experiment with custom allocators! I began by implementing a freelist-style allocator for handling the memory needs of unique pointers that are loaded from the cache. The freelist allocator works by first allocating memory as expected, through the default allocator. But when that memory chunk is no longer needed, rather than returning it to the CPU as expected, it is inserted into a linked list of Node structs that represent chunks of reusable memory. Multiple lists of memory chunks are maintained in order to handle differing sizes of memory chunks. The result is an allocator that is intitially slightly slower than the default, but after exceeding the lifetime of a few objects, becomes significantly quicker as it no longer needs to allocate raw memory. According to my tests, the freelist allocator was about 15% faster overall at allocating unique pointers, compared to the default appraoch. Below is a snippet of the memory linked list struct, which the freelists use to request and return memory chunks.
Buddy Allocator:
The most challenging aspect of this project was creating the buddy-style custom memory allocator, which was responsible for allocating blocks of memory for shared pointers to objects stored within the cache. This system builds off the freelist allocator above, using the same memory pool class and linked list node helper structs. The buddy allocator works by initially allocating a very large chunk of memory at the start of the application. When memory is requested for a shared pointer, that massive chunk is split into two blocks (represented by Nodes, which store the raw memory as char*). The size of these blocks are compared against the requested memory, and continually split into halves until a block is just large enough to hold the allocated memory without being split again.
I wanted to use both the freelist and buddy systems in order to compare their strengths and weaknesses in a practical application. While the buddy allocator is a bit faster when implemented correctly (due to raw memory only being allocated once, it suffers the drawback of a strict memory limit. Once the initial block has been fully used, no more memory can be allocated until requested blocks have been returned and absorbed into larger nodes. This makes the buddy allocator great for systems with predictable memory needs, while the freelist is a better choice for allocation requirement (for example, a multiplayer game where any number of items could be spawned by a large playerbase).
Below are the functions for return memory blocks to the buddy allocator.
Resource Classes:
In order to test the efficacy of my resource manager, I created three test resource classes to be cached and loaded by the system, including:
A base resource which can be serialized and deserialized using the the Serializer struct.
An inherited string resource, which loads large .txt files and can be used for character dialogue.
A weapon resource, which stores a vector of “DamageStat” structs. Each DamageStat struct is assigned a value from an enum of elemental damage types, including ice, holy, fire, et cetera. This resource can be used to store data for complex RPG weapons.
Below is a snippet of the Serializer struct, as well as the function definitions for the base resource class.
Exception Handling:
Being one of my larger and more complex programming projects to date, I figured this would be the perfect opportunity to practice proper etitquette and debugging practices with some detailed custom exceptions. I made a seperate header file to store my exceptions, creating an inherited class for each common issue that I expected users to encouter, including:
Failing to insert a resource into the cache.
Failing to locate a specific resource from the cache
Failing to properly allocate custom memory for either the freelist or buddy allocators.
Failing to locate a .txt file to be loaded by the string resource class.
Included below is a snippet of each exception and their corresponding descriptions, which are surfaced to the user upon triggering.
Wrapping Up:
While I have been casuallly scripting in C# and Python in a game-development capacity for years, I had never considered myself much of an actual engineer, avoiding critical programming qualities like code readability, extensibility, and debugging efficiency. Working towards my Master’s degree in game engine programming has been essential for improving these weaker areas of my skillset, and I am very proud of how vastly my engineering abilities have improved during this first semester. This final project was an excellent opportunity to showcase my understanding of core programming tenets, as well as the finer points of C++, such as pointers, multi-threading, and memory allocation.
If I learned one thing during this class, it’s the importance of understanding systems and data structures in the larger abstract sense. Snytax and technical details can be learned quickly and reviewed even quicker; What mattters most is that I can point at an unordered map, or an array of structs, or an observer pattern, and confidently explain why I chose to implement that particular structure for that specific purpose. To put it simply - Get the large picture, and the details will follow.