7

I have an existing working C++ game library that use Entity-Component-System (ECS).

User of my library would like to create some components e.g. Cat :-

class Cat{ public:
    int hp;
    float flyPower;
};

He can modify hp of every cat by e.g. :-

for(SmartComponentPtr<Cat> cat : getAll<Cat>()){
    cat->hp-=5;  //#1
}

Some days later, he want to split Cat to HP and Flyable :-

class HP{ public:
    int hp;
};
class Flyable{ public:
    float flyPower;
};

Thus, every cat that access hp will compile error (e.g. at #1 in the above code).

To solve, user can refactor his code to :-

for(MyTuple<HP,Flyable> catTuple : getAllTuple<HP,Flyable>()){
    SmartComponentPtr<HP> hpPtr=catTuple ; //<-- some magic casting
    hpPtr->hp-=5; 
}

It works, but needs a lot of refactoring in user's code (various places that call cat->hp).

How to edit the framework/engine to solve maintainability issue when splitting component in ECS?

I have never found any approach that does not suffer from this issue e.g. :-

Bounty Reason

Yuri's answer is a cool technique, but it still requires some refactoring.

My poor current solution (pimpl)

If I want to create Cat, I will create 6 components :-

  • Hp_, Hp_OO
  • Flyable_, Flyable_OO
  • Cat_, Cat_OO

Here is a code example :-

class Hp_ : public BaseComponent{
    int hp=0;
};
class Hp_OO : public virtual BaseComponent{
    Hp_* hpPimpl;
    public: void damage(float dmg){ hpPimpl->hp-=dmg;}
};
class Flyable_  : public BaseComponent{ public:
    float flyPower;
};
class Flyable_OO: public virtual BaseComponent{
    Flyable_* flyPimpl;
    //other function
};
class Cat_: public virtual BaseComponent{};
class Cat_OO: public virtual Hp_OO , public virtual Flyable_OO{
   Cat_* catPimpl;
};

Now, it is valid to call :-

SmartComponentPtr<Cat_OO> catPtr;
catPtr->damage(5);   //: so convenient - no need to refactor 

Implementation:-

  1. If user adds Cat_OO to an entity, my game engine will automatically add its parent classes to the entity e.g. Hp_, Hp_OO ,Flyable_, Flyable_OO, and Cat_.
  2. The correct pointer/handle of pimpl has to be assigned too.

  3. ^ Both actions can use callback.

Disadvantages are :-

  • A lot of components need to be created. (waste memory)
  • If there is a common base class e.g. BaseComponent, I need virtual inheritance. (waste a lot of memory)

Advantages are :-

  • If a user query getAll<Hp_OO>(), Hp_OO of every Cat_OO will also be in the returned list.
  • No refactoring need.
javaLover
  • 6,347
  • 2
  • 22
  • 67
  • 2
    Tip: `class Cat{ public:` -> `struct Cat {` – Quentin Jul 02 '19 at 09:31
  • 3
    If you split a component in two types, most likely you don't want to get both of them where you asked for the original one before. Otherwise the split would be pointless and just a waste, right? Therefore, any automatic mechanism won't solve the problem in many cases. People in industry just refactor it the way it fits with the newly defined domain. – skypjack Jul 12 '19 at 07:48
  • @skypjack Yes, it is true in most cases. Your logic makes sense to me. Thank! I am greedy - want to support the minority where people ask for the original one too. – javaLover Jul 12 '19 at 09:48
  • 1
    @javaLover Just don't. It's like trying to prevent all the errors from the users. It's impossible, there will be ever someone able to think something more stupid than what you've ever thought. Concentrate on the common cases and on providing a good API, a nice to have set of features and so on. My two cents. Is your project public btw? – skypjack Jul 12 '19 at 09:49
  • @skypjack Sorry, no. It is a private project. Thank for the tips. – javaLover Jul 12 '19 at 09:52
  • When you get rid of a class (`Cat`, in this case), don't you inherently have to refactor all code that used to refer to that class? Every `Cat` -- not just those that access `hp` -- will generate a compiler error. (Your code suggests that the `Cat` class no longer exists, as opposed to being replaced by a `struct` containing `HP` and `Flyable` members. If `Cat` is supposed to still exist, please give its definition after the refactoring.) – JaMiT Jul 27 '19 at 16:31
  • @JaMiT Thank, I understand. I add "my current solution" to my question to describe it. – javaLover Jul 28 '19 at 02:14

1 Answers1

6

Member pointers to the rescue:

#include <tuple>

template <typename... Components>
struct MultiComponentPtr {
    explicit MultiComponentPtr(Components*... components)
        : components_{components...}
    {}

    template <typename Component, typename Type>
    Type& operator->*(Type Component::* member_ptr) const {
        return std::get<Component*>(components_)->*member_ptr;
    }

private:
    std::tuple<Components*...> components_;
};

struct Cat {
    int hp;
    float flyPower;
};

struct HP {
    int hp;    
};

struct Flyable {
    float flyPower;    
};

int main() {
    {
        Cat cat;
        MultiComponentPtr<Cat> ptr(&cat);
        ptr->*&Cat::hp += 1;
        ptr->*&Cat::flyPower += 1;
    }

    {
        HP hp;
        Flyable flyable;
        MultiComponentPtr<HP, Flyable> ptr(&hp, &flyable);
        ptr->*&HP::hp += 1;
        ptr->*&Flyable::flyPower += 1;
    }
}

Technically you still need to refactor, but it's trivial to auto-replace &Cat::hp with &HP::hp, etc.

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
  • Thank for a nice alternative. It is ugly, and as you mentioned, some refactoring is still required (find & replace all `Cat::hp` to `HP::hp`). There might be little possibility to have a better solution than this. I wonder how people in industry solve it. – javaLover Jul 03 '19 at 02:01
  • I like it. Very nice. It's not your fault the C++ committee thought that was the best syntax for member pointers. And the refactoring to change the old class name to the new one inside the loop will be necessary with any solution. – Jerry Jeremiah Jul 23 '19 at 03:50