准备把袜子吹走
这篇文章是系列文章的一部分,我在其中记录我从头开始构建ECS游戏引擎的经验。 请查看 该项目 的 主页,以 获取更多帖子,信息和源代码。
我将与您分享的几乎所有东西都在此博客文章中教给了我。 如果您已经阅读过这篇文章,那么我的很多文章都是多余的。 为了保持代码的连续性,我正在写这篇文章,我不想错过我的教程系列的一部分。 我还将尝试包含更多漂亮的图片和gif,以使其更容易理解!
如果您对使用SoA(阵列结构)存储的理由感到好奇,请查看我以前的文章,在其中详细说明:
Nomad游戏引擎:第4.3部分-AoS与SoA
优化数据存储 media.com
就像AoS(数组结构)的实现一样,我们的界面将如下所示:

这里的挑战不是使此结构仅适用于Transform,而是适用于通用组件。 我们可以通过将指针指向数组而不是数组本身存储在ComponentData结构中来简化操作,如下所示:
结构ComponentData {
无符号整数大小= 1;
std :: array * buffer [3]; // x,y,z
}
这是事情开始变得有点恐怖的地方。 上面的代码假设两件事:
- 我们的组件仅由整数组成
- 我们的组件有3个成员
为了解决第一个问题,不幸的是,我们将不得不将友好的std :: arrays更改为可怕的void指针。 这意味着我们将自己管理所有的内存,只要我们小心一点就可以了。 为了解决第二个问题,我们需要以某种方式访问给定组件中的成员数量。 看,我们的通用ComponentData结构!
结构ComponentData {
无符号整数大小= 1;
void * buffer [MEMBER_COUNT];
}
现在我们到了某个地方!
建设者
让我们首先考虑组件管理器的构造函数-我们需要为成员分配缓冲区。 由于我们正在有效地管理自己的内存,因此我们只能使用malloc()。 因此,我们的构造函数将如下所示:
不用担心L13上的循环,我将在下面解释。

几个小笔记做:
- 您可能会想到,对于
packedComponentSize
我们只可以执行sizeof(ComponentType)
,但这将忽略结构填充,并导致我们分配的内存超过了实现所需的实际内存。 - 1024是给定类型的最大组件数。 显然,这将作为配置添加到实际实现的某处。
现在,这是我们面临的挑战的实质。 此时,我们有两个问号:
- 我们已经提到了
MEMBER_COUNT
个,我们实际上如何检索该值? - 我们还需要
TYPE_OF_ITH_MEMBER
,我们如何知道该类型是什么?
现代解决方案:C ++结构化绑定和magic_get
如果您使用的是支持结构化绑定的编译器,则可以使用magic_get库使用pfr::tuple_size
和pfr::get
完成所有这三个任务。 这是一种针对类型特征的高级解决方案,我将在下面进行解释,但是仅适用于现代编译器(MSVC尚不支持它)。
其他解决方案:类型特征
如果您没有奢侈的C ++ 17功能,我们总是可以退回涉及类型特征的解决方案。

我喜欢考虑类型特征的方式是它们是可以“固定”到类型的值。 这就像在结构或类上添加一个额外的静态字段,但这是一个存储在“编译区域”中的值-无论在何处引用它,都将在编译时将其替换为该值。 如果您想了解更多关于它们的知识,这是我发现的最好的文章,可以解释其基本知识。
我们可以使用类型特征为我们提供所需的信息,但这将导致我们在组件定义中增加一些开销。 我们将建立一些方法来从组件中获取类型特征:

让我们看一下代码中的内容:

用于销毁组件的代码如下所示:
再一次,因为我们不能在编译时使用i
,所以请使用我们的类循环:
结论
在这篇文章中,我们设法为数组的Struct组件管理器创建了一个接口,该接口在我之前的一篇帖子中主要反映了我们的数组的Structs实现。 到目前为止,这种实现可能是我迄今为止最引以为豪的,因为我目前的实现可以在编译时使用类型特征设置SoA / AoS存储,如下所示:
//使用SoA存储进行转换
template struct GetStorageType {typedef SoA类型; };
//使用AoS存储进行转换(这也是默认设置)
template struct GetStorageType {typedef AoS Type; };
这以很少的开销为开发人员提供了极大的灵活性-更改存储实际上就像将SoA更改为AoS一样简单,其余代码保持不变。 使SoA和AoS系统提供相同界面的细节将在以后的文章中介绍。 当前的一个缺点是,如果没有C ++ 17,则最终我们的组件需要为每个组件的每个成员的GetType
和GetPointerToMember
类型特征。 可以使用宏大量自动执行此操作,但是我将在以后的文章中介绍。 在我当前的ECS版本中,定义组件看起来像这样:
struct Transform {
int x;
诠释
int z;
}
ANNOTATE_COMPONENT(变换,x,y,z)
确实不错,尤其是因为使用C ++ 17时,甚至不需要宏。
目前的进展
在研究了引擎代码的布局方式之后,我看了一个开源ECS EntityX,并意识到我的游戏应该真正分为三部分:
- ECS,包括我到目前为止在我的帖子中提到的所有内容(系统,世界,组件,实体)
- 引擎,处理渲染,动画,碰撞等。
- 游戏,处理游戏逻辑并具有特定于游戏的自定义组件和系统。
进行这种分离有助于弄清我要分别完成的工作,并使我可以分别处理每个工作而不必担心其他部分。 就我而言,ECS部分实际上已经接近完成。 我对系统的工作方式感到满意,并且(在进行更严格的分析之前)我认为它非常出色。 另一方面,引擎和游戏都有很多工作要做。
我的前进计划是提出一个非常基本的“可玩”等级-可能是一个屏幕,其中涉及按下按钮并杀死怪物。 我认为创建此内容将帮助我弄清楚游戏中哪些部分需要最多的工作,因此我可以继续进行优先排序。
进一步的阅读/参考
实现半自动数组结构数据容器
在游戏等对性能敏感的应用程序中,以缓存友好的方式访问数据至关重要。 特别是… blog.molecular-matters.com