1
\$\begingroup\$

This is not a specific question, but rather someone who is looking for opinions about a typical design problem. In a component based data-driven games, I've often seen something similar to the following when accessing attributes:

int* health = mOwner->get< int >( HEALTH_ATTRIBUTE );

if (health)
    *health -= 10;

Or, alternatively:

if (mOwner->hasAttribute( HEALTH_ATTRIBUTE ))
{
    int health = mOwner->get< int >( HEALTH_ATTRIBUTE );
    mOwner->set( HEALTH_ATTRIBUTE, health - 10 );
}

Where mOwner is a pointer to the parent game object. In this example, assume that the game object stores a map of attributes which are downcasted to the correct type at runtime.

There are some problems with this approach. Mainly, it is prone to bugs since it requires the programmer to know the the type of the attribute they are after. If I was working on a small team or by myself, this wouldn't be an issue. However for my requirements, this is undesired.

The only way to have compile-time checks is to create a helper function for each attribute that returns the proper type, so our code would then look like:

int* health = Attributes::getHealth( mOwner );

Where Attributes is declared as a namespace. Because the type is known at compile time, this gives us several advantages. In addition to the compile-time check, we gain the flexibility to easily declare certain attributes as constant, or that can't be modified outside the game object, etc. Of course, the downside is that it becomes necessary to manually create a new function for each and every attribute in the game.

Thoughts? Opinions?

EDIT:

After thinking it through a little more, I realized there is a way to define the function once using templates to avoid code duplication. For example:

template <typename Type, int ID>
class Attribute
{
public:
    typedef Type type;

    static int getID()
    {
        return ID;
    }

    static type getData( const GameObject* obj )
    {
        return obj->getAttribute < type >( ID );
    }

};

And then for each attribute we have in our game,

typedef Attribute< std::string, MODEL_NAME > ModelName;
typedef Attribute< Vector3, POSITION > Position;
typedef Attribute< int, HEALTH > Health;

/// ... and so on ... 

And when we want to access an attribute:

int health = Health::getData( mOwner );

Or alternatively, if we want to:

Health::type health = Health::getData( mOwner );

If we don't know the health attribute exists or not:

int* health = Health::getData( mOwner );

if (health)
   // ....

I think this approach gives a lot of flexibility while still maintaining type safety.

\$\endgroup\$
8
  • 4
    \$\begingroup\$ What is the actual question? Asking for "thoughts or opinions" is too discussion-oriented for GDSE (you could try the chat though). \$\endgroup\$
    – user1430
    Jan 3, 2013 at 19:52
  • \$\begingroup\$ I don't have a specific question. I'm simply looking for an opinion on what I've posted or an alternative solution to the problem. Perhaps someone has done this before and found a better way? \$\endgroup\$
    – jlanisdev
    Jan 3, 2013 at 20:00
  • \$\begingroup\$ GDSE is a Q&A site, though, not a discussion forum. Other places are better suited for opinion-only chat questions (and consequently I've voted to close this). \$\endgroup\$
    – user1430
    Jan 3, 2013 at 20:04
  • 2
    \$\begingroup\$ If you do post the question somewhere else, could you share the link? The topic sounds interesting \$\endgroup\$
    – nairdaen
    Jan 3, 2013 at 20:49
  • 1
    \$\begingroup\$ You cannot set a template parameter to be a string value. There's however a workaround for that. Check this paste: gist.github.com/4448868. This covers case of when an object doesn't have a specified attribute. You can work on the attribute as if the value always exists, but setting back the value will not add that attribute to the object. However you can still check if an object has a specific attribute and add if required. Check the usage section. \$\endgroup\$ Jan 4, 2013 at 0:43

1 Answer 1

1
\$\begingroup\$

The level of abstraction that is done here is one that I often consider too far when it comes to a component based system. It is also one that seems more and more predominant in general software development. To me both of the examples you have given have gone too far in that there is a generic interface for accessing any type of attribute from a given object. This is not useful as the default interface and leads to the situations you have described.

My components contain data as well as logic and are focused on handling a single area. Since we are using health I will just continue with that. At the root of a inheritence tree I will have an IComponent object that gives you the very basic interface to the object that all components must complete. This gives typedef/load/save capabilities of all components. From there I would then have an ICmpHealth interface that would provide typed accessors to the data values that you would expect to deal with when looking at the health of an object. It is this interface that gives you the specific data types like int ICmpHealth::GetCurrentHealth() or ICmpHealth::GetMaxHealth().

Now when dealing with a data oriented design these are actually static accessors for me as they are acting on contiguous data blocks with offsets.. But in a general system this would replace the calls above to getting a specific attribute to getting a component interface object so something like

ICmpHealth * cmpHealth = mOwner->GetComponent(HEALTH);
if (!cmpHealth)
    throw new Exception("Expected Component for Object not available");
int health = cmpHealth->GetCurrentHealth()
cmpHealth->SetCurrentHealth()

So this looks like just a 'we moved the problem around' sort of answer but I want to point out what you get for this. You already have to be aware of the types of components you are using in order to use them. Same could be said about needing to know the Attributes but the scale is much smaller as the components should contain multiple attributes. What this also gives you type check safety when setting any of the attributes for a component as you are now using a specific API in order to do so and are getting compiler type validation for accessing them.

Hope this helps.

\$\endgroup\$
2
  • \$\begingroup\$ This is what I was doing previously, and it seems to be a very common design pattern. However I feel that keeping the attributes and components (data and logic, respectively) separate give more flexibility. For example, if components (or Behaviors) don't know about each other, then you can more easily re-factor them later on without breaking other code. In addition, a common interface for accessing and modifying attributes means that you can post notifications whenever they get modified, which could be useful. However, I agree that your solution is just as valid. \$\endgroup\$
    – jlanisdev
    Jan 4, 2013 at 0:04
  • 1
    \$\begingroup\$ I see making an easy interface to access an attribute of an object via templates just a rewrite of what the basic C++ language gives you already with you having to write all the handling code.. like what you are doing above. It is all about finding the balance between as abstract as the language itself or tied down to a class hierarchy and where you want your code to be between them I guess. \$\endgroup\$
    – James
    Jan 4, 2013 at 0:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .