A Templated C++ Attribute Library for Object Persistence and Export
In this in-depth technical piece, coding veteran McNickle explains how experience on the Deer Hunter series led him to create a library for managing and exporting important variables.
A common task in game development is serializing objects to and from various storage or transmission mediums, and another is exposing those objects to scripting engines, command and debug windows, and other user interfaces. This article addresses these issues through the use of a generic attribute management system.
Every object can be identified by one or more attributes. Once registered with an attribute manager, these attributes can be serialized or exposed, and each can be queried to read or modify its value at any time.
Unlike C#/CLI properties, this implementation does not require that a change be made to the underlying object type. The attribute system can be expanded to support any type of object, including containers of objects, with minimal programming.
Furthermore, it provides a means of querying for all registered attributes and provides a policy based approach to object serialization.
This article discusses best-practices, performance and safety concerns. It presents a complete I/O policy implementation for serializing objects with TinyXML. It also provides examples for creating complex (non-POD) attributes.
During the development of Deer Hunter 2003, I was in the middle of implementing the debugging console window when a report came in from a tester that a newly implemented player setting was not being restored properly from the saved game format. This bug was preventing the testing team from moving forward with their test cases.
This was not a new problem; it had come up several times before. Essentially, any time a new variable was added to the base actor class or one of its derived classes, the programmer had to manually add support in the archiving routines to persist this data.
The problem was further complicated because it was not desirable to serialize all variables during a saved game, and the logic determining which variables should be serialized when was poorly understood.
Questions such as "Should the variable be saved during a saved game?", "Should it be serialized to the network stream?" and "Is the variable state managed by the server, or is it client authoritative?" had to be answered. They were typically understood at the time the variable was added, but when being reviewed by another programmer, the answers to these questions had to be re-discovered.
To add to this already annoying problem was dozens of lines of repetitive code in the serialization methods that serialized each variable independently. The code was not so much messy but highly repetitive and difficult to scan through quickly to determine if a variable was indeed being correctly archived.
This problem had come up often enough that I was finally frustrated with fixing it. I knew that no matter how many times I sent out a policy email about how and when to serialize player data, someone would inevitably forget, and I would be right back to re-visiting the serialize code again and researching the variable to determine when and if it should be serialized and who should be assigned the change order.
Another task I had been working on in the console window was very similar in nature: repetitive lines of code acting on specific, pre-determined variables. I wanted to replace all of the code that looked like this:
if ( input_str_arg1.compare_no_case("velocity") == 0 )
Selected_obj.setVelocity(atof(input_str_arg2);
else …
The new code would be elegant, automated, less error-prone and would not require significant changes to support the new variable. For example:
SetValue(input_str_arg1, input_str_arg2);
It was clear to me that this issue needed to be resolved. What I wanted was a system that solved the following list of requirements:
It must have minimal impact on the existing code.
It must be simple to expand without changing the code of the management system. In other words, it has to be generic, and it has to support complex types.
It must have a small memory footprint. (Thankfully, we were only developing for the PC, so there was some wiggle room.)
It must provide a simple, easily discernable means for the programmer who implemented the variable to state how the system and the user should interact with it, if at all. Ideally, this means could be adjusted dynamically.
It must provide a way to query an object for all of its attributes that are to be exposed or serialized and, if possible, how and when it should be serialized.
It must provide a way to automate the serialization.
It must support altering the values of these known variables.
And finally, it must not require that the value of the variable be changed through any particular mechanism, or be notified of any such change, while always being aware of the exact value of a known attribute at any time.
Attributes
Throughout this article, the definition of an "attribute" is a named reference to a variable. The AttributeContainer system provides a way to create attributes by "binding" them to a location in memory.
For example:
Container.Bind("player name", &m_name);
In this example, the member variable ‘m_name' is bound to the ID of ‘player name', which must remain unique within the ‘Container' object. Clients of Container then can query the value of this variable at any time by requesting that value from the container by ID.
name = Container.GetValue("player name");
The value of ‘m_name' can also be modified in the same manner, ie:
Container.SetValue("player name", "John Doe");
Yet attributes provide much more than glorified get/set mechanisms.
AttributeContainer is a template class which requires some knowledge of how to serialize the attributes it manages. This is done through the Policy pattern and is defined as:
template <class IO = AttributeContainerIO>
class AttributeContainer : public IO
The included library provides an example implementation of an XML I/O policy using TinyXML, but you can expand on these polices by implementing your own IO classes. Since all of the methods related to serialization are contained within the policy, and the AttributeContainer itself knows nothing of such things, you are not locked into any specific interface for serialization.
Defining an attribute container entails first implementing your IO policy and then passing that policy into the container definition like so:
AttributeContainer<AttributeContainerXmlIO>
I generally prefer to do this with a typedef:
typedef AttributeContainer<AttributeContainerXmlIO> t_XmlAttributeContainer;
Objects that wish to Bind member variables to this container, or to containers of this type, have a few choices. The safest way is for the object to inherit directly from the container, ie:
class myObj : public t_XmlAttributeContainer
By inheriting directly from AttributeContainer, you avoid situations where you might query a container for attributes of an object that has already been deleted. While this is the safest way, it does have one drawback, and that is memory usage. AttributeContainer is not a huge class (see memory considerations), but if you have thousands of instances of myObj, the overhead will add up quickly.
One alternative is to have a global instance of AttributeContainer (or at least one that is in a larger scope than the objects it manages). While this saves some memory, it does complicate use, as each bound attribute must be somehow uniquely identified per object.
Also, access into this global container must either be wrapped in checks to be sure the object in question still exists, or it must be removed from the container before the object is deleted. These hurdles can be easily solved by smart pointers who automatically manage removing the object from any associated containers or a number of other similar mechanisms.
However you choose to implement the container object, once all of your attributes are bound to the AttributeContainer, the container then can easily be used to serialize these attributes.
For example:
Listing 1
class myObj : public t_XmlAttributeContainer;
{
public:
myObj() {};
int m_Int;
float m_Float;
double m_Double;
};
myObj::myObj()
{
Bind("my_int", &m_Int);
Bind("my_float", &m_Float);
Bind("my_double", &m_Double, ATTR_READ_ONLY);
}
Assume that I want to serialize all ‘myObj' objects to disk. The t_XmlAttributeContainer object is a type of AttributeContainerXmlIO object, which implements the ReadXml and WriteXml methods, so the following code sample is an example of how this serialization might be done:
Listing 2
void myServer::Save()
{
TiXmlDocument doc;
TiXmlElement Xml("myObjects");
std::vector<myObj*>::iterator _i;
for (_i = m_objects.begin(); _i != m_objects.end(); _i++)
{
myObj* pObj = (*_i);
pObj->WriteXml(&Xml);
}
}
Every bound attribute of every myObj contained in the list of m_objects has been persisted to xml with the exception of the ‘my_double' attribute. This attribute was bound as ‘read only' and so was not written to the xml stream.
The member objects of the example myObj object are all built in types. What about complex types? AttributeContainer can bind and persist any type of object, as long as the << and >> stream operators exist for that type.
The example above bound member variables to a string id. But what about temporary variables, or variables created on the stack or on the heap? AttributeContainer provides safe ways to bind attributes regardless of their storage or scope but, as any object that deals with pointers does, it requires careful consideration.
Consider the following scenario: a game server object contains (among other things) a list of connected sockets indicating players who have either connected successfully or are in the process of connecting.
Each connection object will typically have a unique identifier, commonly the socket ID. Until the connection is complete, the player object which will ultimately contain the connecting socket has not yet been created.
One of the things the server may want to do is keep a list, unique to each socket, of the number of attempts a player makes at entering their password. Most of the time, a player will connect without ever failing to enter the correct password, but you have to support tracking password attempts per socket. What do you do?
A common approach is to add a counter in each socket class to track the number of failed attempts. While this works fine, it can be wasteful when those counters are not necessary. AttributeContainer provides an alternate solution: temporary attributes.
For example:
Listing 3
GameServer::FailedConnect(Socket& socket)
{
string identifier = "login_attempts_" + socket.guid();
int nAttempts = 1;
if (m_Attributes.GetValue(identifier, nAttempts) == false )
m_Attributes.Bind(identifier, new int(1), ATTR_TEMPORARY);
else
m_Attributes.SetValue(identifier, ++nAttempts);
// do some processing with nAttempts;
}
The ‘identifier' attribute will now remain within m_Attributes until either you specifically ‘Release' it or until m_Attributes goes out of scope, at which point the ATTR_TEMPORARY bit will cause the AttributeContainer to delete the memory allocated by the call to ‘new int'.
While this is a somewhat contrived example, it serves to illustrate the ability of AttributeContainer to perform basic garbage collection of temporary attributes.
Querying AttributeContainer for All Bound Attributes
One useful feature of AttributeContainer is that it can be asked for a list of all Attributes, returning their names and values. This is especially useful in User Interfaces such as console windows or property panes. Listing 4 gives an example.
Listing 4
void PropertyPane::DrawControlProperties(Control* pControl)
{
t_vAttributes list;
t_vAttributes::iterator pos;
pControl->GetAttributes(list);
for (pos = list.begin(); pos != list.end(); pos++)
{
AttributeBase* p = (*pos);
DrawProperty(p->GetName(), p->GetValue());
}
}
In this example, the ‘Control’ object inherits directly from AttributeContainer, allowing the PropertyPane, which has no knowledge of Control or what Attributes it might have, to query the control for all of its attributes and their values, which the PropertyPane object then displays to the user. It is a trivial matter at this point to allow the PropertyPane to be editable, modifying each attribute as the user sees fit.
Exposing Attributes to Scripting Languages
Embedding a scripting language into your application is fairly simple. However, exposing your C++ objects to that scripting language is complex and time consuming, typically entailing modification of each and every object that you want exposed to the interpreter and creating separate, language specific definition and or code files for each object.
If you need your scripting language to be able to create instances of your C++ objects, this may be your only alternative.
Often, all you need is to allow the embedded interpreter to query those objects for values and to change those values through a script. AttributeContainer can make this job much easier. Consider the Python script in listing 5:
Listing 5
import game
# Locate the actor 'Player1'
# Note that LocateActor returns a type 'long' that indicates the actors GUID
# or -1 if the actor cannot be found
nActor = game.LocateActor("Player1")
if nActor > -1:
# Locate one of the actors bound attributes
szName = game.GetAttribute(nActor, "Name")
# Ok, now change the value of the attribute
szName = game.SetAttribute(nActor, "Name", "Joe")
else:
print "Unable to locate 'Player1' anywhere."
After embedding Python into the application, all that was needed for the interpreter to be able to query and change attributes were three methods, which I had defined as:
PyObject* pyGetAttribute(PyObject* self, PyObject* args);
PyObject* pySetAttribute(PyObject* self, PyObject* args);
PyObject* pyLocateActor(PyObject* self, PyObject* args);
static PyMethodDef pyMethods[] =
{
{"LocateActor", pyLocateActor, METH_VARARGS, "Locate an actor."},
{"GetAttribute", pyGetAttribute, METH_VARARGS, "Get an actor attribute."},
{"SetAttriubte", pySetAttribute, METH_VARARGS, "Set an actor attribute."},
{NULL, NULL, 0, NULL}
};
When exposing C++ objects to embedded interpreters, it is often necessary to wrap the objects in obscure macros and write separate definition files in order for the interpreter to gain even the most basic access to the objects or their data. The method described here avoids the need for such practices.
Controlling the Output Format of Specific Types
I mentioned earlier that all that was needed to support a given type as an Attribute is that the input and output stream operators exist for that type.
While this is true, relying on the standard operators for default types may not give you the results you expect or want. C++ stream operators provide several manipulators and output format controls for controlling the output of a variable to the stream.
You might, for example, want your floating point types to output in fixed notation rather than scientific. You do not have to sacrifice this ability when using Attributes.
Using partial template specialization you can override the default serialization for any type of object. Essentially, output to a stream is accomplished by first converting the object to a string representation through a call to the ‘ToString’ method of the Attribute class.
Input is done the same way by calling FromString. Providing specialized versions of these methods gives you the means to completely control the format of types which have already implemented the << and >> stream operators.
Here is the template specialization for ‘Attribute<double>::ToString()’:
Listing 6
template <>
_string Attribute<double>::ToString()
{
_ostringstream s;
if ( m_pData )
s << setiosflags(std::ios::fixed) << *m_pData;
return s.str();
}
You may notice in the included source code that template specializations are also provided for both the ToString and FromString methods of the default string type. This is done as a performance enhancement to avoid using stringstream to ‘convert’ std::string objects.
Possible Changes
The included library uses a policy pattern for implementing the IO of the AttributeContainer. One drawback to this pattern is that it does not provide for different IO mechanisms. As written, if you want to serialize to more than one type of medium, you have to use different AttributeContainers.
One solution around this problem is to use the strategy pattern instead. Policies are compile-time bound, whereas strategies can be changed at run-time. I leave this implementation up to you. You can find more information on strategies in any number of books on patterns.
Performance Considerations
It is always important to know your tools and how they will impact the overall performance characteristics of your software. Attribute and AttributeContainer, while they are very useful and convenient tools, do have some important considerations regarding memory.
On the Windows operating system, using Visual Studio, Attribute uses 52 bytes of memory in debug, and 44 in release. Since Attribute stores only a pointer to the variable, the size of the variable type is irrelevant. AttributeContainer uses 36 bytes. It is important to note however, that these values can be significantly reduced with some careful thought.
The version of the library that accompanies this article was written to be easily understandable and easily expanded. It is not optimized for memory use. Here are some suggestions to optimize the memory use to better suit your specific needs:
Change the type of the variable used to hold the behavior flags in Attribute from the default unsigned long (4 bytes) to a short int (2 bytes) or unsigned char (1 byte). This will reduce the total number of flags that your library can support, but once you know how many flags you need you can reduce this variable to a more reasonable size.
Change the type of the string object to char* or to a string class with a smaller memory footprint than std::string. Keep in mind that while std::string seems excessive in its memory footprint, many implementations of it use a shared string manager to reduce the overall memory requirements. 32 bytes of the total memory used by Attribute is used by std::string.
Consider removing the ‘bind order' variable entirely. AttributeContainer keeps track of the order in which Attributes are bound so that it can write them out to xml or otherwise list them in the same order. This is a convenience that can likely be lived without in environments with rigid memory requirements. This is another candidate for an #ifdef. It might be useful in your UI editor to list all of the Attributes in the order they were bound, but you may not need that functionality in your release code.
Consider using a Proxy object rather than having your object inherit directly from AttributeContainer. In this case, you will most likely be swapping memory for performance issues.
Summary
In closing, let's revisit my original list of requirements for the Attribute library:
The library does not require sweeping changes to existing types or methods.
The library is generic and supports complex types. Furthermore, it allows for full control over output through partial template specialization.
While the memory footprint is larger than I would have liked, it is manageable and easily modified to decrease the overall size. I mentioned using Proxy objects above. Proxy objects can be the answer to the memory problem, swapping run-time processing for memory use.
Attribute behaviors are registered at the time the Attribute is bound to the AttributeContainer. These behaviors can be obtained and modified procedurally.
The AttributeContainer provides a mechanism to retrieve all of the bound Attribute names and values. This aids in exposing attributes to user interfaces and scripts.
Serialization is accomplished through the AttributeContainerIO policy class and is limited in automation only by the implementation of this policy.
Attribute values can be modified indirectly through the SetValue() method.
Since Attributes are nothing more than named keys to the underlying memory address of the variable, any change to that variable is immediately visible to the Attribute class.
To be fair, I agonized over writing this article. Attribute and AttributeManager, while useful, have their limitations as well. They can be a great aid in simplifying I/O and are tremendously useful in exposing variables to scripting engines and user interfaces, but this functionality comes at a cost.
Used wisely, this library can save you a great deal of time and simplify debugging efforts. Use it without thought, and it may quickly spiral into a resource nightmare.
Note: Alongside the article, we present zip files which contain all of the article code in two formats. The first zip file contains only the code necessary for the library and full documentation. The second file contains all of the first zip, but it also contains a visual studio solution file with several examples and unit test code for verifying that the library does what it says it does.
Though the versions hosted by Gamasutra are current as of this posting, the author's website hosts these files as well; any updates that may be made to these files will be reflected in these versions: file 1, file 2.
For More Information
Design Patterns: Elements of Reusable Object-Oriented Software
ISBN: 0201633612
By Erich Gamma, Richard Helm, Ralph Johnson, and John M. Vlissides (Nov 10, 1994)
Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library
ISBN: 0201749629
by Scott Meyers (Jun 16, 2001)
I should point out that this is but one of many different implementations. Several authors have implemented similar libraries. Here are two that I know of:
The Boost Fusion library: http://spirit.sourceforge.net/dl_more/fusion_v2/libs/fusion/doc/html/index.html
The fusion library from boost is, like all things boost, complex and wrought with deep interdependencies on numerous other boost components. I like boost as a testbed, but its complexity and sheer size far outweigh its usefulness in real world applications.
Property class for Generic C++ Member Access, by Charles Cafrelli
Game Gems, Book 1. http://www.charlesriver.com/Books/BookDetail.aspx?productID=8714
The property class implemented by Mr. Cafrelli was the inspiration for this library. I wanted an implementation that was truly and fully generic, one that did not necessarily require a code change to support every new type.
Read more about:
FeaturesAbout the Author
You May Also Like