by Chris "Kiwidog" Hargrove
Download this week's code files: cotc6src.zip (74k)
Be sure to read the license agreement before using the code.
"To be thrown upon one's own resources, is to be cast into the very lap of fortune" - Benjamin Franklin
Happy returns! :) This week's article is going to be a bit of a short one, but the topic is nonetheless important to consider in any game project: resource management.
I've probably mentioned the term "resource management" several times over the past few articles, but never actually explained what I meant by it. In a nutshell, a resource management subsystem is a blend of memory and file handling that helps you load and use resource data files (like bitmaps, sounds, models, textures, etc) in a more carefree manner. You tell the resource manager what resources you might need, and it makes sure they're there when you need them. Kind of like a "file butler".
Resources these days take up a lot of space, so you generally can't afford to load every possibly-used resource file ahead of time unless you feel like wasting a ton of memory. For instance, say you're writing a strategy game and one of the units is capable of shooting a certain type of missile, with a certain missile image bitmap. Now when you're playing, that image may never be needed (for example if there are none of those units on the map, or those units are never called upon to shoot, etc). But if that image is ever needed, it needs to be loaded so it can be used. You could try and put in explicit checks for all these scenarios so the resources could be loaded when not present, but that would be pretty redundant and error prone.
What's needed is a more general facility for "on-demand" or "lazy" loading of resources, and that's what our resource manager will handle for us. If we think we might need a resource file, we "register" it to the resource manager, and it gives us a resource ID to represent that resource. Later on, if we actually need that resource, we can use the ID to get the resource data itself (which will be cached in as necessary). If you're familiar with on-demand loading and/or "proxy" design patterns, this stuff shouldn't be anything new (and if you're not, don't worry... it isn't that complicated).
I added two more files to the project, res_man.h and res_man.cpp, which hold the resource manager subsystem. For those of you who are confused, looking at the interface will probably make things a bit clearer. The whole thing really revolves around just two important functions.
[Look at res_man.h]
In addition to only loading a resource when necessary, our resource manager has three different caching types it can consider. The first is a temporary resource, which only lasts until the next frame. This is for resources that you need now but won't care about anytime after. The second is a level resource, which caches out after the current level is over; most in-game resources end up being of this type. The third is a game static resource, for data that should never be cached out like menu backgrounds, font images, system sounds, etc.
If you look at the interface functions, the two real functions of importance are the RES_Register and RES_PtrForId functions. The first is called whenever you want to get an ID for some resource file (a bitmap for example). You tell it the filename, the caching type (temporary, level, or game static), and optional callbacks to control how the resource is loaded and cached out. It then gives you an ID for the resource, which is tied to that filename (if you register the same filename twice, you'll get the same ID). This ID is used in place of wherever one might otherwise need a pointer to that loaded resource file. When a data pointer is actually needed, you call the second function, RES_PtrForId, and it gets it for you.
The benefit here is that until you call RES_PtrForId, the resource exists nowhere in memory so it doesn't take up space. It can also get cached out according to its cache type without hurting anything depending on it, since RES_PtrForId will be more than happy to load the resource back up again.
The implementation for the resource manager in res_man.cpp is pretty small, and shouldn't be difficult to understand. Most of it is built directly above the zone memory allocation routines that were added in the last article, since each cache type has its own zone that it works with to bring in resources. Take a dive into it and see if you can figure out what's going on; there's less than 400 lines of code in the implementation, so you shouldn't have too much trouble. :)
I kept the code simple and sacrificed efficiency in a couple places, since I want to leave some possible improvements as an exercise to you readers. For example, the routine to find an existing entry based on a name goes through the entry list linearly, and only uses what I call a "lame hash" integer value to speed up rejections. Obviously there are other more efficient data structures that could be used, such as a binary tree, or a real hash table based on string length, checksum or some other hash function, etc. What do you think would be most effective in this case? Experiment with it. Also, the level cache scheme should try and preserve resources that are going to be loaded immediately in the next level, so they're not needlessly cached out. What are some of the ways you could modify the code to do this with a minimum of zone memory fragmentation? Once again, experiment with it. You may end up surprising yourself. :)
Yup, that's it for this week. I told you it was a short one, didn't I? Hey, consider it a breather. We'll need it, because next time we'll be diving into graphics and DirectDraw! :)
Until next time,
- Chris"Kiwidog" Hargrove is a programmer at 3D Realms Entertainment working on Duke Nukem Forever.
Code on the Cob is © 1998 Chris Hargrove.
Reprinted with permission.