Crank It
The workings of the handle manager itself are pretty simple. It contains an array of HandleEntry types (see Listing 2). Each HandleEntry has a pointer to the data and a few other bookkeeping fields: freelist indices for efficient addition to the array, the counter field corresponding to each entry, and some flags indicating whether an entry is in use or it's the end of the freelist.
Listing 2: HandleEntry Structure
struct HandleEntry
{
HandleEntry();
explicit HandleEntry(uint32 nextFreeIndex);
uint32 m_nextFreeIndex : 12;
uint32 m_counter : 15;
uint32 m_active : 1;
uint32 m_endOfList : 1;
void* m_entry;
};
Accessing data from a handle is just a matter of getting the index from the handle, verifying that the counters in the handle and the handle manager entry are the same, and accessing the pointer -- just one level of indirection and very fast performance.
We can also easily relocate or invalidate existing handles just by updating the entry in the handle manager to point to a new location or to flag it as removed.
Handles are the perfect reference to data that can change locations or even be removed, from data that needs to be serialized. Game entities are usually very dynamic, and are created and destroyed frequently (such as with enemies spawning and being destroyed, or projectiles).
So any references to game entities would be a good fit for handles, especially if this reference is held from another game entity and its state needs to be saved and restored. Examples of these types of relationships are the object a player is currently holding, or the target an enemy AI has locked onto.
Getting Smarter?
The term smart pointers encompasses many different classes that give pointer-like syntax to reference data, but offer some extra features on top of "raw" pointers.
A common type of smart pointer deals with object lifetime. Smart pointers keep track of how many references there are to a particular piece of data, and free it when nobody is using it. For the runtime of games, I prefer to have very explicit object lifetime management, so I'm not a big fan of this kind of pointers. They can be of great help in development for tools written in C++ though.
Another kind of smart pointers inserts an indirection between the data holding the pointer and the data being pointed. This allows data to be relocated, like we could do with handles. However, implementations of these pointers are often non-serializable, so they can be quite limiting.
If you consider using smart pointers from some of the popular libraries (STL, Boost) in your game, you should be very careful about the impact they can have on your build times. Including a single header file from one of those libraries will often pull in numerous other header files.
Additionally, smart pointers are often templated, so the compiler will do some extra work generating code for each data type you instantiated templates on. All in all, templated smart pointers can have a significant impact in build times unless they are managed very carefully.
It's possible to implement a smart pointer that wraps handles, provides a syntax like a regular pointer, and it still consists of a handle underneath, which can be serialized without any problem.
But is the extra complexity of that layer worth the syntax benefits it provides? It will depend on your team and what you're used to, but it's always an option if the team is more comfortable dealing with pointers instead of handles.
Destination Data
There are many different approaches to expressing data relationships. It's important to remember that different data types are better suited to some approaches than others. Pick the right method for your data and make sure it's clear which one you're using.
In the next few months, we'll continue talking about data, and maybe even convince you that putting some love into your data can pay off big time with your code and the game as a whole.
[EDITOR'S NOTE: This article was independently published by Gamasutra's editors, since it was deemed of value to
the community. Its publishing has been made possible by Intel, as a platform and vendor-agnostic part of Intel's Visual Computing microsite.]
|
In order to not having to check for null, the handle manager could return a reference to some dummy data, e.g. if an invalid texture is requested, a small black dummy texture could be returned.
I have same question regarding the usage of the data though:
How would you handle the rendering of a mesh for example?
Suppose we have the mesh, the shaders and some textures for rendering.
All is referenced by a handle, so when rendering we'd have to perform the following steps:
1. retrieve a reference to the handle manager(s)
2. retrieve the pointer to the data from the manager
- decode the type
- check the handle's counter
- index into the array and return the pointer
3. bind the resource for rendering
When using a smart pointer, you'd need the following steps:
1. get the indirection structure pointed to by the smart pointer
2. retrieve the pointer to the data from the indirection structure
3. bind the resource
So, which method would you suggest if serialization is not considered?