Execution Sequence(执行序列)
本文着重介绍了Unigine的执行序列。 在这里您将了解到有关Unigine引擎内部的运行机制。
想要获知有关Unigine引擎工作流的更深入介绍,可参阅Engine ArchitectureEngine Architecture(引擎架构)一章。
下面的图表分别展示了单线程模式和多线程模式下的Unigine引擎内部代码以及您项目脚本的执行顺序。 其主要阶段包括:
Unigine Runtime Scripts(Unigine运行时脚本)#
Initialization(初始化)#
当主程序(64-bit版本的.exe文件,调试版本或发布版本都包括)加载相应的Unigine库(.dll,或.so)时引擎的初始化开始。 从即刻起,Unigine便会触发一系列如下事件:
- 初始化Unigine内存分配器;相比于系统默认的分配器,它拥有更快的初始化速度以及更优的配置。 在引擎代码中是通过指令USE_MEMORY来设置它的。
- 初始化C++/C# API。
- 设置应用目录(<Unigine SDK>/bin/)路径和用户根目录(如果有的话)路径。Windows系统中,根目录为C:/Users/<username>/。Linux系统中为 /home/<username>/。
-
解析命令行选项,它们包括:设置data文件夹路径,external packages(外部程序包)和directories(目录)路径, plugin directories(插件目录)路径, log(日志)文件和configuration(文件和)文件路径,以及名中带有project name(项目名称)的目录的路径。
命令行的解析结果将覆盖各选项的默认值以及配置文件中的设定值。
- 创建四(4)个线程,它们分别是:Sound(音效),World(世界),Render(渲染)和File System(文件系统)。
-
引擎将根据实际情况将应用的所有数据保存到默认的路径下:
- 如果您是通过命令行或是在engine initialization(引擎初始化)project name(项目名称),那引擎就会根据操作系统的不同将所有数据(像日志文件,缓存文件和配置文件)都分别保存在用户的根目录中:
- 对于Windows系统,存储在C:/Users/<username>/<project_name>/文件夹下
- 对于Linux系统,存储在/home/<username>/.<project_name>/文件夹下
- 其它的情况,数据将被保存在应用的文件夹(<Unigine SDK>/bin/)中。
- 如果您是通过命令行或是在engine initialization(引擎初始化)project name(项目名称),那引擎就会根据操作系统的不同将所有数据(像日志文件,缓存文件和配置文件)都分别保存在用户的根目录中:
- 创建日志文件。 如果未在命令行选项中指定,那日志文件将默认使用log.html这一文件名并被存储在应用目录下。
- 如果external packages(外部文件压缩包)和directories(外部文件目录)设置有密码保护(如果有的话)且已在命令行中给出,那引擎将检查该给出密码是否与为二进制文件所设的密码匹配,匹配的话才加载它们。
-
加载并解析配置文件:
-
如果在命令行中指定了-engine_config选项,那配置将按如下方式从给定的*.cfg文件中加载:
- 如果已指定了project name(项目名称),并且拥有给定名称的配置文件存储在含有application data(应用数据)的文件夹中,那配置将从这里加载。
- 其它的情况,配置文件都存储在-engine_config选项所指定的文件夹中,配置将从这里加载。
- 如果未指定配置文件,那引擎将使用应用文件夹的路径。 对于这种情况,Unigine将加载默认的unigine.cfg配置文件。
配置文件中存有各种与世界相关的设置:像渲染的标记和参数,editor(编辑器)设置等等。 如果有设置在项目中做了调整,那更改信息将被保存进该文件。
-
如果在命令行中指定了-engine_config选项,那配置将按如下方式从给定的*.cfg文件中加载:
-
再次解析命令行选项;不过,与之前所解析的选项有所不同。 这些选项指定了基本的视频设置,像渲染用的图形API(DirectX或OpenGL;同样您也可以禁用图形API),应用窗口大小等等。
您也可以通过命令行传递任意的外部#define指令以及console variables(控制台变量)。 外部#define指令也可以在任何时间通过控制台来设置。
命令行的解析结果将覆盖各选项的默认值以及配置文件中的设定值。 - 设置*.cache文件的项目路径。 通常,cache files(缓存文件)包含有编译好的Shaders(着色器), System(系统)脚本和Editor(编辑器)脚本。
-
加载external plugin(外部插件)库和存储在插件目录中的插件库,它们分别由命令行选项-extern_plugin和-plugin_path来指定。
自定义插件库可以在Unigine运行时的任意时刻被加载。
- 初始化file system(文件系统)。按名称来缓存命令行选项data_path所指定目录下的所有文件。 如果项目文件被打包成了带有密码保护的UNG或ZIP格式的archives(存档),那引擎将检查给出密码是否与为二进制文件所设的密码匹配(如果有的话),匹配的话才加载这些存档。
- 初始化渲染管理器和音效管理器,它们会自动组织并处理渲染和音效。
- 根据给定设置创建Application Window(应用窗口)。
- 创建所有引擎子系统(像可视器,物理,音效,渲染,寻路子系统等等)。
- 运行音效线程和文件系统线程。
- 设置被传递进-extern_define命令行选项的外部#define指令。
- 调用所有已加载插件中的init()功能。
- 排队命令行中的控制台命令以便做进一步的命令执行。
-
加载并启用System(系统)脚本。 System(系统)脚本主要执行必要的内部管理,它用以启动和维持基于Unigine引擎构建的应用的运行。 它在整个Unigine运行时阶段都保持加载状态。
【默认的】System(系统)脚本会在初始化时进行如下操作:
- 加载本地文件。
- 初始化主菜单。
- 如有所需,可设置splash screen(启动画面)。 我们建议在加载世界之前最好是设置启动画面。 因为对于这种情况,显示启动画面的过程会让引擎有时间来加载资源以及编译好的着色器。 默认情况下,会显示标准的Unigine启动画面。
OpenGL不支持着色器的缓存,因此每次应用启动时它们都要重新编译。 相反,DirectX就支持着色器的缓存,所以,在同种硬件设备上,后续的应用启动都会快很多。 事实上,在应用运行期间可以在任何时间加载着色器,不过它们的编译要花费时间。
- 加载标准的属性库。
您可以不用data/core/unigine.cpp里存储的默认System(系统)脚本,而是使用自定义的脚本来替代它。 在该脚本中,您可以为项目设置专属它自己的启动画面,指定想要加载的自定义模块或材质库等等。 对于大型项目,也是很有必要指定世界的,它是您想在自己的System(系统)脚本中正确加载的。如果您使用了【自定义的】System(系统)脚本,那它就会执行您实施的逻辑。
- 运行之前排好队的命令行中的控制台命令。
如果您要运行System(系统)脚本中的控制台命令,那它们将会在已排好队的控制台命令被执行完后运行。
-
如果在命令行中指定了控制台变量render_manager_check_shaders,那着色器将被编译和缓存。 例如,如果您在System(系统)脚本里的init()功能中加载了自定义材质库,那么在这一步中,所有的这些库都会被编译,您也就无需在运行时中加载它们了。
话虽如此,不过由于着色器的缓存文件很大,如果在引擎初始化时加载它则会降低性能。
待所有这些步骤都执行完后,引擎进入主循环。
Main Loop(主循环)#
当Unigine进入主循环时,它的所有动作都可被分成三个阶段,并在一个循环中被接连执行。 包括有:
在性能分析器profiler中,主循环花费的总时间由Total(总体)计数器显示。
主循环的执行序列拥有2中模式:
Single-Threaded Mode(单线程模式)#
Unigine通常都在主循环中快速完成它的所有阶段,之后GPU才能真正渲染帧。 这也是为什么要引入【Double Buffering(双缓冲)】的原因:通过将GPU的缓冲区换成被执行渲染的缓冲区(后台缓冲区与前台缓冲区交换)便可实现帧的更快速渲染。
当所有脚本都更新完,所有的CPU计算都完成时,GPU仍然在渲染着由CPU计算来的帧。 因此,CPU就不得不等待GPU完成帧的渲染以及渲染缓冲区的交换。 这一等待时间的周期由性能分析器(profiler)中的【Present(呈现)】计数器表示。
如果【Present(呈现)】时间过长,这可能就意味着存在GPU瓶颈,建模内容也需要优化。 但是如果这种情况下帧速始终很高,那说明您还有可用CPU资源来处理虚拟世界中更多数量的对象。
对于从所有脚本都更新完,所有的CPU计算都完成的时刻到GPU完成帧渲染的时刻这一段时长,它也取决于是否启用了Vertical Synchronization(垂直同步)(简写为VSync)(可通过【System(系统)菜单】来操作)。 如果启用(enabled)了它,那CPU不仅要等待GPU完成渲染,还要等待垂直同步的执行。 这种情况下,Present(呈现)计数器的值会更高。
下面4幅示意图给出了如下信息:
- 前2幅示意图给出了在disabled(禁用)VSync时帧的计算和渲染(两种情况下都不考虑监视器的垂直回扫过程):
- 第1幅图中,CPU的帧计算要【快于】GPU的帧渲染。 因此,CPU要等待GPU(Present(呈现)时间会很长)。
- 第2幅图中,执行的CPU计算要【慢于】GPU的帧渲染。 因此,GPU不得不等待CPU完成帧的计算。 这种情况下,Present(呈现)时间会很短。
- 示意图3和4给出了在enabled(启用)VSync时帧的计算和渲染(要考虑监视器的垂直回扫过程):
- 第3幅图中,CPU的帧计算要【快于】GPU的帧渲染,CPU要等待GPU。 不过,对于这种情况,CPU和GPU也都要等待VSync的执行。
- 第4幅图中,CPU的帧计算要【慢于】GPU的帧渲染。 这种情况下,GPU不仅要等待CPU完成帧的计算,还要等待VSync的执行。
Update#
Update(更新)部分包括如下内容:
-
开始计算FPS值。
FPS的计算仅从已渲染的第2帧开始,而略过第1帧。
- 在上一帧输入的控制台命令会在当前帧执行。 这些命令会在update()循环开始之时便被执行,不过也要确保它们早于脚本的更新,否则的话就可能会妨碍到渲染或物理计算的当前进程。
-
如果之前执行了控制台命令video_restart(未在当前更新阶段执行),那么当前就会创建World Shaders(世界着色器)。
当视频发生重启时(例如,当video mode被改变时),应用窗口会调用World(世界)脚本,System(系统)脚本,Editor(编辑器)脚本以及插件的destroy()方法。 不过,它不在当前更新阶段执行,而是在早前的阶段中调用。
- 调用Plugin(插件)的update()功能。 期间都有何动作只取决于该自定义功能的内容。
- 更新Editor Script(编辑器脚本),它负责处理所有与编辑器相关的GUI和逻辑。
-
更新System Script(系统脚本)。 【默认的】System(系统)脚本的update()功能将执行如下操作:
- System(系统)脚本负责鼠标的处理。 它控制着:鼠标被点击时是否要对这一动作进行捕获(默认捕获),有一段时间不移动鼠标时其光标是否要消失(由【MOUSE_SOFT】定义),或鼠标是否不由系统处理(由【MOUSE_USER】定义,它允许某些自定义模块进行输入处理)。
- 更新主菜单逻辑(前提是未做【MENU_USER】定义)。
- 处理与系统相关的用户输入。 例如,是否要保存或恢复虚拟世界状态,或是否要对当前的Unigine窗口内容进行截屏。
- 如果初始化了插件GPU Monitor(做了【HAS_GPU_MONITOR】定义),那插件的输出内容将在应用窗口中显示,同时附加的控制台命令也变为可用。
- 更新GUI和环境声源。
-
如果World(虚拟世界)被加载了(可通过控制台或System(系统)脚本完成),那World Script(世界脚本)也将被更新。 在World(世界)脚本的update()功能中,您可为自己的应用逐帧编码它的行为(点此了解详情)。
虚拟世界以及它的World(世界)脚本将按如下顺序被更新:物理(如果有的话)以及非渲染游戏逻辑可在flush()功能中单独实施,该功能可以固定帧速被调用(update()功能则是每帧都调用)。
- 执行World(世界)脚本的update()功能:更新节点参数,设置非物理节点的变换等等。
- 更新存在于虚拟世界中的State of Nodes(节点状态)(主要针对的是可见节点):播放蒙皮动画,粒子系统喷射新粒子等等。 触发的World Callbacks(世界回调函数)将被添加到堆栈(它们会在稍后执行)。
- 调用World(世界)脚本的render()功能。
- 调用Editor Script(编辑器脚本)的render()功能。
- 如有所需,可执行System Script(系统脚本)的render()功能(点此了解详情)。 它可以访问节点状态中的已更新数据,并在同一帧中对行为进行相应地更正。
- 更新世界空间树。
- 调用Plugin(插件)的 render()功能,前提是有插件。
在性能分析器(Profiler)中,更新阶段的总时间由Update(更新)计数器显示。
Rendering(渲染)#
一旦完成了更新阶段,Unigine引擎便可开始渲染虚拟世界了。 此时物理和游戏逻辑将被并行计算。 这一方法能有效平衡CPU和GPU间的负载,从而使基于Unigine引擎构建的应用能跑出更高帧速。
下面详细介绍了单线程模式下的渲染阶段是如何进行的:
-
由于图形和音效须同处于当前帧中,因此Unigine便分别渲染了图形现场(指的虚拟世界)以及音效现场。 图形现场被发往GPU,而音效现场则被发往声卡。 一旦CPU完成了数据准备并将渲染指令注入到了GPU,GPU便会全力进行帧的渲染。
在性能分析器(Profiler)中,渲染阶段的总时间由Render(渲染)计数器显示。 在此之后,CPU 便被闲置下来,这时我们就可以加载它来进行所需的计算了。
- 物理模块调用Plugin(插件)的flush()功能,前提是有插件。
-
物理模块调用要被执行的World Script(世界脚本)的flush()功能。 在flush()功能中,您可以:
- 修改物理
- 实施您自己应用的逻辑(具体请看下方的介绍)。
在这里并不是每帧都调用flush()功能。 物理模块拥有专属的固定帧速,它不依赖渲染帧速。 在物理的每帧(或者物理的每次Tick)中,引擎都会执行大量的计算迭代(每次迭代前都要调用flush()功能)。
-
更新物理模块:内部物理仿真启动。 在该步骤中,Unigine会对全部拥有Physical Bodies(物理实体)和Collision Shapes(碰撞形状)的对象执行碰撞检测。
在性能分析器(Profiler)中,flush()的调用连同物理仿真的总时间由Physics(物理)计数器显示。 - 更新Pathfinding(寻路)模块。 在Thread(线程)的性能分析器(Profiler)中,寻路的总时间由PathFind(寻路)计数器显示。
- 渲染插件的GUI(前提是插件有GUI)。
- A最后,如有所需,可对所有GUI的顶层进行渲染。 在性能分析器(Profiler)中,界面渲染的总时间由Interface(界面)计数器显示。
Swap(交换)#
交换阶段是主循环中的最后一个阶段。 它包含如下内容:
- 如果之前执行了控制台命令video_grab,那么在该阶段所获得的截屏将被保存到存储有Application Data(应用数据)的文件夹中。
- 调用Plugin(插件)的swap()功能,前提是有插件。
-
与已渲染的虚拟世界进行物理同步(Synchronization)。 这时物理计算的结果将被应用到虚拟世界。 换句话说,在上一阶段我们已经计算了Physical Bodies(物理实体)是如何借助Collision Shapes(碰撞形状)来改变它们自身的位置和方向的(因为我们在flush()功能中实施了逻辑或交互)。 现在这些变换可以被最终应用到节点,也就是已渲染的网格上。
由于物理同步是在渲染阶段之后进行的,因此所应用的物理变换也就只能在下一帧的屏幕图像上才可看到。
- 更新性能分析器(Profiler)中的显示值。
待swap()阶段完成后,会由Application Window(应用窗口)像上面描述的那样启动GPU缓冲区的交换。
Multi-Threaded Mode(多线程模式)#
下面的示意图展示了enabled(启用)VSync时帧的计算和渲染:
在多线程模式下,执行流水线在某些地方是以不同方式实施的:
-
在Update(更新)阶段,World(世界)脚本会利用所有的可用线程来更新可见节点,而不是逐个更新它们。 通过该方式,Unigine可分析节点的依赖关系,因此这些操作完全是线程安全的。 控制台命令world_threaded用来设置模式:多线程模式或单线程模式。
1个父节点下的所有子节点总是使用1个线程来更新。 为了能发挥多线程的优势,要执行计算的多个巨型节点(像粒子系统)就应拥有不同的父级或是无父级。 由于每个Node reference(节点引用)都被按没有任何父级的根节点来处理,因此它会被自动采用多线程来优化。
-
此外,在更新阶段的末尾,多线程的物理和多线程的寻路开始使用它们各自的线程来进行更新。 之后,它们则会利用所有的可用线程来与Rendering(渲染)工作并行执行各自的任务。 控制台命令physics_threaded和pathfind_threaded用来选择计算模式。
带有须执行计算的巨型实体(bodies)(像布料和绳索)的多个节点不应有同一个父级;否则,它们会使用单线程来更新。
- 这种模式下的Swap(交换)阶段包括了等待所有附加线程完成它们任务的环节。 在此之后,物理线程和寻路线程将被同步,而它们的计算结果也会被应用到虚拟世界。
至此,主循环的重复环节结束。
Shutdown(关停)#
当Unigine引擎要停止应用的执行时,它会执行如下操作:
- 如果正在运行Editor Script(编辑器脚本),引擎就会调用该脚本的shutdown()功能。
- World Script(世界脚本)调用自身的shutdown()功能。
- Unigine调用System Script(系统脚本)的shutdown()功能。 比如,您可以在该功能中设置一个启动画面,用来在基于Unigine引擎构建的应用退出时显示"关于作者"的信息。
- 调用Plugin(插件)的shutdown()功能。
- 如果用户有对任何设置做了调整,引擎便会将更改信息保存到配置文件中。
- 终止World(世界)线程,Sound(音效)线程和File System(文件系统)线程。
- 释放分配给Unigine的所有资源。
- 销毁为应用设置的所有路径。
- 关停C++/C# API。
- 关停内存分配器。
Correlation between Rendering and Physics Framerates(渲染帧速和物理帧速间的相互关系)#
渲染帧速通常是变化的,不过,我们也在前面提到了,物理仿真的帧速是固定的。 也就是说,您是使用不同的频率来调用World(世界)脚本中的update()和flush()功能的。
上图描述了当物理帧速固定在60FPS,渲染帧速发生变化时所出现的情况。 通常来讲,有三种可能:
- 渲染帧速过【高】。 这种情况下,要对两帧或更多帧完成一次物理计算。 这不会引发任何问题,因为移动对象的位置被进行了插值计算。
- 渲染帧速与物理帧速完全相同或几乎相同。 这种情况也没问题,它是每一帧执行一次物理计算;物理会与图形保持同步,反之亦然。
- 渲染帧速过低。 这时问题开始显现。 首先,正如您从图中所看到的,每一帧要完成两次甚至是更多次的物理计算,这种情况不会加速整体的渲染进程。 其次,您不能将物理帧速设的太低,因为那样的话计算精度会丢失过多。
将物理帧速设的太高也没意义,因为如果计算耗时多于40ms,那物理计算也会不完全。 因此,如需进行附加的迭代,那进一步的计算也会被略过不计。
限定渲染用FPS为物理系统用FPS#
渲染用FPS可以被限定为物理系统用FPS,前提是渲染用FPS高。 为此要在代码中设置engine.physics.isFixed(1)标记。 这种FPS的限制允许引擎对已渲染的每一帧进行物理计算(而不是当该标记被设置为0时对它进行插值)。 在此模式下,如果物理对象拥有非线性速率,那它们就不会出现“抽搐”的问题。 (如果渲染用FPS低于物理系统用FPS,那该标记将不起任何作用。)