使用多重继承来实现实体 - 组件编程

时间:2014-02-11 23:07:45

标签: c++ inheritance

我已经阅读了一些关于实体 - 组件编程风格的文章。提出的常见问题之一是如何表达组件之间的依赖关系,以及与同一实体相关的组件如何通信。

在我看来,这个问题的一个简单解决方案是使每个依赖项成为其依赖的虚拟基类。

这样,当组件包含在实体中时(通过虚拟继承),所有依赖组件只包含一次。此外,组件所依赖的所有功能都将在其成员函数中提供。

class C_RigidBody : public virtual C_Transform {
    public void tick(float dt);
};

class C_Explodes : public virtual C_Transform {
    public void explode();
};

class E_Grenade : public virtual C_RigidBody, public virtual C_Explodes {
    //no members
};

有没有人这样做?

(我意识到由于“钻石问题”,多重继承通常不受欢迎,但这个问题无论如何都是组件必须处理的问题。(想象一下,有多少组件将取决于实体在游戏世界中的位置))

2 个答案:

答案 0 :(得分:1)

我最近也和你提出了同样的想法。

从理论上讲,我认为如果正确应用,这种方法是依赖性反向的完美解决方案。否则你就会搞砸了。

回顾DIP所说的内容:

  
      
  • 高级模块不应依赖于低级模块。两者都应该   取决于抽象。

  •   
  • 抽象不应该依赖于细节。细节应取决于抽象。

  •   

请参阅以下代码示例

struct IClockService
{
    virtual unsigned timestamp() = 0;
};

struct ITimingService
{
    virtual unsigned timing(std::function<void()> f) = 0;
};

struct ClockService : public virtual IClockService // public virtual means implement
{
    virtual unsigned timestamp() { return std::time(nullptr); }
};

struct TimingService : public virtual ITimingService
                     , private virtual IClockService // private virtual means dependency
{
    virtual unsigned timing(std::function<void()> f)
    {
        auto begin = timestamp();
        f();
        return timestamp() - begin;
    }
};

class Application : public ClockService
                  , public TimingService
{
};
Application a; 
unsigned runingTime = a.timing(anyFunction);

在上面的例子中。 ClockServiceTiminigService是两个模块,他们不需要彼此了解,只需要了解界面IClockServiceITimingServiceApplication将两个模块组合在一起。

总结:

  • 依赖映射为私有虚拟继承,否则会破坏Liskov-Subtitution Principle
  • 实现映射为公共虚拟继承。
  • 模块实现取决于抽象,这是DIP所需要的。
  • 如果发生任何编译,则通过正常继承将所有模块合并在一起,这意味着接口存在未满足的依赖关系或多重实现。

我只在我的玩具项目中应用这样的习语。请自担风险。

答案 1 :(得分:0)

很多答案都是基于猜想,但由于没有人尝试过,所以这是我的尝试。

构图而不是继承

ECS的核心是一种美学上有利于构成而不是继承。传承首先被设计为模拟一个&#34; is-a&#34;通过层次结构的关系。虽然它确实有更多的用途,甚至包括超越它的政策类,其核心就是那种美学&#34;。

&#34;美学&#34;这里经常会出现人类倾向。在理想的世界中,务实的团队受益于更灵活的工具,使用它们可以获得更大的利益。不幸的是,有时现实情况是,团队只有最薄弱的环节才能解决问题。

当你开始继承像变换或位置这样的东西时,它同样为这个最薄弱的环节打开了大门,开始处理这样的实体 - 组件关系,就像它们模拟&#34; is-a&#34;关系(例如:动态向下转换为僵硬的身体,试图通过破坏其位置来破坏手榴弹资源并忘记使位置虚拟或至少受到保护和非虚拟,甚至更加模糊的情况无意中切片)。

通常情况下,我们从未接触过这样的天真,但我已经看过很多次,因为它的观点非常乐观。组合使这种情况变得不可能,或者至少减轻了与其中许多相关的影响和成本。它是一种限制性更强的工具,可以降低灵活性,施加更多限制,有时在这种意义上限制自由和灵活性,这在团队环境中是一种避免墨菲定律的严厉方式。这总是有点主观,因为它基于对人类倾向的预测,这种预测永远不会是完美的,但人类倾向于将构图搞砸而不是深层嵌套的继承等级。它往往需要更多的努力和样板,但往往不太可能严重搞砸。

运行时可扩展性

这仅适用于某些引擎,但有时引擎希望允许在运行时对其实体进行进一步编程,包括引入新组件,扩展现有实体等,而无需静态编译过程。例如,现有实体的新组件,实体或扩展可能通过脚本语言(例如嵌入式Lua)或专有节点编程语言应用,这种语言允许没有强大编程背景的设计器类型组成新实体。

在这些情况下,继承变得太过于硬编码的静态编译概念,无法在运行时扩展。这仅适用于选择引擎,但它演示了故意设置以避免继承的情况实际上可以提高另一种灵活性(特别是在运行时)。

那就是说,鉴于合适的团队,标准和要求,我认为你的提案没有什么特别的错误。但这两点可能有助于解释为什么使用继承来模拟实体组件系统有点罕见。

还有一些其他潜在问题,例如依赖于RTTI和动态转换以确定哪些组件可用,增加了保留ABI的难度,vptr开销等,如果需要我可以进入。其中很多只是回到构图与继承的核心。