将实体-组件-系统中的组件拆分需要进行过多的重构。

7
我有一份现有的C++游戏库,使用实体-组件-系统(ECS)。我的库的使用者想要创建一些组件,例如Cat
class Cat{ public:
    int hp;
    float flyPower;
};

他可以通过以下方式修改每个猫的hp:
for(SmartComponentPtr<Cat> cat : getAll<Cat>()){
    cat->hp-=5;  //#1
}

几天后,他想将 Cat 分割成 HPFlyable :-

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

因此,每个访问 hpcat 都会编译错误(例如在上面代码中的 #1 处)。
为了解决这个问题,用户可以重构他的代码为:-
for(MyTuple<HP,Flyable> catTuple : getAllTuple<HP,Flyable>()){
    SmartComponentPtr<HP> hpPtr=catTuple ; //<-- some magic casting
    hpPtr->hp-=5; 
}

它能够工作,但需要用户代码中的大量重构(调用cat->hp的各个位置)。

如何编辑框架/引擎以解决在ECS中拆分组件时的可维护性问题?

我从未发现任何不受此问题困扰的方法,例如:

悬赏原因

Yuri的答案是一种很酷的技术,但仍需要进行一些重构。

我当前糟糕的解决方案(pimpl)

如果我想创建Cat,我将创建6个组件:

  • Hp_Hp_OO
  • Flyable_Flyable_OO
  • Cat_Cat_OO

下面是一个代码示例:

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;
};

现在,可以这样调用:

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

具体实现

  1. 如果用户将Cat_OO添加到实体中,我的游戏引擎将自动将其父类添加到实体中,例如Hp_Hp_OOFlyable_Flyable_OOCat_
  2. 必须分配正确的pimpl指针/句柄。

  3. ^ 两个操作都可以使用回调函数。

缺点是:

  • 需要创建大量组件(浪费内存)。
  • 如果存在一个公共的基类,例如BaseComponent,我需要虚继承(浪费大量内存)。

优点是:

  • 如果用户查询getAll<Hp_OO>(),则每个Cat_OOHp_OO也将包含在返回的列表中。
  • 不需要进行重构。

2
提示:class Cat{ public: -> struct Cat { - Quentin
3
如果你将一个组件分成两种类型,那么在你之前请求原始组件的地方,最可能并不希望获得它们两个。否则这个拆分就是无意义的浪费,对吧?因此,在许多情况下,任何自动机制都不能解决这个问题。工业界的人只能根据新定义的领域来重构它。 - skypjack
@skypjack 是的,在大多数情况下是正确的。你的逻辑对我很有意义。谢谢!我有点贪心,想支持那些要求原始版本的少数人。 - javaLover
1
@javaLover 别这样做。这就像试图阻止用户发生所有错误一样。这是不可能的,总会有人能够想出比你曾经想过的更愚蠢的事情。专注于常见情况和提供良好的API、一组不错的功能等方面。这是我的看法。顺便问一下,你的项目公开吗? - skypjack
@skypjack 抱歉,不行。这是一个私人项目。感谢你的建议。 - javaLover
显示剩余2条评论
1个回答

6

成员指针来拯救:

#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;
    }
}

技术上来说,你仍然需要重构,但是使用自动替换&Cat::hp&HP::hp等操作很简单。


谢谢提供一个不错的替代方法。它看起来有些丑陋,正如你所提到的,还需要进行一些重构(将所有的 Cat::hp 替换为 HP::hp)。也许没有比这更好的解决方案了。我想知道业界的人是如何解决这个问题的。 - javaLover
我喜欢它。非常好。这不是你的错,C++委员会认为这是成员指针的最佳语法。而且,在任何解决方案中,更改旧类名为新类名的重构在循环内部都是必要的。 - Jerry Jeremiah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接