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. :-
vel.dx = 0.;
line)int currentHealth;
line)comp->DoStuff( dt );
line)playerGunComponent.timer += _time.deltaTime;
)Yuri's answer is a cool technique, but it still requires some refactoring.
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:-
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_
. The correct pointer/handle of pimpl has to be assigned too.
^ Both actions can use callback.
Disadvantages are :-
BaseComponent
, I need virtual inheritance. (waste a lot of memory)Advantages are :-
getAll<Hp_OO>()
, Hp_OO
of every Cat_OO
will also be in the returned list. 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With