|
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.
|
After rereading the article, I see you mentioned this refactor potential:
"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."
There are certainly some not-too-difficult solutions that simplify the global shared container pattern for client code.
In a system I worked with, the following client code would serialize an object's properties to a stream:
Object *pObject = ...;
MutableString mutstr;
StringOutputStream sos(mutstr);
XmlSerializer ser(os);
ser.Serialize(pObject);