【问题标题】:How to avoid downcasting in entity component system c++如何避免实体组件系统c ++中的向下转换
【发布时间】:2017-12-21 17:41:58
【问题描述】:

我最近遇到了游戏引擎中经常使用的实体组件系统。我决定自己用 C++ 实现它,但很快遇到了一个熟悉的问题。一切都开始天真无邪。我从以下课程开始:

class Entity
{
    private:
        int entityId;
        std::map<std::string, Component*> components;

    public:
        Entity();
        ~Entity();

        void AddComponent(Component *component);
        void RemoveComponent(std::string name);
        bool HasComponent(std::string name);
        Component* GetComponent(std::string name);
};


class Component
{
    public:
        int componentId;

};

然后我决定创建一些特定的组件类型:

class Input : public Component
{
    public:
        void process();
}

class Physics : public Component
{
    public:
        void update();
}

我开始对此进行测试。我创建了一个实体:

Entity *entity = new Entity();
entity->AddComponent(new Input());
entity->AddComponent(new Physics());

这就是事情开始出错的地方。然后我想到了如何从实体中获取组件。如果我想做类似的事情怎么办:

Physics *physics = entity->GetComponent("Physics");
physics->update();

但是 GetComponent("Physics") 返回基类 Component,而不是派生类 Physics!我在互联网上进行了一些搜索,但在 c++ 中找不到显示如何解决此问题的示例。在查看了 Unity 的功能后,我发现它们似乎只是进行了向下转换。例如,在 Unity 中,代码 (C#) 将是:

Physics physics = entity.GetComponent("Physics") as Physics;

这不是很糟糕吗?在设计实体组件系统时,如何在 C++ 中解决这个问题?或者如何执行向下转换?

【问题讨论】:

  • 也许processupdate 使用相同的函数名,并使其成为Component 类中的纯虚函数以被子类覆盖?您确实了解virtual 函数吗?它是 C++ 继承和多态性的重要组成部分。
  • 是的,但这是一个简单的例子。如果我有 100 种不同的组件类型,每一种都有几十种不同的方法会怎样。然后组件必须有 100 个虚拟方法。
  • 如果将GetComponent 声明为模板方法T GetComponent&lt;T&gt;(const std::string&amp; name),则目标可能是可以实现的。有点无聊,但允许隐藏方法中的所有血腥细节
  • @James 我可能会选择一种设计来查询和访问类似于 COM 接口的某些接口类型(纯抽象类)。
  • 或许你应该研究一下访问者模式

标签: c++


【解决方案1】:

组成实体的组件以它们的类型为关键字。如果 ECS 让你写 entity.get("Foo") as Foo,那这已经是设计上的弱点了;它应该让你写entity.get&lt;Foo&gt;()

在 C++ 中,您可以这样编写代码:

class Entity {
  std::unordered_map<std::type_index, std::unique_ptr<Component>> components;

public:
  template <typename C>
  void AddComponent(std::unique_ptr<C> p) {
    // can't remember if unique_ptr allows this conversion
    components[typeid(C)] = std::move(p);
  }

  template <typename C>
  C& GetComponent() {
    // You *know* this cast has to work. You should handle null here though.
    return *static_cast<C*>(components[typeid(C)].get());
  }
};

但请注意,实体持有其组件完全是相当不寻常的。通常你有一个世界,它包含所有的组件。一个实体只是键入世界的组件集合。

当您的世界包含所有组件时,您可以为每个组件类型提供一个集合。 (确实,这是一种受欢迎的表示形式,因为它通常会为您提供更好的内存访问模式。)然后您就不再需要为组件创建抽象基类了。

【讨论】:

  • +1 我还没有机会尝试typeid(我的 ECS 必须在运行时围绕信息,有时甚至是脚本)。您是否知道typeid 是否可以跨模块边界和不同的编译器可移植地使用,例如如果一个人使用由 GCC 制作的 dylib 将获得与另一个针对(例如,ICC)构建的 dylib 相同的typeid(Foo) ID?过去,我总是不得不推出自己的 typeid 类解决方案,但在某些情况下,如果它以这种方式可移植,则不必这样做。
  • @DrunkCoder 由于 C++ 标准没有说明模块边界,我会说这是有风险的。 Common C++ ABI 确实指定了 typeid 的结果,但我知道过去存在 Clang/G++ 互操作性问题。
  • 哎呀,我希望他们可以用 C++11 纠正它。我总是为typeid-like 解决方案找到一些不错的用例,但在过去,由于必须支持针对不同编译器构建的模块,我不得不推出自己的用例,而且使用起来不如@987654330 好用@ 本身。
  • @SebastianRedl 谢谢你,这正是我想要的。在我的例子中,我实际上并没有将组件添加到我的实体中,而是决定将它们添加到 Entity 类中的静态映射中。通过这种方式,我可以在我的渲染系统等中访问它们,例如 for(it = Entity::components.begin(); it != Entity::components.end(); it++) 等。这对你来说是否合理在你的最后两段中谈到了?
【解决方案2】:

ECS 围绕强制类型转换和鸭子类型展开。但是,如果需要,您可以使用dynamic_cast 来确保安全(可能仅用于调试构建),并将其对客户端隐藏,以便获得这种类型的语法:

Physics* physics = entity->get<Physics>();

而且你比我更安全!我的是用 C 实现的,并通过一个 C API 提供,以便通过插件和脚本使用,所以我实际上必须从 void 指针进行转换(但只是在一个中心位置)。

这在实践中往往没什么大不了的,因为客户端代码倾向于确保正在发生的事情的完整性,因为它们表达了他们想要检索的组件类型。在 C++ 中,您甚至可以在编译时进行静态检查,以确保它们检索的是实际的组件类型而不是其他类型。我不建议让他们像你一样指定字符串,因为这样一个错字不会导致任何类型的编译时错误。

在 DirectX 中使用的 COM 也围绕这些转换来检索接口。当它通过 COM 中的接口查询或 ECS 中的组件查询来统一时,它在实践中往往不是什么大问题(人们不会因为代码库而绊倒它,因为代码库偶尔会通过深度继承层次结构来降低事物)。

很多围绕 OOP 的传统实践不一定适用于 ECS,因为它是一种完全不同的架构处理方式。例如,已建立的 OOP 实践倾向于强烈鼓励信息隐藏,不鼓励向下转型,而 SOLID 建议依赖关系流向抽象。

嗯,在 ECS 中,我们有向下转换来检索组件(但它是统一的),我们的组件只是以原始形式公开它们的数据(但没什么大不了的,因为只有少数系统访问这些数据,所以数据是狭窄的),并且依赖关系流向数据,而不是抽象(但由于其巨大的灵活性,设计至少在游戏或类似游戏的领域更容易保持稳定)。

或者一个人如何执行向下转换?

一种方法是将用于获取组件的键存储为组件类型的一部分,例如:

struct Physics
{
    enum {id = ...};
};

然后你可以这样做:

template <class Component>
Component* Entity::get()
{
     const int id = Component::id;
     // Find for the component from the id.
     return component_ptr;
}

... 或者正如Sebastian Redl 指出的那样,您可以只使用typeid (由于那个C API,我忘记了如何静态地执行这一切,因为我有脚本和插件动态添加新的组件类型) .

【讨论】:

    猜你喜欢
    • 2014-08-12
    • 2019-04-15
    • 1970-01-01
    • 2018-01-05
    • 1970-01-01
    • 2020-01-10
    • 2014-07-09
    • 2017-10-23
    相关资源
    最近更新 更多