Exposing object properties in C++
Contents
-
1. The problem
-
1.1. Properties
-
1.2. Trivial solution
-
2. Self-registering properties
-
2.1. A baseclass trick
-
2.2. Sets of properties
-
2.3. Adding typesafety
-
3. Code
This article is based on an original idea voiced on the
Software Engineering mailing list by Charles Cafrelli.
Written by Bert Peers, feedback very welcome on bpeers@acm.org
!
Thanks & enjoy..
Last update : 02/02/2000 09:40
1. The Problem
1.1. Properties
If you're building a game and use an object-oriented approach, you'll end up
with a number of objects which all have their own properties. In an RPG an
object, like a sword, may have a certain value, a certain weight, and a
certain name. The game code could represent this by having an object CSword,
which has member fields like GoldValue (a long), Weight (a float), and Name
(a string). Obviously, the actual game code will be tightly integrated with
these members : trading code will have to know exactly where to look to find
out how much an object costs; inventory code will have to know exactly where to
find the weight when checking if you can still carry an object. Adding a
new property, like "Attack strength" is not a big deal, since you will have
to write new game code anyway to deal with the property. Similarly, the fact
that you have to recompile the whole thing and change all the places an
obsoleted property (say "Weight") is referenced when dropping it from an object,
is not a big hassle either, because you would have to go and remove all the
Weight-handling code anyway.
The story is different when you're building an editor to set and tune these
properties. In the RPG example, it may take a while until you've found the
right combination of weight, value and offensive power to make the sword
a good gameplay element. The problem this article is about is how you can
connect the editor with the object. It seems trivial, so we immediately have
an apparent solution for this.
1.2. Trivial solution
So, what's the problem anyway ? First, you'd take the header files in which you
declare the object and load an object from disk (or have it imported from
a DLL or something). Then you'd use MFC or another GUI builder to build nice
dialogs full of properties, one for every member you see in the header file;
using data exchanges between the dialog and actual members in the object,
you can keep tuning the object, and when you're done, you just save the object
to disk again. Variations could be that instead of actually building an
in-game-like object, you just open a data file with some numbers representing
the object fields and edit those. In any case, you're directly matching the
GUI, and thus the editor, with what's declared in the header files.
This works fine, until a property changes. Everytime a property is added,
removed, or changes type (say from long to float), the editor has to be modified
accordingly by adding, removing or changing the GUI elements, and ofcourse
the input/output code of the editor. If you already have the perfect object
layout, this may look like a bogus argument; and indeed, if you're building
a relatively simple game, or you're building a game that's been done so
often that you practically know for sure what properties your objects will
have, the rest of the article may be overkill for the project. However, if
you're prototyping a relatively new game where properties keep being added
and changed, or you're working on an engine for a gametype which may need
tweaking by several interested parties, an explosion of editors could
result, each for a slightly different version of the engine.
Total coolness would be achieved if the editor could somehow ask the
object itself what kind of properties it has, and then present a GUI
to edit these properties according to their type : a dial for numerical
values, a checkbox for booleans, a textfield for strings. It turns out
this is in fact surprisingly simple to do.
2. Self-registering properties
2.1. A baseclass trick
In the source code, each property can be described by using it's name,
such as "Weight". Once compiled, this name is lost and the value it
represents can only be retrieved by reading out some position in memory.
What we need is a way to find this position, given the name, at runtime :
specifying the name of the property we look for as a string, should give
us a pointer to where the matching property is. This can easily be done :
class CPropertyList
{
public:
std::string PropertyName;
long &PropertyLocation;
void Register (std::string Name, long *Long)
{ PropertyName = Name; PropertyLocation = Long; }
};
class CMyStuff : public CPropertyList
{
long Weight;
MyStuff ()
{
static std::string Weight_S ("Weight");
Register (Weight_S, &Weight);
}
};
|
Any object which (multiple) inherits from CPropertyList, can now be checked
to see if PropertyName matches a given string like "Weight". If it does, then
we know that PropertyLocation points to the member of Weight;
CMyStuff Stuff;
if (Stuff.PropertyName == "Weight") *Stuff.PropertyLocation = 5;
|
The trick here basically is that an object interested in exposing some of
it's properties, registers them at construction time; now, any other object
can ask such a CPropertyList object what property it has exposed by checking the
string.
2.2. Sets of properties
The next step is to map multiple strings to the corresponding variable.
Instead of just checking one string to find out what member a CPropertyList exposes,
we would search a map for the name of a member we're interested in, and when
found look at the corresponding pointer to access that member :
class CProperty
{
public:
union
{
long *Long;
};
};
class CPropertyList
{
public:
std::map <std::string, CProperty> PropertyList;
void Register (std::string Name, long *Long)
{
CProperty &P = PropertyList [Name];
P.Long = Long;
}
void Unregister (std::string Name)
{
std::map <std::string, CProperty>::iterator I = PropertyList.find (Name);
if (I != PropertyList.end ()) PropertyList.erase (I);
}
};
|
Now, a CPropertyList-inherited object registers it's longs just as before.
An interested client, like the editor, could now use an iterator to find ()
a member it's interested in, and when found, read/write it. Even more interesting is
the use of an iterator to run over every registered member and display an editing
control for every long found. This is what the editor should be doing :
use an iterator running from start () till end (), using the
names found to populate a drop down list. When the user selects a variable from the
drop down list, the editor pops up a control in a modal dialog box which allows the
user to enter a new value. That value is then stored in whatever location the
CProperty matching the string reports.
The important thing to realise here is that this happens completely at runtime. All the editor cares about is that the object
being edited is a CPropertyList; only the members of this CPropertyList type are used to
inspect and modify the members of the object being edited; that object can be changed
and recompiled at will. If the Editor just has a way to get such a new object without
recompilation (say using a CPropertyList *Factory () method in a DLL), it will
report any new properties just fine without any recompiling. All the modified object
has to do is make sure it registers the correct members at construction time; since this
is compile-time safe, you cannot register a variable that has been deleted. Consequently,
the editor cannot accidentally offer an editor box for a value that has been dropped,
and worse yet, continue by writing values at bogus locations; this already removes a
whole class of engine/editor versioning problems. At the same time, you get to decide
which new members get exposed and which remain hidden, which is not always the case
with some approaches based on virtual tricks.
2.3. Adding typesafety
Normally, you'd want to expose more than just longs : booleans, strings,
floats etc are typical properties. This can be done trivally by just expanding the
CProperty class, and adding Register code :
typedef enum { PropBool, PropString, PropFloat, PropLong, PropInt } TProperty;
class CProperty
{
public:
TProperty Type;
union
{
bool *Bool;
std::string *String;
float *Float;
long *Long;
int *Int;
};
};
class CPropertyList
{
public:
std::map <std::string, CProperty> PropertyList;
void Register (std::string Name, bool *Bool)
{ CProperty &P = PropertyList [Name]; P.Type = PropBool; P.Bool = Bool; }
void Register (std::string Name, std::string *String)
{ CProperty &P = PropertyList [Name]; P.Type = PropString; P.String = String; }
void Register (std::string Name, float *Float)
{ CProperty &P = PropertyList [Name]; P.Type = PropFloat; P.Float = Float; }
void Register (std::string Name, long *Long)
{ CProperty &P = PropertyList [Name]; P.Type = PropLong; P.Long = Long; }
void Register (std::string Name, int *Int)
{ CProperty &P = PropertyList [Name]; P.Type = PropInt; P.Int = Int; }
void Unregister (std::string Name)
{
std::map <std::string, CProperty>::iterator I = PropertyList.find (Name);
if (I != PropertyList.end ()) PropertyList.erase (I);
}
};
|
The editor queries the CPropertyList as before, but using a switch on the Type reported in the CProperty, it offers a slightly different modal dialog box. More advanced GUI programming could in fact skip the dialog box
altogether and just update the form holding the dropdown list and the controls, and enable/disable the appropriate controls when a selection from the list is made (say disabling
anything but the boolean check box when a variable is selected which is PropBool).
3. Code
Here is a header file which contains the above code. Just
#include it in the header file of an object you want to make a CPropertyList, expose the members you want in the constructor, and you're
ready to go.
|