龙空技术网

基于ECS的游戏引擎架构设计「译」

Real游戏引擎开发者 201

前言:

现时看官们对“游戏引擎编程架构”大体比较注意,大家都想要剖析一些“游戏引擎编程架构”的相关内容。那么小编也在网上收集了一些对于“游戏引擎编程架构””的相关内容,希望咱们能喜欢,咱们快快来学习一下吧!

摘要

游戏编程设计和组织是困难而复杂的,为了简化开发进程,会使用被称为游戏引擎的游戏框架(framework),该框架包含了一组实用工具。本项的目的是探索游戏引擎设计和开发一个模块化和可扩展的游戏引擎。本文中的设计是面向对象(OOP)的以及两个实体组件系统(ECS)。OOP设计常用于计算机科学,使用对象层次来共享函数功能,ECS设计基于继承之上的组合的概念,ECS中对象包含特性而不是继承它们(特性)。然而,设计都有它们的弱点,例如OOP设计中的可扩展性问题,该问题来自于层次结构的紧耦合性,在层次结构的根附近进行的更改需要进行有效的代码重构。ECS解决了耦合问题,然而,问题存在于跨系统通信和共享组件。使用了两个ECS设计来解决这个问题,分别是Cupcake 和 Artemis。通过一个简单的游戏应用来测试和分析设计的功能性。结合使用Cupcake 和 Artemis的优点,本该提出了一个能最小化架构问题的新ECS设计。

1 介绍

游戏开发是一个复杂的进程,它可能需要图形、声音、物理、网络、AI以及输入,一个常见的实践是使用已经实现一种技术的现有的库来节省时间和精力。即便如此,将几种技术结合到一个系统中可能也是一项艰巨的任务。游戏引擎通过提供制作游戏所需的框架和技术解决了这个问题。这使得游戏开发者可以跳过技术的实现来专注于游戏开发。

开发游戏引擎时遇到的最大问题是如何表示游戏对象,游戏对象可以从没有控制或交互的简单2D图像到具有控制、声音、动画和AI的异常复杂3D对象。在概念上,l很容易将游戏对象理解为表示游戏中具有多个功能的实体。

当试图组织一个能够处理特性组合的体系结构时,问题就出现了。

一种常见的方法是使用面向对象的编程(OOP)体系结构,然而,由于继承和层次结构的本质,在表示游戏对象时出现了困难。作为解决面向对象的游戏对象问题的答案,实体组件系统(ECS)变得非常流行。

本文将讨论OOP的缺点、ECS的优点、ECS的变体,并提出一种更加模块化的ECS设计。

2 面向对象编程(OOP)

面向对象编程是一种可重用和模块化的程序设计,它将信息表示为包含数据和逻辑的对象。面向对象的一个独特而实用的特点是继承,这允许架构创建共享特性和结构的层次结构,这有助于促进代码重用和组织。

游戏对象的层次结构会是什么样子?OOP听起来正是游戏引擎需要的代码重用和组织。图1是一个表示车辆和花盆的层次结构的示例。当路径如前面所示一样简单时,这就可以实现,然而,当游戏对象开始共享功能时,问题就出现了。

2.1 OOP的问题

当涉及到层次结构中的共享特性时,设计架构就变得非常困难。图1对于带有声音的静态对象或者带有声音和控件(比如播放器外观)的非渲染对象会发生什么变化?图2显示了添加了游戏对象的潜在层次结构树,我们的树开始在特征和路径上显示冗余。添加到游戏对象的功能组合越多,层次结构就会变得越复杂,这使得代码难以管理和组织。

图2-冗余的OOP层次结构

这种复杂层次结构的一个主要问题是处理游戏对象,很难创建一个函数来接受持有特定功能的各种类型的游戏对象。诸如函数重载之类的解决方案很难维护,因为包含特定功能的每种类型的对象都需要自己的函数。这将强制所有不同组合使用非直观的层次结构或代码重用。

这个复杂层次结构的更糟糕的问题是可扩展性,每个子类都在很大程度上依赖于父类的结构。如果没有大规模的代码重构,添加新特性或更改当前特性可能是困难的,也可能是不可能的。

3 实体组件系统

继承之上的组合是解决面向对象的层次结构问题的一种方法。这个概念通过组合而不是继承来增加功能,使系统更加模块化和独立。例如,使用ECS,车辆对象包含用于声音或控制等特性的对象,而不是创建层次结构链。这使得车辆对象更具动态性,因为更改特性就像添加或删除对象一个简单,而不需要主要代码的重构。

3.1 架构

ECS由三个基本部分组成:实体、组件和系统。实体是与对象关联的唯一id,组件是表示实体信息的数据,系统在实体上运行逻辑,以利用或更改来自组件的数据。

在游戏开发中,游戏对象被当作实体,功能被表示为组件和系统。这可以看作是一个数据库,其中实体是惟一的id,系统是数据库表,组件是数据库表中的数据。

图3-ECS中的游戏对象

图3显示了游戏对象在ECS体系结构中的样子。车辆与一个惟一的id关联。使用这个惟一的id,调用者通过创建与所需特性关联的组件来附加特性。这允许系统使用组件中提供的数据处理实体和应用动作。

3.2 优点

ECS体系结构的优点在于它是独立的,每个组件只不过是简单的数据点,因此本质上是与其他组件隔离的。系统需要特定的组件来运行其逻辑,这使得系统相对解耦,因为实体中包含的不同组件的数量不会改变系统解释实体的方式。只要满足组件需求,系统就会在实体上运行其逻辑。

图4-实体组件系统(ECS)

图4显示了以前层次结构转换为ECS的设计。关于渲染系统,花盆和扬声器有什么不同?渲染系统是否关心扬声器是否有声音组件?向屏幕渲染对象是否需要声音组件?这是ECS系统如何提高隔离性和强化代码组织的一个例子。一个简单的表格可以用来查看游戏对象包含哪些功能,而不是遵循一个复杂且长树。

图5-添加功能的ECS

ECS架构的另一个好处是系统的模块化和可扩展性。由于系统和组件是相对独立的,所以很容易添加新特性。例如,向花盆中添加声音与添加声音组件一样简单。添加诸如爆炸之类的新特性可以与创建新系统和组件一样简单。更新的图5显示了新特性如何对引擎的总体结构产生较低的影响。

3.3 缺点

虽然ECS系统可以解决OOP带来的一些问题,但是它们有自己的一些问题,隔离和独立是ECS系统的正反两面。当设计需要共享组件和跨系统通信时,就会出现负面影响。

共享组件的问题是包含关系和处理顺序,该概念促进系统耦合,这与ECS的隔离设计背道而驰。根据设计,共享组件的位置是不确定的,此外,处理实体的顺序可能是至关重要的。例如,在引擎中的移动、碰撞和渲染,流程顺序将更改可见的结果。例如,移动、碰撞和渲染的顺序可以创建这样一种情况:由于尚未处理碰撞,对象将会穿透墙壁。

跨系统通信是ECS的另一个难点。跨系统通信对于事件发生这一情况是必要的。例如,一个球与地面相撞时应该发出噪音。ECS的问题是碰撞和声音系统是不同的,彼此是隔离的。解决这个隔离问题的两种方法是向组件添加状态或实现消息传递系统。无论哪种方式,设计一种方法在保持系统独立的同时连接它们都是困难的。

4 CUPCAKE ECS

CUPCAKE ECS架构被设计成一个模块化的ECS系统,其目标是在不影响架构的情况下方便地添加或删除系统。这就要求系统之间是完全独立的,永远不要使用或引用任何其他系统。理想情况下,这将创建一个用户可以插入和添加新特性的引擎,并有望提高代码重用度和用户之间的共享。

用户如何使用Cupcake的一个例子是从基本ECS框架开始。这将包括管理系统、实体的实用工具,以及创建新系统和组件的框架。接下来,用户将从插件(系统)列表中选择所需的功能。例如,用户可以选择一个功能列表,如声音的FMOD, 3D渲染的OpenGL, 物理的Bullet。最后,安装脚本将插件和框架编译成静态库和一组头文件。结果将是一个高度可定制的、特定于用户的引擎。

4.1 架构

Cupcake架构分为几个部分:引擎、管理器和系统。图6显示了Cupcake的基本架构。引擎是所有系统、管理器和实体的顶层接口,它负责运行游戏循环和管理系统和实体。该引擎通过提供添加和删除附加实体来管理实体,同时为每个实体维护一个惟一的id,通过提供正在处理的系统和空闲系统两个列表来管理系统,正在处理中的系统由引擎游戏循环进行处理。

图6-Cupcake架构

系统负责管理组件和实现几种抽象方法。抽象方法包括init、release和update(用于正在处理的系统)。使用于FOD播放声音的系统的一个例子,是一个包含声音和位置的组件列表的系统。init函数将初始化FMOD库文件,release函数将释放所有与系统绑定的资源,如组件和FMOD库。update函数用于更新移动对象的声音位置。

4.2 Cupcake 方案

Cupcake包含与任何具有共享组件和跨系统通信的ECS系统相同的通用问题。为了解决共享组件的问题,Cupcake在引擎外部有一个外部组件集。这些外部组件在创建时被传递给系统,这允许多个系统之间具有相同的组件集,而不需要任何方法来保持数据的同步。此外,它还保持了系统之间的独立性。共享组件的一个例子是渲染和物理系统的位置,通过引用相同的组件列表,物理系统对位置所做的任何更改都将自动用于渲染。

采用外部消息系统解决了系统间的交叉通信问题。此消息传递系统由句柄、触发器和消息组成。触发器是针对特定条件在每个循环中执行的一段逻辑,根据这些条件,将向消息系统发送一条消息。消息句柄是为系统创建的对象,用于消息捕获和基于消息的逻辑执行。例如,一个玩家的移动需要三个部分,基于玩家输入执行的触发器,当触发器注册了一个玩家输入(事件)时,它将向系统发送适当的move消息,比如向前移动,然后,这个向前移动的消息句柄被物理系统捕获,然后物理系统将被调用,根据该句柄移动玩家。

4.3 分析

Cupcake系统通过提高系统的独立性和采用数据驱动架构,保持了实体组件系统的基础本质。解决常见ECS问题的方法是有效的,但是还有改进的余地。

共享组件是解决共享数据问题的一个相对简单的解决方案。外部共享组件的问题是包含共享组件的内容的模糊表示。除非明确指示,否则不清楚调用者应该在何处生成或获取共享组件。通过要求调用者被指示这个外部列表应该从哪里来,暴露了一个脆弱的设计。它允许调用者为相同的组件创建重复列表,这将达不到共享组件的目的。

事实证明,外部消息传递系统工作得相对较好。消息传递系统的外部性有助于提高系统独立性,并允许调用者定制消息的行为。外部消息传递系统的问题在于其与引擎的集成性较弱,创建一个关于如何处理跨系统通信的标准会更有用。通过将消息传递集成到引擎中,系统更有可能围绕标准消息传递系统进行设计。

最后,Cupcake的一个缺陷是引擎中去除了实体。由于组件存储在系统中,所以没有简单的方法可以从系统中删除实体。调用者必须手动地从各个系统中释放组件来删除实体。这就留下了一个问题––允许在设计系统时不实现在特定实体上释放数据的函数。

5 ARTEMIS ECS 框架

由Gamadu创建的Artemis ECS框架是为游戏开发而设计的,该设计与Cupcake很相似。实体是惟一的id,组件是数据,系统使用数据来处理逻辑。Artemis框架最初是用Java编写的,但是本文将使用C来适配Artemis框架,从Java到C,有几个特性没有实现,所以该分析只对C++而而言是准确的。

5.1 架构

Artemis框架包含一个中心化的World对象,该对象是这个框架的接口。World对象包含所有manager并运行游戏循环,此框架中使用的manager用于系统、实体、组(group)和标记(tag)。系统和实体的manager类似于Cupcake,它们用于管理实体或系统。组和标记管理器是简单的系统,用于将实体组织在一起并向特定实体添加标记,通过将一些字符串关联到一组实体或单个实体来实现的。

Artemis中的实体管理器是惟一的,因为它将所有组件存储在引擎中,这是通过一个二维数组完成的,其中第一个标识符是组件类型,第二个标识符是实体id。实体管理器还为每个活动实体管理惟一的id。

Artemis中的系统要管理需要处理的实体列表,这是通过为每个组件类型分配标识符来实现的。使用组件类型,系统可以通过过滤只处理有效的实体。为了访问数据,调用者必须创建组件mapper,以便从实体管理器获取组件。

5.2 分析

Artemis有一个独特的解决方案来解决共享组件问题,通过将组件附加到实体管理器,这使得所有系统都可以访问组件,这也使得组件在使用它们的系统之间自动同步。此外,它还消除了谁包含共享组件的歧义问题,因为所有组件都存储在同一个位置。这在调用者需要删除实体时非常有用,因为实体管理器可以搜索组件列表并删除与实体关联的所有数据。

Artemis的一个问题是缺乏跨系统的通信,系统之间唯一共享的是可以用作消息的组件。然而,由于创建和删除组件的开销很大,因此这可能是一种开销很大的消息传递方法。

最后,Artemis最大的限制是组件和系统类型的限制,与组件和系统关联的惟一id是用位(bit)完成的,比特数被限制在32个,这就产生了一个问题,当用户超过组件或系统限制时,过滤就会失效。新组件分配了未定义的类型,这样就允许将无效实体传递给系统(译注:言外之意是有问题的)。

6 提出的架构

本文提出的ECS系统架构是Cupcake和Artemis的结合,Cupcake解决了跨系统通信问题,并保持了系统的独立性。通过在实体管理器中本地化所有组件,Artemis解决了共享组件的问题。这两种技术的结合解决了ECS的问题,同时保持了模块化。

6.1 架构

这个架构像Cupcake和Artemis将有一个中心接口对象称为引擎(engine),引擎将负责管理器和游戏循环。引擎所包含的管理器是系统、实体、资源和消息。引擎还负责使用系统管理器处理“正在处理的系统”的每个游戏循环。

图7-引擎对象

系统和实体管理器的工作方式与Artemis相同,其中组件存储在实体管理器中。组件类型将具有唯一id,以便允许系统过滤组件类型,通过使用组件id,系统可以创建具有适当组件的有效实体列表并处理它们。

新增的资源管理器存储组件可能共享的信息。对象的mesh是多个组件共享数据的一个例子,例如,如果游戏中存在50个相同的敌人,那么为每个实体创建重复的mesh数据将会非常昂贵。资源管理器将处理在实体之间共享完全相同数据这一理想情况。

消息管理器将负责跨系统通信,与Cupcake一样,它将由调用者创建的处理程序(handler)组成,这些处理程序捕获消息并在系统中执行逻辑。系统将负责向消息处理程序发送消息,而不是触发器。

7 使用ECS的项目

在这个项目中,实现了两个不同的ECS系统,并在一个简单的游戏环境中使用。Cupcake引擎是用C编写的,用于测试应用程序Cron。使用基于Artemis框架的C开发了用于CPE 476项目Carrota的引擎。

7.1 Cron

图8-Cupcake测试应用

开发Cupcake引擎时,创建了一个测试应用程序来测试引擎的功能。图8显示了示例测试应用程序。球体代表玩家,用户可以在其中使用控件移动对象。此外,位置音频是基于球的位置。这些立方体代表了物理作用于它们的测试对象,它们播放恒定的音频。这是用来确保3D音频正常工作。

7.2 Carrota

图9-Carrota兔子

图10-Carrota商店菜单

图11-Carrota甜甜圈

利用Artemis框架为CPE 476项目开发了ECS引擎,使用该引擎创建一个FPS防御游戏。该引擎的特性包括:2D/3D/文本呈现、声音、碰撞、物理、动画、framebuffer对象和阴影,其中每个功能都是相对独立。

8 未来的工作

在未来的工作中,将提出的ECS方案继续发展和完善,首先构建框架,然后实现功能。希望能够实现支持3D渲染和实时图形的功能,最终目标是创建一个冗长的框架,其中包含多个可以跨引擎共享的特性。

参考文献

[1] Entity-Component-System-Revisited, - component-system-revisited.html?m=1: July 6th, 2013.[2] Randy Gaul, Component Based Engine Design, - based-engine-design/: May 20th, 2013.[3] Ted Brown, Fast Entity Component System, : January 18th, 2011.[4] Bob Nystrom, Component, [5] Gamadu, Artemis Entity System Framework, [6] Alec Thomas, EntityX, : May 15th, 2014.[7] Entity System Wiki,

翻译原文:

标签: #游戏引擎编程架构