问题
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 forvel.dx = 0.;
line) - https://medium.com/@savas/nomad-game-engine-part-2-ecs-9132829188e5
(blog - search forint currentHealth;
line) - https://www.randygaul.net/2013/05/20/component-based-engine-design/
(blog - search forcomp->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 forplayerGunComponent.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:-
- 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
, andCat_
. The correct pointer/handle of pimpl has to be assigned too.
^ 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 everyCat_OO
will also be in the returned list. - No refactoring need.
回答1:
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.
来源:https://stackoverflow.com/questions/56846715/spliting-component-in-entity-component-system-demands-too-much-refactoring