引擎内部:管理游戏世界

每个游戏都需要一种方法来管理您所玩的世界。在我们的情况下,这是一个3D世界,其中包含很多东西。 您有子弹般的旋转,到处都是爆炸,军队正在步入他们的荣耀之路。 我们的世界需要美丽而快速地绘制,您现在正在阅读的文章是关于我们如何构建构成框架的系统的系统,以实现所有这些。

面向数据的设计

要了解我们的场景管理系统的体系结构,您需要对称为“ 面向数据的设计 ”的编程范例有基本的了解。 DOD背后的核心原理是,一切都被视为数据从一种形式到另一种形式的转换 。 本质上,一切都是数据问题,重点是设计代表世界状态的数据以及改变世界状态的过程。

面向数据的设计的重点是实现,而不是在代码中表示现实世界的概念。 而不是实现诸如“猫”之类的抽象,我们关注的是猫的组成:定义猫属性的数据是什么。

实际上,以猫为例,我们将猫视为是其各个部分之和的实体。 例如,这只猫可能有四只腿,一件皮大衣和一个大脑。 所有这些属性都被解释为单独的组件,但整个系统的最终结果是猫本身。

面向数据设计的另一个重要方面是,在现实世界中的实际硬件上执行程序至关重要。 没有一种程序被认为是在纯粹理论上的计算机上执行的,事实并非如此。 相反,在设计数据及其上的流程时,我们会考虑基础架构及其属性。

这样的方面之一就是内存访问速度 。 当摩尔定律掌握了计算行业,并且处理能力突飞猛进时,内存访问速度却落在了后面。 主要原因是内存占用大量空间,并且需要与CPU保持一定距离。 距离等于等待时间。 此属性使内存布局变得异常重要,因为它们决定了如何访问内存以及如何有效利用可用的处理器优化。 值得注意的是,有一些实验系统正在开发中,这些系统将内存放置在离使用位置更近的位置,从而有效地减少了内存获取延迟的问题。

为了全面研究面向数据的设计,我鼓励您看一下由Richard Fabian撰写的此网络书籍。

组件和实体

我们的场景管理系统是基于组件的,就像上面介绍的cat方法一样。 每个组成部分 它的核心是定义某些属性的数据结构 。 组件通过实体链接在一起。 实体本身主要是便利构造。 可以用单个ID代替它们,但是我们选择在其中内置一些逻辑以优化某些场景处理任务。

每个组件都有一个与之关联的系统 。 这些系统是自治的 ,因为它们管理分配给它们管理的组件的所有资源。 它们到实体管理系统的唯一链接是功能指针的结构,该函数指针定义用于创建组件实例以及渲染和更新它们的操作。

组件系统实现在组件实例上运行的许多进程 。 其中包括更新所有组件并渲染它们。 组件系统还实现了用于根据实体指针或组件ID查找单个组件的接口。

实体是通过模板创建的, 模板定义了要创建并链接到实体的组件集。 各个组件模板包含对资源(例如材料和网格)的引用。 这些引用在加载模板时被解析,这将单个组件实例的创建几乎变成了内存副本 。 这是最初的设计选择,因为该系统最初用于效果 ,需要真正快速地创建,并且使用寿命相对较短。

分而治之

实体组成系统的一部分权力来自责任分离 。 这使得扩展系统就像创建新的组件系统一样容易。 只要界面不变,更改现有组件也不会影响其他组件系统。

将执行工作划分为多个独立的单元自然也会带来并行工作的机会。 当系统之间的连接松散时,一个系统中的更改与另一系统中发生的工作冲突的可能性就较小。

组件系统始终根据其优先级进行更新。 我们不是一次更新一个实体的所有组件,而是一次更新一个系统的所有组件。 这使我们能够为每个流程定义一个静态执行顺序 ,从而更轻松地处理不同系统之间的依赖关系。

针对数据访问进行了优化

如前所述,面向数据的设计认识到所有应用程序都在真实的硬件上运行。 每个组件系统完全控制其内存分配和数据布局的能力突出了这一点。 通过组件系统接口,组件数据布局的唯一可见方面是单个不透明指针 。 实际上,它甚至不必是指针,例如,可以只是由组件系统管理的ID。

您可能已经知道,内存访问速度是由缓存效率决定的 。 这是我们能够利用处理器的分层高速缓存来限制需要往返于实际片外存储(RAM,磁盘等)的时间的概念。 缓存以突发形式更新,典型的缓存行长度约为64字节。 这意味着无论您只需要一个字节还是64个字节,都将更新64个字节。自然,这应该解释为什么将彼此靠近使用的信息如此重要的原因。

组件方法的好处是每个组件都可以为其数据定义布局。 请看下面的代码清单,以获取有关如何布局数据以实现更优化访问的示例。

如您所见,存在多个用例,每个用例都需要访问不同的数据。 例如,我们不需要访问变换的ID或实体指针来更新世界变换矩阵。 请注意,在上面的示例中,每个数组的索引定义了组件实例的标识。

让我们将优化的数据布局与上述未优化的数据布局进行比较。 假设我们想根据其ID查找单个转换。 为此,我们将不得不遍历单片式Transform结构的所有实例,从而仅读取一个字段。 但是,因为我们一直在读取完整的缓存行数据,所以我们可能正在世界矩阵的某些部分以及潜在的实体指针(取决于对齐方式)中进行读取。 反过来,这意味着仅检查ID,我们可能会被迫对主要RAM(或速度较慢的缓存)进行N次提取,以查找正确的ID。

如果我们要使用更优化的布局和ID数组,我们将遍历在内存中连续布置的32位无符号整数的向量。 由于在一个64字节宽的单个高速缓存行中可以包含16个ID,因此我们可以通过一次从主RAM(或更慢的高速缓存)中读取一次来检查16个ID。

同样,由于组件系统完全负责所有资源分配及其使用的内存布局,因此这种优化是可能的。

组件系统示例

最后,我想举例说明我们已构建的组件:

  • 模型组件定义了要渲染的网格和用来渲染它们的材质的集合。
  • 摄影机组件负责编排场景渲染。 它挂接到引擎实体的绘制调用界面,并确保所有模型(例如,使用正确的相机配置绘制)。
  • 动画组件负责播放单个动画,并处理不同动画剪辑之间的混合。 还有一个动画控制器组件,该组件负责管理更复杂的动画过渡图,这些图又命令各个动画组件。
  • 上面讨论的变换组件负责定义场景中的位置和方向。 不同的组件链接到此组件以定义其空间信息。
  • 粒子组件管理一个粒子发射器。 这使我们可以将粒子发射器添加到场景中的任何对象。 借助变换和动画组件,我们还可以使用骨骼动画驱动发射器,将它们无缝地混合到角色动画中。

以上示例全部来自引擎和渲染方面。 我们不将引擎实体系统用于游戏代码,因为它不能保证确定性。 实际上,这本身就是一个有趣的讨论,我将为其保留整个博客文章。

摘要

实体组件系统方法是一种将功能划分为易于管理的自治系统的好方法,这些自治系统还可以用作大型项目的工作单元 。 它们使我们可以优化内存使用访问模式,以便我们可以充分利用基础平台。 实体组件系统也是面向数据设计的一个很好的例子,它专注于架构在实际硬件上运行良好的软件。