在使用Unity了很长时间(自v3x开始)并了解其功能和复杂性之后,我决定尝试构建自己的游戏引擎。
为什么?
好吧,如果您真的要问我这个问题,我会开玩笑地说:“好吧,为什么不呢?”但是,总的来说,我对引擎的复杂性和功能着迷了,这是我想知道的理由关于,尝试创建和理解。 现在,我不期望以与Godot,Unity和Unreal相同的质量来创建完整的引擎,因此这是我为自己准备的一些指导原则/目标:
A.面向数据的设计-在大学时,我首先通过Noel Llopis的博客文章“面向数据的设计(或者为什么您可能会用OOP自杀)”被介绍给面向数据的设计,最终使我进入了Mike Acton的CPPCon主题演讲。 从那时起,我就一直以一种主要面向功能和面向数据的方式工作,因此这对我来说很有意义,我想突破自己的知识范围。
B.保持简单—再次重申一下,这是我探索复杂的引擎架构并能够对此进行推理的沙箱。 我不会为顶级游戏疯狂,而是一些小而简单的事情,最有可能是子弹般的地狱游戏,因此我可以利用面向数据的设计。
C.低级语言-我的大部分工作是用C#和Python完成的,距我真正接触C ++或另一种低级语言已经差不多一年了,所以这对我来说是一个很好的实践。 (C ++ 17)
D.多线程— CPU上的多个内核很有用,如何有效利用它们?
E.为了理智起见,进行单元/集成测试! 我将使用Catch2,因为它是一个非常有表现力的测试框架,易于设置。
因此,事不宜迟,让我们开始在这个由多部分组成的博客文章系列中介绍Eccentric ECS的全部含义。
第1部分。什么是实体组件系统?
实体组件系统(ECS)是一种体系结构系统模式,其重点在于数据的构成和数据的操作,而不是对数据和行为进行建模以对现实世界进行建模。
类似地,面向数据的设计,当然还有实体组件系统,可以认为是一个巨大的电子表格,其中每一列代表数据的一个组成部分,每一行代表组成一个实体的一组相关数据。 我们的系统可以是对数据的列和行进行操作的函数。
第2部分:表示实体
以我的电子表格示例为基础,实体只是类似于行的数值(例如,行1、2、3、4等)的唯一ID。 在偏心ECS中,实体表示如下:
结构实体{
unsigned int id ;
实体(){ id = 0; };
实体( 未签名的int id):实体(){ 此 -> id = id; };
const bool运算子 ==( const实体其他) const {
返回ID ==其他。 id ;
}
const bool运算符 !=( const实体other) const {
返回id !=其他。 id ;
}
};
在我的情况下,它存储的只是一个无符号整数,因为我真正关心的是为实体分配唯一的ID。
通常,所有实体都是通过EntityManager管理的,该实体负责:
- 实体的创建/销毁
- 确定实体的存在
这样,我们可以这样表示一个EntityManager:
class entity_manager {
公众 :
实体create_entity();
void create_entities(size_t大小,实体entity []);
void destroy_entities(entity []实体);
布尔存在(实体e);
entity_manager();
〜entity_manager();
私人的 :
std :: vector current_entities ;
unsigned int last_entity ;
};
创建一个实体很简单,我们会跟踪last_entity并在需要创建新实体时将其递增。 这样最多可支持2³²个实体 。 现在,如果我要创建2³²+ 1个实体,则会收到一个溢出错误。 考虑到规模 ,我可能想重用某些已经释放了ID的实体,或者使用64位整数(而不是32位整数)进行查询。
在当前的标头中,我表达了元素数组的用法,而不是用于创建和销毁单个参数的用法。 最好是,我希望处理批量数据,而不是单独处理单个元素。 现在,根据这些操作的性能分析(带或不带多线程),此内容可能会更改。
一些事后思考
在当前的entity_manager中,我使用一个STL向量,目前它解决了管理实体的问题。 我一直在研究创建自己的自定义分配器,以便可以在这些块中分配一块内存和插槽实体。 这样做的好处是,我可以按组而不是单个地释放内存,并限制malloc调用的次数。 作为通用内存分配器的Malloc必须工作于任何大小(1 KB,1GB等),这可能效率很低。
稍后,我将在另一篇博客文章中重新讨论该主题!