[译] Designing a Parallel Game Engine (2)
原文:https://software.intel.com/en-us/articles/designing-the-framework-of-a-parallel-game-engine/
此文章是为介绍多线程并行游戏引擎的经典文章,原文较长,这里分成2部分。本篇为第二部分
4. Interfaces
接口即Framework,manager,system之间的通信桥梁,framework和manager是直属Engine部分,所以他们直接可以相互直接调用没问题,但是system前面说了,它属于Engine之外的部分,是不同功能高度独立的抽象模块,它无法直接访问Engine内部接口,同理Engine也无法直接访问System。Interfaces部分抽象了一部分framework以及manager,system模块的接口,类似一个中间件把两部分连接起来,如此,Engine和System相互透明,不需要直接各自的具体细节,这对于系统的后期迭代,更新,甚至system的替换都有巨大的好处。
4.1.Subject and Observer Interfaces
事件和观察者接口用于观察者注册对应感兴趣的事件。同时提供了一个默认的通用实现(笔者认为这部分的抽象可能意义并不是特别大)
4.2.Manger Interfaces
尽管Manager是单例,但仍然只能被engine framework访问(假设system以dll的方式动态链接至引擎,那么在system中访问manager单例会导致出现2个单例的问题,这通常会带来各种奇怪的bug)。Manager会针对自己的部分公共接口提供interface以便system访问
4.3.System Interfaces
一个system有4个component组成,分别是System,Scene,Object,Task,这些components在第5章会详细介绍。System接口提供了创建,销毁Scene的方法,Scene接口提供了创建,销毁Object以及获取Task的方法,在Task Manager中发射Task时会使用Task接口。
4.4.Change Interfaces
System之间也有若干接口,某个system对数据的修改如果需要其他system可见,那么就需要提供system之间的接口。举例来说,Geometry system对场景内模型位置,缩放等属性的修改可能需要让Physics system可见
5.Systems
system为引擎提供游戏功能。没有它们,引擎只会空转,没有任何任务可执行。为了让引擎不需要知道所有不同的系统类型,系统必须实现4.3节“系统接口”中描述的接口。这使得向引擎添加新系统变得更加简单,因为引擎不需要知道system的实现细节。
5.1.Types
引擎应当提供默认必须的system,例如:几何,图像,物理(刚体碰撞),音频,输入,AI和动画。 游戏内的一些其他特定的功能也建议使用自定义system。
5.2.System components
System的组件关系图如下:
System和Engine的关系如下:
5.2.1.System
system组件负责初始化系统资源,这些资源在引擎的执行过程中或多或少保持不变。举例来说,graphics system分析所有传进来的资源位置,以确定它们位于何处,以便在使用资源时更快地加载。另外,system组件还负责设置一些系统参数,譬如屏幕分辨率
system组件是framework的主要入口点,它提供了system本身的信息例如类型,以及创建或者销毁scene的方法等。
5.2.2.Scene
scene组件负责处理场景相关的资源。universal场景使用这个场景作为其功能的扩展。比如,physics scene作为专门处理物理碰撞的scene,在场景初始化时为世界设置重力。
scene组件提供了创建,销毁object的方法,同时还管理task组件(task组件是对scene内部的一些操作),并提供获取task的方法。
5.2.3.Object
object组件是scene中的一个对象,通常与用户在屏幕上可见的物体相关联。universal对象使用此对象组件作为其功能的扩展。
举例来说,一个universal对象扩展出了geometry、graphics和physics来创建大世界里的一根木头。geometry保存对象的位置、方向和缩放信息,graphics system使用对应的网格将其显示在屏幕上,physics system对其应用刚体碰撞,以便它将与其他刚体物体和重力相互作用。
在某些情况下,system object可能对不同universal object或其扩展object之一的更改感兴趣。在这种情况下,可以建立一个链接,以便系统对象可以观察其他对象。
5.2.4.Task
Task组件,负责对scene中的object进行操作,譬如当task组件接收到了一个command,随即会对object进行某些操作。
Task支持细分子任务,以实现更加细粒度的多线程并行(注意到,system之间本身是完全并行的)。这部分在第一篇文章中有所提及,实际上就是data decomposition。比如说对多个object并行draw
6.全局视角
本章主要讲如何将之前引擎划分的各个模块之间关联起来,整个引擎的执行流分为哪几个阶段。
6.1.Initialization 阶段
• 框架调用scene loader来加载场景
• scene loader决定有哪些scene需要被使用来加载对应需要的模块(可能是LoadLibrary或者其他方式)
• platform manager加载对应模块后,使用接口创建对应的system
• 模块返回一个system interface的指针
• system模块还会注册其提供的一些服务
6.2.Scene Loading 阶段
初始化结束之后开始load scene
• Loader创建一个universal scene并通过interface创建system scene。
• universal scene检查每个system scene,查看他们可能会做出哪些共享数据更改,以及他们希望接收哪些共享数据更改。
• universal scene注册对应的system scene到state manager里,以便这些scene能够接收到他们感兴趣的数据更改消息。
• Loader为每个scene中的object创建一个universal object,并决定哪个system会对其进行扩展。
• Loader通过interface为每个universal object创建对应的system scene object
• 调度器查询scene中的primary task,然后在任务执行期间分发到task manager
6.3.Game Loop阶段
游戏主循环部分
• platform manager处理所有需要的窗口消息
• 对应的消息执行函数传递给调度器,调度器需要等待当前时钟周期到期再执行
• 对于free step模式,调度器检查哪些任务是在上一个时钟周期中已经执行完毕了。
• 调度程序确定哪些任务是需要在当前时钟上完成,并等待这些任务的完成。
• 对于lock step模式,调度器直接分发所有任务,并在每个时钟周期中等待他们完成。
6.4.Task Execution
• task manager将所有的task推入task队列,然后分发到不同的线程
• 当任务执行时,它们将对整个场景或特定对象进行操作,并修改它们的内部数据结构。
• 所有的共享数据,比如位置和方向,都需要传播到其他系统。系统任务通过让systerm scene或system object通知它们的observer来实现这一点。observer实际上就是位于state manager中的change controller
• change controller把所有的change事件保存在队列中以便后续处理
• 如果task需要使用service,将会call到service manager中。servide manager也会改变不同system的不走消息分发机制的那些属性,比如:玩家改变了graphic system中的屏幕分辨率
• task也可以call到environment manager来读取环境变量,改变runtime状态
6.3.2. Distribution
一旦所有当前时钟周期的task全部执行完毕,主循环调用state manger来分发changes事件
• state manager调用change controller来分发change队列中的change事件。会遍历每一个change事件看看是否有观察者在监听他们
• change controller通知对应的change事件给观察者。对于free step模式,观察者直接获取变化的数据。对于lock step模式,观察者需要从change事件中查询数据
• system object感兴趣的事件通常是挂在同一个universal object下的其他system object的内容。这有利于data decomposition,为了同步数据,通常把一个universal object下面的所有system object分成一个group
6.3.3. Runtime Check and Exit
这部分简单提了下runtime check,作者把game run,pause,exit,甚至scene的切换划分到了runtime check,这里不做赘述。
7. Final Thoughts
作者最后对于设计一个并行游戏引擎做了几点总结:
• data decomposition能够大大提高并行度
• 观察者模式非常有用,它能够实现不同的系统之间的数据共享
• 调度器在多线程负载均衡方面扮演了重要角色
8. 总结
看到这里,相信读者已经读完了译文,但可能会对很多细节有所疑惑,事实上本人在读原文章时也有诸多疑惑,但总的来说不必深究,因为这篇文章并没有对某个游戏引擎的知识点做深入分析,仅仅只是在一个引擎框架设计层面提供了并行化,模块化的解决方案。原文是2009年的文章,随着后来引擎架构的演进以及UE的开源,部分设计目前已经有更好的方案,比如ECS。但其中data decomposition,interface的一些思想于我们对引擎的理解仍然有非常大的价值。