大家好!
从实际的会议来看,第四部分
“ C ++开发人员”从这里开始,这是我国最活跃的课程之一,在这里,不仅有“十字军
战士 ”与
Dima Shebordaev交谈 :)总体而言,该课程已经发展到
迪马(Dima)是我们国家最大的活动之一,它举办公开课程,并且在课程开始之前我们会选择一些有趣的材料。
走吧
参赛作品
实体组件系统(ECS,“ entity-component-system”)现在作为一种体系结构替代方案正处于流行高峰,该体系结构强调了组合原则而非继承原则。 在本文中,我将不讨论该概念的细节,因为有关此主题的资源已经足够了。 实施ECS的方法有很多,但是,我通常会选择相当复杂的方法,这会使初学者感到困惑并花费大量时间。
在本文中,我将介绍一种非常简单的方法来实现ECS,其功能版本几乎不需要代码,但完全遵循该概念。

精英
说到ECS,人们通常会表达不同的意思。 当我谈论ECS时,是指一个系统,它允许您定义具有零个或多个纯数据组件的实体。 这些组件由纯逻辑系统选择性地处理。 例如,组件的位置,速度,命中框和运行状况与实体E相关。 他们只是简单地在自己中存储数据。 例如,运行状况组件可以存储两个整数:一个用于当前运行状况,一个用于最大值。 系统可以是运行状况再生系统,它可以找到运行状况组件的所有实例,并每120帧将其增加1。
典型的C ++实现
有许多库提供ECS实施。 通常,它们包括列表中的一项或多项:
GravitySystem : public ecs::System
类的基本Component / System的GravitySystem : public ecs::System
;- 积极使用模板;
- 这两者,以及一些CRTP的外观;
EntityManager
类,以隐式方式控制实体的创建/存储。
Google的一些快速示例:
所有这些方法都有生命权,但是它们也有一些缺点。 他们以不透明的方式处理数据的方式意味着将很难理解内部发生了什么以及性能是否降低。 这也意味着您必须研究整个抽象层,并确保它与现有代码完全匹配。 不要忘记隐藏的bug,这些bug可能隐藏了很多要调试的代码。
基于模板的方法会极大地影响编译时间以及您必须重建构建的频率。 虽然基于继承的概念可能会降低性能。
我认为这些方法过多的主要原因是它们解决的问题太简单了。 最后,这些仅仅是与实体相关联的其他数据组件及其选择性处理。 下面,我将展示一种非常简单的方法来实现它。
我的简单方法
精华液在某些方法中,定义了Entity类,在另一些方法中,它们与作为ID /句柄的实体一起使用。 在组件方法中,实体不过是与之关联的组件,为此不需要一个类。 实体将根据其相关组件明确存在。 为此,请定义:
using EntityID = int64_t;
实体组件组件是与现有实体关联的不同类型的数据。 我们可以说,对于每个实体e,e将具有零个并且具有更多可访问类型的组件。 从本质上讲,这是一个爆炸的键值关系,幸运的是,有一些卡形式的标准库工具可用于此目的。
因此,我将组件定义如下:
struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; };
正如ECS所期望的,这足以通过组件指示实体。 例如,要创建具有位置和状态但无速度的实体,您需要:
要销毁具有给定ID的实体,我们只需从每张卡中
.erase()
。
系统篇我们需要的最后一个组件是系统。 这是与组件配合使用以实现特定行为的逻辑。 由于我喜欢简化事情,因此我使用常规功能。 上面提到的健康再生系统可能只是下一个功能。
void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } }
我们可以在主循环中的适当位置放置对该函数的调用,并将其转移到运行状况组件的存储中。 由于运行状况存储库仅包含具有运行状况的实体的记录,因此可以独立处理它们。 这也意味着该功能仅获取必要的数据,而不涉及无关的数据。
但是,如果系统可以使用多个组件,该怎么办? 假设一个物理系统根据速度改变位置。 为此,我们需要将所有涉及的组件类型的所有键相交并遍历它们的值。 在这一点上,标准库已不再足够,但是编写帮助程序并不那么困难。 例如:
void updatePhysics(Positions& positions, const Velocities& velocities) {
或者,您可以编写一个更紧凑的助手,该助手允许通过迭代而不是搜索来进行更有效的访问。
void updatePhysics(Positions& positions, const Velocities& velocities) {
因此,我们熟悉了常规ECS的基本功能。
好处
这种方法非常有效,因为它是从头开始构建的,没有限制抽象。 您不必集成外部库或使代码库适应实体/组件/系统应该是什么的预定义思想。
由于此方法是完全透明的,因此您可以在此基础上创建任何实用程序和帮助程序。 随着您项目的需求,这种实现也随之增长。 对于简单的原型或用于游戏Jam'ov的游戏,您很可能具有上述足够的功能。
因此,如果您不熟悉整个ECS领域,那么这种简单的方法将有助于您理解主要思想。
局限性
但是,与任何其他方法一样,存在一些限制。 以我的经验,正是在任何不平凡的游戏中使用
unordered_map
这样的实现都会导致性能问题。
迭代具有多个实体的
unordered_map
多个实例上的关键交集无法很好地扩展,因为您实际上是在进行
N*M
查找,其中N是重叠组件的数量,M是匹配实体的数量,并且
unordered_map
不太擅长缓存。 可以通过使用更适合于迭代的键值存储代替
unordered_map
来解决此问题。
另一个限制是电镀。 根据您的操作,识别新组件可能变得很乏味。 您可能不仅需要在Components结构中,还需要在spawn函数,序列化,调试实用程序等中添加公告。 我自己遇到了这个问题,并通过生成代码解决了问题-我在外部json文件中定义了组件,然后在构建阶段生成了C ++组件和帮助器函数。 我确信您可以找到其他基于模板的方法来解决遇到的任何样板问题。
结束
如果您有任何问题和意见,可以将其留在此处或与
Dima一起上
公开课 ,听他的话,并已经问了一下。