常见问题解答
目录
简介
这是一个不断更新的章节,我试图在此处汇总最常被问到的问题的答案。
如果您在这里找不到答案,有两种情况:还没有人问过,或者本章节需要更新。在这两种情况下,您都可以 提交新 issue,或者进入 Gitter 频道 或 Discord 服务器 寻求帮助。
可能已经有人为您准备了答案,随后我们可以将这部分内容整合到文档中。
FAQ
为什么我在 Windows 上的 debug 构建如此缓慢?
EnTT 是一个实验性项目,我也用它来跟进语言和标准库的最新修订版。因此,您正在使用的某些类很可能在底层使用了标准容器。
不幸的是,众所周知,标准容器在 debug 模式下的性能并不好(原因超出了本文档的范畴),而且在 Windows 上似乎更是如此。幸运的是,这也可以在很大程度上得到缓解,在许多情况下都能取得良好的效果。
首先,在 Windows 项目中需要做两件事:
- 禁用
/JMC选项(Just My Code 调试),该选项从 Visual Studio 2017 15.8 版开始提供。 - 将
_ITERATOR_DEBUG_LEVEL宏设置为 0。这将禁用检查迭代器 (checked iterators) 和迭代器调试 (iterator debugging)。
此外,设置 ENTT_DISABLE_ASSERT 变量或重定义 ENTT_ASSERT 宏以禁用 EnTT 内部的 debug 检查:
#define ENTT_ASSERT(...) ((void)0)
引入这些 assert 是为了帮助用户,但它们需要访问底层容器,因此在某些情况下可能会破坏性能。
进行这些更改后,在大多数情况下 debug 性能应会有足够的提升。如果您想要更好的性能,还可以切换到优化级别 O0 或最好是 O1。
如何用 component 表示层级结构?
这是任何人在开始使用 entity-component-system 架构模式时最先提出的问题之一。
解决该问题有几种方法,最佳方法主要取决于面临的实际问题。在所有情况下,如何实现并不严格依赖于所使用的库,但后者肯定允许或不允许使用不同的技术,具体取决于数据的布局方式。
我试图描述一些适合 EnTT 模型的方法。这篇文章是试图 探索 该问题的系列文章的第一篇。未来可能还会有更多文章。
此外,EnTT 还提供了创建稳定 storage 类型的可能性,从而为一个、所有或某些 component 提供指针稳定性 (pointer stability)。在创建层级结构等场景时,这是迄今为止最方便的解决方案。有关更多详细信息,请参阅库的 ECS 部分文档,特别是关于 component_traits 类的内容。
自定义 entity 标识符:赞成还是反对?
至少在两种情况下,自定义 entity 标识符绝对是个好主意:
- 如果
std::uint32_t对您的目的来说不够大,因为这是entt::entity的底层类型。 - 如果您想避免在使用多个 registry 时发生冲突。
标识符可以通过 enum class 和定义了 std::uint32_t 或 std::uint64_t 类型的 entity_type 成员的 class type 来定义。
事实上,这是一个等同于 entt::entity 的定义:
enum class entity: std::uint32_t {};
可定义的标识符数量没有限制。
警告 C4003:min、max 与宏
在 Windows 上,某个头文件定义了两个宏 min 和 max,这可能会导致它们与标准库中的对应物发生冲突,从而在编译期间引发错误。
这是一个相当大的问题。但幸运的是,这不是 EnTT 的问题,并且有一个相当简单的解决方案。
它包含在包含任何其他头文件之前定义 NOMINMAX 宏,以消除多余的定义:
#define NOMINMAX
有关更多详细信息,请参阅 此 issue。
标准库与不可拷贝类型
EnTT 内部使用 trait std::is_copy_constructible_v 来检查 component 是否真正可拷贝。然而,该 trait 并没有真正检查类型是否实际可拷贝。相反,它只检查是否存在合适的拷贝构造函数和拷贝运算符。
由于标准的一些特性,这可能会导致令人惊讶的结果。
例如,std::vector 定义了一个条件启用的拷贝构造函数,具体取决于值类型是否可拷贝。因此,std::is_copy_constructible_v 对以下特化 (specialization) 返回 true:
struct type {
std::vector<std::unique_ptr<action>> vec;
};
然而,在特化时,拷贝构造函数实际上被禁用了。因此,尝试将此类型的实例分配给 entity 可能会触发编译错误。
作为一种解决方法,用户可以显式将该类型标记为不可拷贝 (non-copyable)。这也会抑制移动构造函数和运算符的隐式生成,因此必须相应地将它们默认化 (defaulted):
struct type {
type(const type &) = delete;
type(type &&) = default;
type & operator=(const type &) = delete;
type & operator=(type &&) = default;
std::vector<std::unique_ptr<action>> vec;
};
请注意,因此聚合初始化 (aggregate initialization) 也会被禁用。
幸运的是,这种类型的技巧非常罕见。坏消息是,由于语言的设计,无法在库级别处理它。另一方面,语言本身也提供了一种缓解该问题的方法,使其变得可控。
哪些函数触发哪些 signal
Storage 类提供三个 signal,在特定操作后发出。不过,也许并非所有人都清楚这些操作是什么。
如果不清楚,您可以在下方找到一份用于此目的的 备忘录 (vademecum):
on_created(注:API 实际为on_construct)在 component 首次被添加(既未修改也未替换)到 entity 时调用。on_update在现有 component 被修改或替换时调用。on_destroyed(注:API 实际为on_destroy)在 component 从 entity 中显式或隐式移除时调用。
最具争议的函数包括 emplace_or_replace 和 destroy。然而,遵循上述规则,很容易知道会发生什么。
在第一种情况下,如果 entity 没有该 component,则调用 on_created,否则替换后者并因此触发 on_update。至于第二种情况,component 从其 entity 中移除,因此在回收时被释放。这意味着对于被销毁的 entity 拥有的每个 component,都会触发 on_destroyed。
同一 component 的重复 storage
这种情况很少见,但有时您可能会看到“重影”,尤其是在涉及 storage 时。这可能是由于分配给各种 component 类型的哈希发生冲突(独一无二),或者由于您的编译器存在 bug(显然更常见)。
无论原因如何,EnTT 提供了一个定制点 (customization point),在这种情况下也可作为解决方案:
template<>
struct entt::type_hash<Type> final {
[[nodiscard]] static consteval id_type value() noexcept {
return hashed_string::value("Type");
}
[[nodiscard]] consteval operator id_type() const noexcept {
return value();
}
};
直接特化 type_hash 会绕过 EnTT 提供的默认实现,从而避免任何可能的冲突或编译器 bug。