这是受到对我的另一个问题的评论的启发:
在C++中为类提供可访问的"名称"时,您如何"不重复自己"?
nvoight:"RTTI很糟糕,因为它暗示你没有做好OOP.做自己的自制RTTI不会让它更好OOP,它只是意味着你在糟糕的OOP之上重新发明轮子."
那么这里的"优秀OOP"解决方案是什么?问题是这个.该程序是用C++编写的,因此下面还提到了C++特定的细节.我有一个"组件"类(实际上是一个结构),它被子类化为包含不同类型组件数据的许多不同的派生类.它是游戏"实体组件系统"设计的一部分.我想知道组件的存储.特别是,当前的存储系统具有:
"组件管理器",用于存储单个类型组件的数组,实际上是哈希映射.哈希映射允许通过其所属实体的实体ID来查找组件.此组件管理器是从基础继承的模板,模板参数是要管理的组件类型.
一个完整的存储包,它是这些组件管理器的集合,实现为指向组件管理器基类的指针数组.这有插入和提取实体的方法(插入时,组件被取出并放入管理器,删除时,它们被提取并收集到新的实体对象中),以及添加新组件管理器的方法,所以如果我们想要为游戏添加新的组件类型,我们所要做的就是为其插入一个组件管理器.
这是提示这一点的完整存储包.特别是,它无法访问特定类型的组件.所有组件都存储为基类指针,没有类型信息.我想到的是使用某种RTTI并将组件管理器存储在映射类型名称的映射中,从而允许查找,然后适当地向下转换指向相应派生类的基类指针(用户将调用模板成员)在实体存储池上执行此操作).
但是如果这个RTTI意味着糟糕的OOP,那么设计这个系统的正确方法是什么,所以不需要RTTI?
免责声明/资源:我的BCS论文是关于用于编译时实体 - 组件 - 系统模式生成的C++ 14库的设计和实现.你可以在GitHub上找到这个库.
本答案旨在概述您可以应用于实现实体 - 组件 - 系统模式的一些技术/想法,具体取决于在编译时是否知道组件/系统类型.
如果您想查看实现细节,我建议您查看我的库(上面链接),以获得完全基于编译时的方法.戴安娜是一个非常好的C库,可以让您了解基于运行时的方法.
您有多种方法,具体取决于项目的范围/规模以及实体/组件/系统的性质.
所有组件类型和系统类型在编译时都是已知的.
这是我在BCS论文中分析的案例 - 您可以做的是使用高级元编程技术(例如使用Boost.Hana) 将所有组件类型和系统类型放在编译时列表中,并创建在编译时将所有内容链接在一起的数据结构.伪代码示例:
namespace c
{
struct position { vec2f _v };
struct velocity { vec2f _v };
struct acceleration { vec2f _v };
struct render { sprite _s; };
}
constexpr auto component_types = type_list
{
component_type<c::position>,
component_type<c::velocity>,
component_type<c::acceleration>,
component_type<c::render>
};
Run Code Online (Sandbox Code Playgroud)
定义组件后,您可以定义系统并告诉他们"要使用哪些组件":
namespace s
{
struct movement
{
template <typename TData>
void process(TData& data, float ft)
{
data.for_entities([&](auto eid)
{
auto& p = data.get(eid, component_type<c::position>)._v;
auto& v = data.get(eid, component_type<c::velocity>)._v;
auto& a = data.get(eid, component_type<c::acceleration>)._v;
v += a * ft;
p += v * ft;
});
}
};
struct render
{
template <typename TData>
void process(TData& data)
{
data.for_entities([&](auto eid)
{
auto& p = data.get(eid, component_type<c::position>)._v;
auto& s = data.get(eid, component_type<c::render>)._s;
s.set_position(p);
some_context::draw(s);
});
}
};
}
constexpr auto system_types = type_list
{
system_type<s::movement,
uses
(
component_type<c::position>,
component_type<c::velocity>,
component_type<c::acceleration>
)>,
system_type<s::render,
uses
(
component_type<c::render>
)>
};
Run Code Online (Sandbox Code Playgroud)
剩下的就是使用某种上下文对象和lambda重载来访问系统并调用它们的处理方法:
ctx.visit_systems(
[ft](auto& data, s::movement& s)
{
s.process(data, ft);
},
[](auto& data, s::render& s)
{
s.process(data);
});
Run Code Online (Sandbox Code Playgroud)
您可以使用所有编译时知识为上下文对象内的组件和系统生成适当的数据结构.
这是我在论文和库中使用的方法 - 我在C++ Now 2016上讨论过它:"在C++ 14中实现多线程编译时ECS".
所有组件类型和系统类型在运行时都是已知的.
这是一种完全不同的情况 - 您需要使用某种类型擦除技术来动态处理组件和系统.一个合适的解决方案是使用诸如LUA之类的脚本语言来处理系统逻辑和/或组件结构(更高效的简单组件定义语言也可以手写,以便它一对一映射到C++类型或引擎的映射类型).
您需要某种上下文对象,您可以在运行时注册组件类型和系统类型.我建议使用唯一的递增ID或某种UUID来识别组件/系统类型.在将系统逻辑和组件结构映射到ID之后,您可以在ECS实现中传递这些内容以检索数据和流程实体.您可以将组件数据存储在通用的可调整大小的缓冲区(或大容器的关联映射)中,这些缓冲区可以在运行时通过组件结构知识进行修改 - 这是我的意思的一个示例:
auto c_position_id = ctx.register_component_type("./c_position.txt");
// ...
auto context::register_component_type(const std::string& path)
{
auto& storage = this->component_storage.create_buffer();
auto file_contents = get_contents_from_path(path);
for_parsed_lines_in(file_contents, [&](auto line)
{
if(line.type == "int")
{
storage.append_data_definition(sizeof(int));
}
else if(line.type == "float")
{
storage.append_data_definition(sizeof(float));
}
});
return next_unique_component_type_id++;
}
Run Code Online (Sandbox Code Playgroud)某些组件类型和系统类型在编译时是已知的,其他组件类型和系统类型在运行时是已知的.
使用方法(1),创建某种"桥"组件和系统类型,实现任何类型擦除技术,以便在运行时访问组件结构或系统逻辑.一个std::map<runtime_system_id, std::function<...>>
可以用于运行时系统逻辑处理.一个std::unique_ptr<runtime_component_data>
或一个std::aligned_storage_t<some_reasonable_size>
可以用于运行时组件结构.
回答你的问题:
但是如果这个RTTI意味着糟糕的OOP,那么设计这个系统的正确方法是什么,所以不需要RTTI?
您需要一种将类型映射到可在运行时使用的值的方法:RTTI是一种适当的方法.
如果您不想使用RTTI并且仍希望使用多态继承来定义组件类型,则需要实现一种从派生组件类型检索某种运行时类型ID的方法.这是一种原始的方式:
namespace impl
{
auto get_next_type_id()
{
static std::size_t next_type_id{0};
return next_type_id++;
}
template <typename T>
struct type_id_storage
{
static const std::size_t id;
};
template <typename T>
const std::size_t type_id_storage<T>::id{get_next_type_id()};
}
template <typename T>
auto get_type_id()
{
return impl::type_id_storage<T>::id;
}
Run Code Online (Sandbox Code Playgroud)
说明:get_next_type_id
是一个非static
函数(在翻译单元之间共享),它存储static
ID类型的增量计数器.要检索与特定组件类型匹配的唯一类型ID,您可以调用:
auto position_id = get_type_id<position_component>();
Run Code Online (Sandbox Code Playgroud)
在get_type_id
"公共"功能将检索的相应实例的唯一ID impl::type_id_storage
,调用get_next_type_id()
建设,这反过来又返回其当前next_type_id
计数器值,并增加它的下一个类型.
需要特别注意这种方法,以确保它在多个翻译单元上正确运行并避免竞争条件(如果您的ECS是多线程的).(更多信息在这里.)
现在,解决您的问题:
这是提示这一点的完整存储包.特别是,它无法访问特定类型的组件.
// Executes `f` on every component of type `T`.
template <typename T, typename TF>
void storage_pack::for_components(TF&& f)
{
auto& data = this->_component_map[get_type_id<T>()];
for(component_base* cb : data)
{
f(static_cast<T&>(*cb));
}
}
Run Code Online (Sandbox Code Playgroud)
您可以在旧的和废弃的SSVEntitySystem库中看到此模式正在使用中.您可以在我过时的"现代C++中基于组件的实体系统的实现" CppCon 2015演讲中看到基于RTTI的方法.
如果我理解正确的话,您需要一个集合,例如地图,其中值具有不同类型,并且您想知道每个值是什么类型(以便您可以向下转换它)。
现在,“好的 OOP”是一种你不需要沮丧的设计。您只需调用方法(对于基类和派生类来说是通用的),派生类对同一方法执行与其父类不同的操作。
如果情况并非如此,例如,您需要使用子级的一些其他数据,因此您想要向下转型,则意味着在大多数情况下,您在设计上做得不够努力。我并不是说它总是可能的,但你需要以这样的方式设计它,多态性是你唯一的工具。这是一个“好的 OOP”。
无论如何,如果你真的需要向下转型,你就不必使用RTTI。您可以在基类中使用公共字段(字符串)来标记类类型。