5

I've read a few articles about the Entity-Component style of programming. One of the common problems posed is how to express dependencies between components, and how components related to the same entity communicate.

It seems to me that a simple solution to this problem is to make each dependency a virtual base class of its dependant.

That way, when a component is included in an entity (via virtual inheritance), all of the dependant components are included exactly once. Additionally, all of the functionality that a component depends upon will be available in its member functions.

class C_RigidBody : public virtual C_Transform {
    public void tick(float dt);
};

class C_Explodes : public virtual C_Transform {
    public void explode();
};

class E_Grenade : public virtual C_RigidBody, public virtual C_Explodes {
    //no members
};

Is there any reason no one does this?

(I realize that multiple inheritance is usually frowned upon due to the "diamond problem," but this problem is something components have to deal with anyway. (Imagine how many components would depend on an entity's position in the game world))

Dan
  • 12,409
  • 3
  • 50
  • 87
  • It could work (like how you make exception inheriting from std::exception). could you post an example? – Syl Feb 11 '14 at 23:26
  • There are of course the obvious drawbacks, none of which seem major to me and all of which I suppose you've already considered. Performance penalty for virtual classes, couldn't have any circular dependencies, entity can only have one instance of each component, namespacing issues. – Pace Feb 12 '14 at 17:09
  • Can you think of a use case for an entity having more than one instance of a component? – Dan Feb 12 '14 at 17:51
  • More than one component instance per entity is a smell. At that point it becomes a system. – Samuel Danielson Nov 17 '14 at 04:11
  • I did it, but [it](https://stackoverflow.com/questions/56846715/spliting-component-in-entity-component-system-demands-too-much-refactoring) (see my poor solution) is 1. hard 2. ugly (in implementation) 3. need more memory to make it pool and flexible. .... Dan, I am curious. How do you actually solve it? – javaLover Oct 24 '19 at 01:26

2 Answers2

2

I recently come up with the same idea with you too.

In theory I blieve that this approach is a perfect solution for Inverse of Dependency if properly applied. Otherwise you'll mess things up.

Recalling what DIP states:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • Abstractions should not depend on details. Details should depend on abstractions.

See the following code example

struct IClockService
{
    virtual unsigned timestamp() = 0;
};

struct ITimingService
{
    virtual unsigned timing(std::function<void()> f) = 0;
};

struct ClockService : public virtual IClockService // public virtual means implement
{
    virtual unsigned timestamp() { return std::time(nullptr); }
};

struct TimingService : public virtual ITimingService
                     , private virtual IClockService // private virtual means dependency
{
    virtual unsigned timing(std::function<void()> f)
    {
        auto begin = timestamp();
        f();
        return timestamp() - begin;
    }
};

class Application : public ClockService
                  , public TimingService
{
};
Application a; 
unsigned runingTime = a.timing(anyFunction);

In the above example. ClockService and TiminigService are two module, they don't need to know each other but only the interface IClockService and ITimingService. And the Application composits the two modules together.

In conclusion:

  • Map depend as private virtual inheritance, otherwise breaks Liskov-Subtitution Principle
  • Map implements as public virtual inheritance.
  • Module implementation only depends on abstraction, this is what DIP required.
  • Compositing all module together via normal inheritance, if any compilation occurred, that would means there is unmet dependency or multiple-implementation for an interface.

I do only apply such idioms in my toy projects. Take it at your own risk.

thynson
  • 51
  • 6
0

A lot of this answer is going to be based on conjecture, but since no one has attempted one, here's my attempt.

Composition over Inheritance

At the heart of ECS is an aesthetic favoring composition over inheritance. Inheritance was designed, first and foremost, to model an "is-a" relationship through a hierarchy. While it certainly has more uses beyond that, and even things like policy classes which depend on going beyond it, at the heart of it is that kind of "aesthetic".

"Aesthetic" here often evolves out of human tendencies. In an ideal world, a pragmatic team benefits from more flexible tools, uses them to greater benefit. Unfortunately, sometimes the reality is that a team is only as strong as the things the weakest link can't screw up.

Suddenly when you start inheriting things like a transform or a position, it likewise opens up the doors for that weakest link to start working with such entity-component relationships as though they model the "is-a" relationship (ex: dynamic down-casting of a transform to a rigid body, trying to destroy a grenade resource by destroying its position and forgetting to make the dtor of the position virtual or at least protected and non-virtual, even more obscure cases like inadvertent slicing).

Typically we're never exposed to such naivety, but I've seen it too many times to have a very optimistic perspective. Composition makes such scenarios impossible, or at least mitigates the impacts and costs associated with a lot of them. It's a more restrictive tool that reduces flexibility, imposes more constraints, and sometimes restricting freedom and flexibility in this sense is desirable in a team setting as a kind of heavy-handed way to avoid Murphy's Law. This is always going to be somewhat subjective since it's based on a prediction of human tendencies which is never going to be perfect, but humans tend to screw up composition less than deeply nested inheritance hierarchies. It tends to require more effort and boilerplate upfront but there tends to be less potential to seriously screw up.

Runtime Extensibility

This only applies to certain engines, but sometimes engines want to allow further programming of its entities at runtime, including the introduction of new components, extension of existing entities, etc. without a static compilation process. For example, new components, entities, or extensions to existing entities might be applied through a scripting language (embedded Lua, e.g.), or a proprietary nodal programming language that lets designer-types without a strong programming background compose new entities.

In those cases, inheritance becomes too much of a hard-coded static compilation concept to extend at runtime. This only applies for select engines, but it demonstrates a scenario where deliberately setting out to avoid inheritance can actually increase flexibility of another kind (specifically at runtime).

That said, I think there's nothing particularly wrong with your proposal given the right kind of team, standards, requirements. But these two points might help explain why it's somewhat rare to use inheritance to model an entity-component system.

There are some other potential issues like dependencies to RTTI and dynamic casting to determine what components are available, increased difficulties in preserving ABI, vptr overhead, etc. that I could go into if desired. A lot of it is just getting back to the heart of composition vs. inheritance.

  • How would you do this using composition? – Dan Nov 17 '15 at 10:58
  • 1
    Just generally speaking? Often entities are simple aggregates associating components to IDs of some sort -- we fetch the appropriate component available through an ID (perhaps returning a null/nil if the component is unavailable). Depending on the language, there could sometimes be a cast involved (something resembling a downcast). Here's one such example in C++ which models the pure kind of ECS with entities being simple aggregates, though it loses the benefits I cited above of runtime extensibility since it relies heavily on code generation: https://github.com/alecthomas/entityx –  Nov 17 '15 at 11:14
  • Another kind of approach that's in between these two is to inherit pure virtual interfaces -- no state, just the interface. Then aggregate components in an entity and return the appropriate interface for the appropriate, available component through an interface ID. In that case the entity is still aggregating concrete components through composition, but aggregating their interfaces through inheritance. The techniques vary wildly and with the languages employed, but often at the heart of it is an emphasis on composition over inheritance. –  Nov 17 '15 at 11:15
  • That kind of loses some of those benefits though. If you look at systems like Unity or this EntityX library above, you can create new entities without modifying their class definition, e.g. New components can be added to existing entities without modifying their source code, and without coupling the component to the entity that contains it. That might be a key factor favoring composition to inheritance -- the greatest flexibility can come from the ability to compose a new entity without modifying its implementation (put crudely, sort of like being able make a class "inherit"... –  Nov 17 '15 at 11:34
  • ... something new without actually changing its definition at all). A lot of these systems depend on a less hard-coded form of "composability" -- to be able to create new combinations without hard-coding (inheritance inevitably tends to be quite hard-coded). –  Nov 17 '15 at 11:34