GPU Pro
译: By 王钰涵 2024 4.14
10.1 Introduction(简介)
引擎的定义在整个行业中有所不同。在最基本的层面上,该术语描述了一个代码库,它在多个项目中提供共同的功能。其目的是分享开发这些功能所需的资源成本。更先进的引擎提供了一个平台和工具,可以对游戏开发过程产生重大影响。引擎的架构决定了引擎的灵活性、功能性、可靠性和可扩展性,从而决定了它能够在多个项目中成功使用的程度。
强调模块化和封装,将大型系统分解为更易管理的组件,面向对象编程(OOP)的原则体现了许多这些特性。由于游戏往往以由对象构成的模拟为中心,大多数引擎在对象级别应用这些原则,创建各种对象类,通常在继承层次结构中,具有逐渐复杂的功能。
而面向 "Aspect" 的引擎则在引擎级别上应用这些原则。通过聚合,引擎由模块构成,称为Aspect,每个 "Aspect" 提供了一组严格的功能子集,例如渲染、音频或物理模拟。这些Aspect共享一个通用接口,允许它们与引擎的核心进行通信,并访问描述当前游戏状态的共享数据。在理论上,每个Aspec都是一个单独的引擎,具有非常特定的任务,通过一个狭窄的视角解释核心中的共享数据,以最好地适应它提供的功能。
10.2 Rationale(理由)
引擎的设计和开发不仅是一个编程问题,也是一个管理问题。对于人员和时间资源都有限的开发人员来说,开发引擎的方法将对参与开发和使用引擎的每个人产生巨大影响。这就意味着,任何此类引擎都不可能由一个专门的团队用几个月的时间来规划和执行;相反,它必须以项目的即时需求为出发点快速编写。
基于 Aspect 的引擎架构通过创建一个简单的接口和一套规则,减轻了管理负担。所有Aspect都必须按照这些规则编写。一旦核心及其与Aspect的接口被定义,Aspect本身的工作可以由个人或团队独立进行,拥有相当程度的自主权。Aspect之间及其作者之间的交互保持非正式,完全通过通过Aspect接口操纵引擎核心来实现。
因此,这种架构非常适合小型团队或分布式团队,因为他们没有能力建立一个专门的架构来设计和管理引擎的所有元素,但仍希望利用开发自己的技术所带来的好处。高度模块化的特性还允许改变开发方向,或容纳要求千差万别的多个项目。
接下来,我们将描述引擎架构的细节,这些细节可以通过引擎的核心、各Aspect以及它们之间的交互来概括。
10.3 Engine Core(Engine核心)
Core 是引擎最重要的元素;所有Aspect都可以随时更换或更改,但每个Aspect都高度依赖核心提供的界面和功能,因此核心在整个开发过程中保持稳定至关重要。引擎核心的功能是存储游戏或模拟的结构和状态,各Aspect都将在此基础上运行。顾名思义,核心是构建引擎其他部分的中心和基础(图 10.1)。
10.3.1 Scene Graph(场景图)
引擎设计的核心组件之一是游戏模拟环境的表示法,即对象和概念的实际逻辑表示法,这些对象和概念相互作用,创造了游戏中的一切。这种表示法以某种场景图的形式存储,是一种树形结构,其中每个节点都代表模拟中的一个兴趣点。不过,它的功能并不像场景图一词所暗示的那样严格。这棵树并不一定代表单一的物理空间;一个分支或子图可能存储三维场景的信息,而另一个分支或子图可能存储二维图形用户界面的信息,还有一个分支或子图可能存储纯粹的抽象数据(图 10.2)。
10.3.2 Scene Node(场景节点)
由于场景图并不对其子图强加特定的含义,因此模拟的结构完全是通过数据来定义的。这些信息存储在场景图的节点中,必须由各Aspect进行解释。为了提高灵活性,场景图的节点不像通常那样使用继承层次结构来定义,而是在运行时通过聚合来构建。
场景图中的每个节点都存储了一个数据属性列表,通过名称和类型进行标识(图 10.3)。节点的含义及其属性的构成不受引擎设计的限制或定义,可根据需要表达任意数量的含义。
节点可以描述一个物理对象,如三维场景中的灯光或摄像头。同样,它也可以描述角色动画装备中的一根骨头或一个鼠标光标。它甚至可以代表一个抽象概念,如游戏规则中的胜利条件。每个节点的含义由其在图中的相对位置、属性以及引擎其他部分对这些内容的解释决定。
10.3.3 数据访问 (Data Access)
使用聚合技术实现节点的一个后果是,没有接口提供直接访问节点所含数据的方法。访问必须通过请求,即调用代码查询节点是否包含具有所需名称和类型的属性。然后,节点会返回一个访问指针,该指针模板化为正确的类型,并递增一个引用计数(图 10.4)。
在多线程环境中,通过这些指针和核心元素的安全访问,需要调用线程获取互斥锁(mutex)。在这种情况下,通常更有效的做法是复制任何相关数据,并在内部表示对象上操作,这些对象可以在定义的点与核心场景图同步。这种方法有助于避免数据竞争,并确保在同时访问共享资源时线程安全。通过使用互斥锁和内部表示,开发人员可以有效地管理同步,并在多线程环境中保持数据的完整性。
10.3.4 Subgraph State (子图状态)
场景图中的节点可以处于几种状态之一,这些状态会影响以该节点为根的子图中的所有子节点(图 10.5):
• Active. 当节点处于激活状态时,意味着所有相关方都能够处理它并更新与之相关的任何数据或内部资源。
• Inactive. 子图可以随时被停用。一旦处于非活动状态,节点应该被 "Aspect" 处理为就好像它们不存在一样,但可以保留任何资源。子图以非活动状态附加到场景中,以允许 "Aspect" 在更新开始之前初始化任何相关资源。
• Pending deletion. 所有子图中的节点都处于非活动状态,并将很快被删除。相关 "Aspect" 应该通过清理任何相关资源来为此做好准备。
与所有节点属性一样,子图状态的确切定义可由引擎用户自行定义。不过,由于此类状态的影响范围很广,而且各Aspect可能会对其做出不同的解释,因此应格外小心。
10.3.5 Event Queue (事件队列)
场景图及其组成节点描述了模拟的当前结构和状态。然而,这个模拟不会保持静态。随着模拟逻辑的执行,场景的结构和节点的内容将发生变化。
引擎 "Aspect" 必须意识到并对模拟状态的这些变化做出反应。虽然这可以通过 "Aspct" 实时的检查场景元素的变化来实现,但这样做效率会非常低下。更有效的解决方案是维护一个描述这些变化的事件队列,每个Aspect在更新期间都可以审查这些事件,忽略与其功能无关的事件。
事件与引擎的其余部分一样具有灵活的定义。每个事件都有一个描述事件性质的标识符,并可以提供两个指针,指向特定节点内的节点或数据。这足以描述模拟中的大多数事件,标记节点状态的变化,以及根据此状态变化的子图,节点内特定属性的变化,或两个节点之间的交互。
更复杂的事件可以使用引用节点内的附加属性来描述,或者通过创建一个节点在场景中代表事件本身。因此,两个节点之间发生碰撞导致声音和粒子效果的事件可以生成一个包含这些元素的新子图,而不是描述它们的事件。
10.4 Aspects ("方面")
引擎核心存储模拟的当前状态和最近发生的任何变化。它不关心模拟的内容,也不关心模拟如何运行。相反,引擎执行的所有操作都在 "Aspects" 内进行。
一个"Aspect"是引擎模块,旨在提供引擎功能的有限子集。这可能包括渲染、动画、物理、音频,甚至游戏逻辑。因此,"Aspect" 通常用于封装个别API的功能,用于任务,如物理,将API的内部模拟与引擎核心中的相应对象同步。每个 "Aspect" 的范围完全是任意的,一个单一的 "Aspect" 可以用来封装现有第三方引擎、框架或一组相关API提供的所有功能。同样,如果这样更符合其目的,一个单一的API的功能可能会在多个 "Aspect" 之间进行细分。
该架构的一个限制是,各方应遵守严格的依赖规则:它们可以共享基础引擎库、引擎核心和共享库,但不应该相互了解(见图10.6)。这意味着所有对共享资源的访问应通过引擎核心提供的接口进行,从而防止各方之间形成任何耦合或相互依赖。因此,如果两个 "Aspect" 共享具有特定类的基础库,这些数据可以被嵌入到实体中,就像任何其他类型的数据一样。由于这些数据只与相关 "Aspect" 有关,因此对于引擎的其余部分来说是不透明的。
各 "Aspect" 的设计应尽量减少此类共享。静态资源(如当前图形设备/上下文或窗口句柄)不应在各 "Aspect" 之间共享,如果共享不可避免,则应提供一个 mutex 来控制访问。
通过维护这些规则,各 Aspect 将独立运行,更易于维护和替换。使用一种应用程序接口的音频 "Aspect" 可与使用另一种应用程序接口的音频 "Aspect" 互换。因此,每个 Aspect 都可以独立开发和测试,并通过操作引擎核心进行交互。
引擎通过单独的接口管理所有 "Aspect" ,按照引擎用户指定的顺序初始化、更新和关闭各Aspect。理论上,这可以通过数据来实现,允许引擎在运行时从动态链接库中构建自身。
10.4.1 Scene Interpretation 场景解读
每个 "Aspect" 应该维护一个对其感兴趣的节点的内部引用结构。通过查询节点的属性并寻找特定模式来确定兴趣。例如,具有与刚体匹配的标识符的节点对物理Aspect感兴趣,而引用几何体和材质实例的节点对渲染Aspect感兴趣。这种结构的性质以及它引用节点的方式可以根据 "Aspect" 进行定制,从而为其提供的功能提供最有效的解决方案。这可以是一个空间树,用于基于节点的相对位置进行操作,例如针对相机节点对可见节点进行视锥体裁剪,或者是一个简单的节点链表,每帧可以迭代一次 (图10.7)。
当向引擎核心注册一个 "Aspect" 时,它会解析整个现有场景图,并注册对任何感兴趣的节点。从那时起,它就会接收到有关节点和场景图结构变化的事件,从而同步自身的内部结构。
10.4.2 Node Interfaces 节点接口
当出现一个新的子图时,"Aspect" 会检查其中的节点,查询数据模式是否表明该节点代表了 "Aspect" 所识别的概念。这种属性模式可以看作是节点通过其属性子集输出的接口(图 10.8)。
在大多数情况下,这些接口将由 "Aspect" 预定义,直接映射到其功能域内的对象。例如,音频 "Aspect" 可能将节点解释为潜在的声音发射器、接收器或环境修改器。而动画Aspect则对代表骨骼或蒙皮网格段的节点感兴趣。
在某些情况下,接口将通过加载到 "Aspect" 中的数据来定义。通过这种方式,引擎可以在确定节点表示所需对象之后,请求访问任意属性,通常使用单独预定义的接口。这使得引擎能够自动将节点的属性映射到脚本或着色器的输入,而无需理解节点或目标的内容。
通常,最好让 "Aspect" 使用接口将其内部对象与场景图同步,而不是直接在共享数据上执行操作。对于包装API的Aspect,在这些对象可能已经提供并且确实需要使用所需功能的情况下,这一点尤为重要。这使得 "Aspec" 可以并行执行操作;它们只需要在设定的点锁定引擎的核心,以将其内部数据与中央模拟同步。
10.5 通用的 "Aspect"
10.5.1 Render Aspect
《GPU Pro 3》中的章节“Designing a Data-Driven Renderer” [Revie 12] 详细描述了渲染Aspect的关键元素。该设计展示了核心场景表示的灵活性,可以定义具有各种概念的对象,从相机或灯光等具体实体到用于控制渲染顺序的帧图的抽象元素。它还描述了查询对象以检索着色器的任意输入的过程。
10.5.2 Logic Aspect
Logic Aspect是应添加任何特定于游戏的功能的地方。这是将场景图中的对象解释为游戏中代表它们的实体的Aspect,例如玩家、敌人、武器和增益道具。它将使用提供的游戏规则更新场景。
Logic Aspect的实现方式可以有很大的变化,并应根据项目的需求和团队的构成来确定。一个以程序员为中心的团队可能希望通过代码处理大部分逻辑,将引擎核心的接口隐藏在他们自己的实体类和结构后面。另一种方法是Logic Aspect可以完全由数据驱动,执行附加到各个节点的脚本文件,并将附加节点的内容作为参数或对象暴露在脚本语言中。
10.5.3 Data Instrumentation Aspect
通过实现一个简单地通过GUI公开核心组件的Aspect,引擎的用户可以直接观察引擎的内部状态,甚至编辑单个节点属性的值。根据开发这种 "Aspect" 的程度,它的功能可能会有所不同,从一个简单的调试工具显示属性值为格式化字符串,到一个能够操纵场景图结构并渲染复杂小部件以编辑节点属性的游戏内编辑器。
10.5.4 File Aspect
值得注意的是,引擎核心中没有将数据从文件加载到引擎中的功能。这是因为该功能也是由一个 "Aspect" 提供的。将场景数据加载到核心是由文件Aspect控制的,这个 "Aspect" 可以查找具有引用文件名属性的节点。一旦找到这些文件名,就会提交给工厂系统。
Factories. 工厂系统本身采用模块化结构。每种文件类型都与一个工厂模块相关联,该模块负责处理文件,并根据文件内容构建一个子图。然后,这个子图被传回工厂 "Aspect" ,Factory Aspect 就可以用它所代表的子图来替换文件引用节点。一旦插入到场景中,新的子图会被所有 "Aspect"(包括 File "Aspect")解析,确保子图中包含的任何文件引用也会被处理。
因此,在依赖规则中,工厂模块存在于相关 "Aspect" 之上,因此它们有可能了解各个相关 "Aspect" 。如果一个文件只包含与单个 "Aspect" 相关的资源,那么工厂可以直接与相关 "Aspect" 通信,甚至成为相关 "Aspect" 本身的一部分,从而避免将这些数据插入场景图的需要。
Scene conditioners(场景调节器).。当一个场景由各种文件类型构建,并通过递归解除引用节点时,以这种方式构建的结构可能会导致许多冗余节点和各种其他低效。为了对抗这种情况,进一步使用一套模块系统来迭代场景图的各个部分,分析和优化其结构。这些调节模块还可以用于 "Aspect" 所需的实体添加进一步属性,因此充当 "Aspect" 的预处理阶段。
当子图由工厂系统构建时,它会被预插入调节器处理。然后,一旦它被插入到场景图中,它可以被一组后插入调节器处理,以根据其在图中的位置上下文执行进一步操作。这些调节器执行的任务是特定于引擎、"Aspect" 设计和 加载的文件类型。它们的模块化特性使得可以为每个任务构建一个小型、高度专业化的调节器。
预插入调节器通常用于优化在资产创作过程中可能需要的场景数据,但在具体游戏中并不需要。在调整阶段这样做可以减少工厂的复杂性,从而实现更精细的控制。这些任务可能包括:
• 删除在资产流程中早期导出到匹配API特定文件中但仍存在于源文件中的碰撞几何体;
• 删除代表相机和其他UI元素的特定于编辑器的节点,这些元素存在于场景中。
另一方面,后插入调节器执行需要了解文件内容被取消引用的上下文的任务。这些任务可能包括
• 生成用于引用每个节点的唯一 ID;
• 将变换传播到取消引用的子图中,使节点的位置和方向与场景中子图的直接父节点相对应;
• 折叠冗余节点的长列,这些节点除变换外不包含任何数据,且只有一个子节点。这些节点通常是在取消引用过程中产生的,会人为地增加场景的深度。
Offline processing. (离线处理) 架构的灵活性使其不仅可用于构建各种引擎配置,还可用于构建处理相同数据集的工具。这些工具可用于离线处理或分析场景数据,使用的Aspect和条件与游戏本身所涉及的Aspect和条件截然不同。
这些工具可以内置到资产管道中,自动处理从制作程序导出的数据,并创建经过优化的文件,以便在一系列目标平台上直接加载到游戏中。
10.6 Implementation
这个引擎设计的关键原则之一是通过数据聚合而不是引擎代码中的显式类来构建场景节点。大部分与核心场景之间的交互将受到这一原则实施的影响,因此值得更深入地讨论。
节点本质上是容器,将每个属性的描述、名称和类型标识符与相关数据的指针关联起来。标准模板库(STL)提供了一系列具有不同特性和共享接口的容器(见列表 10.1),因此相对简单地选择一个以适应任何给定情况 [SGI 11]。在这种情况下,关联容器如 map 或 set(如果想允许重复属性名,则可以选择 multimap/multiset)是一个明显的选择,因为它具有易于搜索内容的特点,并且不要求元素存储在连续的内存中,这在插入/删除内容时可能会导致过度碎片化。
实际上,所有数据的搜索、插入和删除 ideally 应该限制在对象的初始化和关闭中,这样选择容器的重要性就较小了。可以编写自定义分配器来进一步减少容器内存重新分配的影响,尽管它们的实现超出了本章的范围。
属性数据可以是任何类型。但是,容器只能容纳单一类型的对象。因此,必须引入一层间接性,通过存储一组统一指针来管理非统一的属性数据集合。数据可以在堆上构造,然后返回一个 void 类型的指针。这有一个明显的缺点,即丢弃了关于数据的任何类型信息,并且可能调用其析构函数而无法将其强制转换回原始类型。
另一种解决方案是构造一个属性接口类,从中可以自动为每种属性类型派生一个模板化的类。这将允许通过接口类访问类型信息,并提供适当的虚拟析构函数以清理属性数据。通过使用编译时功能,通常作为运行时类型信息(RTTI)的一部分提供,可以检索一个简单的类型信息对象,该对象表示正在存储的数据类型,从而可以将其识别并与其他类型进行比较(见列表 10.2)。这样的实现将允许属性对象不仅存储正确类型的指针到其相应的数据,而且还存储属性的标识名称,并提供对类型信息对象的访问。因此,在搜索时不需要单独的键来识别属性,节点的内容可以使用带有自定义排序算法的集合存储,该算法通过查询接口指针以获取属性的名称和类型。
10.7 Aspect Interactions
各Aspect故意彼此独立,并且引擎的核心通过通用接口与它们进行交互。唯一了解引擎组成的代码将是用于组装所有相关模块、Aspect、工厂和调节器的项目特定代码,尽管即使这也可以理论上通过数据来执行。
在会话期间,引擎的核心将被初始化,然后依次初始化每个 "Aspect",然后将它们注册到核心以接收事件并获得访问场景表示的权限。一旦这个过程发生,引擎可以启动一个或多个线程,并从中执行任何Aspect可能需要的更新循环。在这里,应该解决任何 "Aspect" 优先级;引擎的正确功能可能取决于更新的顺序,并且通常会有一种最佳的顺序和频率来更新各个 "Aspect" 。
10.7.1 Aspect Update
每个 "Aspect" 都是独立自主的,实际上是引擎功能的垂直切片,每个 "Aspect" 都有自己的更新循环,执行管理核心场景和其自身内部资源的所有操作,以满足其功能子集(见图 10.9)。
当Aspect需要更新时,它必须在引擎的核心上获取锁定,然后处理自上次更新以来发生的任何新事件,解析已添加到场景中的子图或删除即将被删除的节点的Aspect。它还需要将自己内部对象的值与相应场景节点的值同步,然后释放锁定。此时,Aspect可能与引擎的其余部分并行更新,执行任何内部逻辑。一旦其内部状态完全更新,Aspect再次获取引擎核心的锁定,将核心实体与任何相关数据同步,并在再次释放锁定之前生成新的事件。现在,它必须等待一段时间,直到下一个更新步骤可以执行。
10.7.2 Example: Entity Changing Color Upon Taking Damage (示例:受伤后实体改变颜色)
这个简单的例子描述了在一个面向 "Aspect" 的引擎中可能发生的一系列操作,当游戏中的子弹击中角色时,导致目标的材质值发生改变。
-
1 在物理Aspect更新期间,一个子弹对象与角色的碰撞包围盒相交。这在物理 API 中生成了一个内部事件。在其更新结束时,物理Aspect与核心同步,将引用子弹和角色的碰撞事件推送到事件队列上 (10.3.5 Event Queue)。
-
2 当逻辑Aspect下一次更新时,它从队列中检索到碰撞事件。它识别了事件类型“碰撞”,并观察到了引用节点的两个。它调用了子弹和角色的碰撞处理器脚本函数。子弹的碰撞处理器要求将表示子弹的子图从场景中移除。角色的碰撞处理器将角色的内部状态更改为“受伤”,从角色的生命值属性中减去子弹的伤害属性,并将角色的颜色属性从白色改为红色。
-
3 一旦逻辑Aspect释放了对 Core 的锁定,渲染Aspect就能够开始更新。它注意到了子弹子图上的挂起删除状态变化,并清理了用于表示子弹的内部实体。然后,它继续将所有当前可见实体的着色器输入与其相应节点的属性同步。在这样做时,它从角色的属性中获取新的颜色值,当相关批次被渲染时,角色现在呈现为红色。
10.8 Praetorian: The Brief History of Aspects Praetorian: "Aspect" 的简史
Praetorian 是 Cohort Studios 的专有引擎,采用了所描述的基于 "Aspect" 的架构进行开发。该引擎的目的是让公司能够快速地在各种类型的游戏中开发原型,并迅速将最有前途的项目推向完整生产阶段。
最初计划使用中间件来开发这些游戏,以节省开发时间;然而,在评估第三方引擎时,很明显它们通常更适合某一类别的游戏,或者对可以添加的功能施加了限制。因此,决定让一个资源有限的小团队开始着手开发一个内部开发的引擎,该引擎可以在广泛的项目范围内使用。
另一种敏捷主题的方法可能是在项目中直接开发引擎功能,并在项目开发过程中重构代码,直到任何共同功能都形成可以共享的模块。然而,这样的方法可能需要更长的时间才能开发出一个可以作为各种游戏基础的引擎,需要几个项目达到完成状态。
新引擎的第一个目标是减少需要编写的新代码量,尽可能地重用现有技术或集成第三方 API。因此,创建一个核心场景表示,可以将这些不同的模块附加到其中,是合理的。
术语 "Aspect" 最初在研究中遇到,用于描述在 OpenSG 中对单个场景图进行多线程访问 [Voss et al. 02],表明每个线程或远程客户端将维护对场景图内容的特定视角。这与简单的多线程访问有所不同,转向了基于任务所需的这些视角的概念,然后进一步发展成了本章所描述的 "Aspect" 概念。
进一步的研究表明,采用基于聚合而不是继承的实体方法将进一步增加系统的灵活性 [Cafrelli 01]。这将巧妙地避开了开发一个可以满足所有Aspect需求的实体层次结构,而不会创建它们之间的依赖关系。
核心的最后一个组件是事件系统。尽管它在最初的设计中存在,但为了加快依赖于核心接口完成的Aspect的开发速度,它被删除了。
事件管理后来被实现,尽管它不是核心的中心部分。事件在变换Aspect中被用来正确地传播节点位置的更新到它们的子节点,然后在物理对象之间的碰撞事件中使用,并最终在场景图中实现以促进对场景图结构的异步操作,处理插入和删除。事后看来,即使意味着采用不太高效的设计,从一开始就实现事件处理也会更有效率。
随着 "Aspect" 的开发逐步向核心添加功能。引擎的第一个可用版本仅包括渲染Aspect和一个简单的文件加载模块,该模块可以解析 Collada 数据并将其推送到核心(后来正式成为文件 "Aspect" 使用的 Collada 工厂)。这允许从模型编辑软件导出资产并直接导入到引擎中。此后不久,物理 "Aspect" 的第一次通过允许使用附加数据导出的对象通过物理模拟进行更新。接着是一个脚本 "Aspect",它更新了具有关联 Lua 脚本的对象以执行游戏逻辑。
在相对短的时间内,项目团队能够开始在一个简单但功能强大的数据驱动引擎上构建游戏,随着需要,该引擎的功能不断增加。
10.9 Analysis
正如所有设计一样,基于 "Aspect" 构建引擎也有其优点和局限性。"Aspect" 架构的特性主要通过模块化和数据的灵活性来促进开发过程,但刚性的结构和间接性会限制效率。
优点。基于 "Aspect" 构建引擎的优点包括以下几点:
- 推动数据驱动的开发理念有助于吸引资产创作者和设计师。
- 高度模块化的插拔式架构允许快速更改引擎。
- 模块化的性质使得更快地跟踪和调试错误成为可能。
- 封装加速了第三方 API 的集成。
- 着色器和脚本输入的直接连接使得开发新的图形技术和原型游戏功能变得更容易和更快速。
- 将功能的知识和管理分散化增加了不同Aspect程序员的自主权。
局限性。以下是一些限制:
- 在 "Aspect" 中创建重复或冗余数据以及用于存储核心数据的聚合结构可能会显著降低内存效率。
- "Aspect" 的异步性质可能会使程序员难以处理,因为因果关系在代码中很少直接相邻。
- 试图在多个执行线程上保持Aspect之间的完全自治性可能会增加复杂性,因为需要确保数据的一致性和同步。
10.10 Conclusion
编写引擎的方法就像程序员的数量一样多样。面向 "Aspect" 的架构既是对开发者个性和项目需求的让步,也是一种引擎设计。同时,尽管严格的关于 "Aspect" 封装和访问共享数据的规则不可避免地限制了优化,但它们有助于结构化和指导功能设计,使得快速实现所需功能成为可能。
我们的目标是为开发者提供一个简单易用的框架,帮助加速引擎开发,同时留给他们在需要时探索结构和技术的自由。
观察到这种引擎架构在各种项目中的应用,并且总体上似乎达到了这些目标。总会有一些情况会考验引擎的能力,但迄今为止,在这种设计的限制范围内,还没有被证明是不可克服的。
Bibliography(参考)
[Cafrelli 01] C. Cafrelli. “A Property Class for Generic C++ Member Access.” In Game Programming Gems 2, edited by Mark DeLoura, pp. 46–50. Hingham,MA: Charles River Media, 2001.
[Revie 12] D. Revie, “Designing a Data-Driven Renderer.” In GPU Pro 3, edited by Wolfgang Engel, pp. 291–319. Boca Raton, FL: CRC Press, 2012.
[SGI 11] Silicon Graphics International. Standard Template Library. http://www.sgi.com/tech/stl/, 2011.
[Voss et al. 02] G. Voss, J. Behr, D. Reiners, and M. Roth. “A Multi-Thread Safe Foundation for Scene Graphs and Its Extension to Clusters.” In Proceedings of the Fourth Eurographics Workshop on Parallel Graphics and Visualization, pp. 33–37. Aire-la-Ville, Switzerland: Eurographics Association, 2002.