2
\$\begingroup\$

Question

A component based system's goal is to solve the problems that derives from inheritance: for example the fact that some parts of the code (that are called components) are reused by very different classes that, hypothetically, would lie in a very different branch of the inheritance tree.

That's a very nice concept, but I've found out that CBS is often hard to accomplish without using ugly hacks. Implementations of this system are often far from clean. But I don't want to discuss this any further.

My question is: how can I solve the same problems a CBS try to solve with a very clean interface? (possibly with examples, there are a lot of abstract talks about the "perfect" design already).

Context

Here's an example I was going for before realizing I was just reinventing inheritance again:

class Human {
public:
    Position position;
    Movement movement;
    Sprite   sprite;
    // other human specific components
};

class Zombie {
    Position position;
    Movement movement;
    Sprite   sprite;
    // other zombie specific components
};

After writing that I realized I needed an interface, otherwise I would have needed N containers for N different types of objects (or to use boost::variant to gather them all together). So I've thought of polymorphism (move what systems do in a CBS design into class specific functions):

class Entity {
public:
    virtual void on_event(Event) {} // not pure virtual on purpose
    virtual void on_update(World) {}
    virtual void on_draw(Window) {}
};

class Human : public Entity {
private:
    Position position;
    Movement movement;
    Sprite   sprite;
public:
    virtual void on_event(Event) { ... }
    virtual void on_update(World) { ... }
    virtual void on_draw(Window) { ... }
};

class Zombie : public Entity {
private:
    Position position;
    Movement movement;
    Sprite   sprite;
public:
    virtual void on_event(Event) { ... }
    virtual void on_update(World) { ... }
    virtual void on_draw(Window) { ... }
};

Which was nice, except for the fact that now the outside world would not even be able to know where a Human is positioned (it does not have access to its position member). That would be useful to track the player position for collision detection or if on_update the Zombie would want to track down its nearest human to move towards him.

So I added const Position& get_position() const; to both the Zombie and Human classes. And then I realized that both functionality were shared, so it should have gone to the common base class: Entity. Do you notice anything? Yes, with that methodology I would have a god Entity class full of common functionality (which is the thing I was trying to avoid in the first place).

Meaning of "hacks" in the implementation I'm referring to

I'm talking about the implementations that defines Entities as simple IDs to which components are dynamically attached. Their implementation can vary from C-stylish:

int last_id;
Position* positions[MAX_ENTITIES];
Movement* movements[MAX_ENTITIES];

Where positions[i], movements[i], component[i], ... make up the entity. Or to more C++-style:

int last_id;
std::map<int, Position> positions;
std::map<int, Movement> movements;

From which systems can detect if an entity/id can have attached components.

\$\endgroup\$
2
  • \$\begingroup\$ Just leverage the same fundamental concepts behind most component systems: segregate functionality, prefer aggregation, and support data-driven behavior. There's nothing magical there. I've seen a lot of the 'dirty' implementations you are alluding to, and it usually due to over-engineering; building solutions to problems that don't exist. \$\endgroup\$
    – user1430
    Oct 22, 2013 at 15:43
  • \$\begingroup\$ It's pretty difficult to answer without discussing your point of view. As someone could easily just say, you can solve it by using an existing implementation (since they don't agree that the existing implementations have hacks). It's too broad because unless the hacks you're talking about are universally agreed upon, you'll get people suggesting how to avoid what they think are the hacks. It's why you need to pick one of the problem hacks, specify exactly what you think the problem is, and why it's a problem for you. \$\endgroup\$
    – House
    Oct 22, 2013 at 15:54

2 Answers 2

4
\$\begingroup\$

First, I would hardly consider the practice of making entities IDs to which a component is attached a "hack," it is rather a conscious design decision that has pros and cons -- none of which are really relevant to your actual problem, though, so I won't comment further except to note that this goes towards Byte56's comment about the subjectivity of the term.

It seems to me that your problem is that you made too large of a leap between stages 1 and 2 of your example. You don't need go directly from unrelated Player and Zombie types to having them inherit a super-generic interface you will then use for every type ever in your game. You just need an interface that covers your immediate needs right now:

struct Character {
   const Position & GetPosition () const;

   // ....
};

struct Zombie : Character { /* ... */ };
struct Player : Character { /* ... */ };

This allows you to treat all objects in your game that act like humanoid characters -- which have a position in the world, probably have health and speed stats, et cetera -- in a uniform fashion, but doesn't force you to treat other objects (such as bullets, which probably don't need health) that way. Even though a bullet will have a position, it doesn't need to be a Character and you don't need to cram it into that inheritance hierarchy at all.

The interfaces a type implements should be based on how that type may be used, not how it actually functions. It's okay to have two different classes that each have a function to get a position that isn't the same function. Sometimes you need to use that data in two different ways (consider the circle-ellipse problem, which leverages a common-but-flawed example used to academically demonstrate inheritance). And if you ever need to use that data in the same way, that's when you can consider what to do based on your actual, practical needs at the time.

The principle at work here is that you should evolve your interfaces by making the least functional thing that could work. Expose only the data and types you need now, expose only the relationships you need now... name things accordingly.

This allows you to control the growth of dependency on the interface more easily, which is more manageable in the long run.

\$\endgroup\$
1
\$\begingroup\$

Inheritance is a perfectly valid thing to use where it makes sense and makes your code easier to use. Don't believe that components are just about removing inheritance; they're not. Also don't believe that you have a strong need to avoid inheritance in every case or that components means you shouldn't be using it.

For your design, consider using a virtual interface in Entity for optional components. Something like this:

class Entity {
public:
  virtual Position* getPosition() { return nullptr; }
  virtual Movement* getMovement() { return nullptr; }
  virtual Sprite* getSprite() { return nullptr; }
};

Now your individual game object types can optionally provide any of the known components they need and you can easily query whether a particular object has a component or not by checking if the corresponding getter returns nullptr.

class Item : public Entity {
  Position m_position;
  Sprite m_sprite;
public:
  virtual Position* getPosition() override { return &m_position; }
  // getMovement() is not overridden as we have no Movement component
  virtual Sprite* getSprite() override { return &m_sprite; }
};

If you want to have a system or container that keeps track of all instances of a particular component, you can do something like:

void PositionSystem::onAddEntity(Entity* entity) {
  Position* position = entity->getPosition();
  if (position != nullptr)
     registerPosition(entity, position);
}
\$\endgroup\$
4
  • \$\begingroup\$ That reminds me of the interface I proposed a while back asking if it was a "valid instance of ECS". Then I deleted it, but still... \$\endgroup\$
    – Shoe
    Oct 22, 2013 at 19:03
  • \$\begingroup\$ It just seems that Component-Entity Systems are bad at scaling. For every new type of component you plan on adding (or didn't think of and need to add later) you have to add it to Entity base, and heaven forbid if an entity needs to have the new one or the world needs to check for the new one on every entity. \$\endgroup\$
    – Casey
    Oct 22, 2013 at 19:34
  • 1
    \$\begingroup\$ @Casey: This is not an Entity Component System. It is a simplistic approach using component-based design. Other approaches make it very simple to create new components, add or remove components to existing objects, and remove any need to modify base interfaces; of course, these other approaches are far more complicated and aren't implicitly worth the implementation cost for every project. \$\endgroup\$ Oct 22, 2013 at 21:29
  • \$\begingroup\$ Also if you are planning on being const-correct you are going to need to duplicate the number of functions in the base class. \$\endgroup\$
    – Shoe
    Oct 23, 2013 at 17:26

You must log in to answer this question.

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