Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spliting component in Entity-Component-System demands too much refactoring

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. :-

  • https://github.com/skypjack/entt
    (opensource - search for vel.dx = 0.; line)
  • https://medium.com/@savas/nomad-game-engine-part-2-ecs-9132829188e5
    (blog - search for int currentHealth; line)
  • https://www.randygaul.net/2013/05/20/component-based-engine-design/
    (blog - search for comp->DoStuff( dt ); line)
  • (C#, Unity3D) http://www.sebaslab.com/learning-svelto-ecs-by-example-the-unity-survival-example/
    (a blog refereed by https://codereview.stackexchange.com/questions/48536/an-ecs-model-for-game-development ;
    search for playerGunComponent.timer += _time.deltaTime;)

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.
like image 646
javaLover Avatar asked Jul 02 '19 06:07

javaLover


1 Answers

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.

like image 196
yuri kilochek Avatar answered Nov 15 '22 14:11

yuri kilochek