游戏引擎架构

这篇文章是系列文章的一部分,我在其中记录我从头开始构建ECS游戏引擎的经验。 请查看 该项目 的 主页,以 获取更多帖子,信息和源代码。
在上一篇文章解释了组件管理器和系统的基本概念之后,您可能想知道所有内容如何组合在一起。 这篇博客文章是最后一篇完全概念性的文章,在此之后,我们将开始深入研究实现的细节。
在我跳到这一点之前,有一个简短的提示是,引擎仍处于开发阶段。 将来我可能会更改一些实现,但就目前而言,我正在使用的体系结构在我能想到的所有用例中都运行良好。 如果有人阅读本文有任何想法或建设性的批评,我想讨论一下!
好的。 让我们潜入。
实体经理
实体管理器处理将哪些ID分发给新实体。 当您创建新实体时,实体管理器要确保没有其他活动实体共享相同的ID(因为这会引起严重的问题)。 因此,实体管理器为单例。
示例:游戏开始时,我需要将玩家添加到游戏中。 为此,我将调用诸如EntityManager.create()之类的东西,它将为我返回一个新的实体(如果您还记得上一篇文章,则一个实体只是一个ID)。
//返回{2}-玩家的实体ID为2
实体播放器= entityManager.create();
组件管理器
注意:在 上一篇博客文章 中,我对组件管理器进行了深入 的介绍 。 如果其中任何一个令人困惑,请回头阅读。
组件是与实体相关联的数据块。 与其让实体拥有自己的组件,不如由实体的组件管理器处理同一类型的所有组件。 与组件数据本身的所有交互都通过组件管理器进行。
示例:我的玩家以最多10个中的7个HP开始游戏。因此,在游戏开始时,我需要告诉“健康”组件管理器我需要添加一个与玩家实体关联的新组件。这些值。
//创建实体
实体播放器= entityManager-> create();
//创建组件
HealthComponent hp;
hp.currentHp = 7;
hp.maxHp = 10;
//将组件添加到实体
healthComponentManager-> addComponent(player,hp);
系统
注意:我在 上一篇博客文章 中对系统进行了较深入 的介绍 。 如果其中任何一个令人困惑,请回头阅读。
系统是游戏功能的独立单元。 通常,系统会在每次游戏更新时运行其逻辑。 每个系统都指定它要注意的组件,并以此为基础交付实体。
示例:我有一个系统需要每20秒将具有“健康”组件的所有实体恢复为1。 在这种情况下,系统将指定它要关注“健康”组件。 每当实体获得运行状况组件时,它将被添加到系统需要更新的实体列表中。 每当实体丢失健康组件时,系统都会停止更新它。
无效HealingSystem :: update(int dt){
_timeSinceLastHeal + = dt;
if(_timeSinceLastHeal> 20000){
// 20秒过去了
_timeSinceLastHeal = 0;
//更新所有我们关注的实体
对于(汽车和实体:_entities){
healthComponentManager-> heal(entity,1);
}
}
}
您可能会认为其中的某些代码看起来很难编写,并且您永远都不想使用这种语法来开发游戏。 好吧,你是对的! 这就是为什么我添加了句柄。 查看此博客文章结尾附近的“问答”问题之一,以查看引擎中的游戏代码实际上是什么样的。
世界
紧随其后的那些人肯定会意识到这里缺少一块。 需要回答的主要问题是系统如何发现已添加或删除组件? 我们已经提到过这里有太多相互联系的部分。 为了证明对“世界”的需求,让我们以从游戏中移除实体的系统为例。
对于此示例,我们假设游戏中有一颗炸弹会爆炸并杀死爆炸半径内的所有物体,从而从游戏中移除所有实体。 没有世界的情况下,控制流程如下所示:

您会注意到,实体经理在这里做了很多繁重的工作。 它不仅管理实体的生存期和创建(如上所述),而且实质上还运行着表演。 为了执行上面概述的任务,它需要引用游戏中存在的所有组件管理器和所有系统。
因为它已经在管理所有系统和组件的列表,所以对于实体管理器来说,在运行时也处理所有系统都是有意义的,例如,每个游戏周期都调用update()。 这意味着实体管理器还将处理我们希望存在于引擎中的任何并行化。 突然,实体管理器的范围已经远远超出了我们最初希望的范围。 因此,我们转向“世界”。 世界是我们上面已经介绍过的部分之间的粘合剂。 让我们看一下相同的用例,删除一个实体,但是有一个世界。

您会注意到,基本上所有内容都是相同的,但不是由实体经理来运行节目,而是运行节目的“世界”。 整个世界都引用了引擎的上述所有部分:所有系统,所有组件管理器和实体管理器。 对其中任何一个的访问都遍历整个世界,但是这个世界本身实际上并没有做任何工作,只是分发引用并进行调用。
这是世界上其他两个用例:


因此,世界将跟踪所有系统,并且系统会指向它们所处的世界。系统对引擎其余部分的所有调用都遍历整个世界。
“这个过程使游戏逻辑代码很难编写”
我上面编写代码示例的方式是为了清楚起见。 在以后的文章中,我将解释如何使用句柄抽象出许多实现细节,以便编写游戏逻辑非常直观,并且看起来像这样:
//创建播放器
实体播放器= _world-> createEntity();
//为玩家提供生命值,从7/10 HP开始
HealthHandle hp = player.addComponent(HealthComponent(7,10));
//治愈玩家
hp.healFor(3);
“我可以有多个世界吗?”
是的,在某些情况下,您可能需要两个世界,例如一个用于处理库存的世界(当您打开库存屏幕时,“库存世界”变成显示的一个世界)。 这样可以在游戏各部分之间实现更多的并发性,而这不会显着影响彼此。 在这一点上,我只玩过一个世界,所以我还没有完全充实多个世界的概念。
“系统之间如何通信?”
假设我们的游戏中有一颗炸弹会爆炸并造成伤害。 我们可能已经有一个专门用于碰撞检测的系统,这使在“炸弹系统”中进行第二次碰撞检查变得愚蠢。 相反,我们使用事件队列来管理游戏不同部分之间的通信。 关于此事的更多信息,现在我们可以想象一个看起来像这样的系统调用:
//“碰撞系统”的内部update()
实体e1;
实体e2;
if(collided(e1,e2)){
eventQueue-> fireCollisionEvent(e1,e2);
}
目前的进展

- 更好的碰撞检测(涵盖更多的边缘情况)
- 基于层的冲突(与Unity的方式相同)
- 使用lambdas的自定义脚本组件(稍后会详细介绍)
- Made AoS / SoA切换以存储组件(稍后再介绍🙂)
- 各种错误修复