UDN
Search public documentation:

ContentStreamingCH
English Translation
日本語訳
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 主页 > 引擎编程 >内容动态载入

内容动态载入


概述


次世代游戏机平台在图形和计算处理能力方面呈现了令人难以置信的飞跃式发展。但是,在这些平台上内存仍然很珍贵。为了获得最大化的细节效果,次世代游戏需要使用动态载入及无缝世界来根据需要加载游戏内容。

动态载入


这里, streaming (动态载入) 的意思是: 按照可见性预测机制,根据需要,把大块(一般占很多兆字节)的原始内容以游戏平台的本地格式直接地优化加载到平台的内存中,从而不需要对飞行中的数据进行转换。

动态载入系统的支持目标不是复杂的面向对象数据,比如UObject衍生的类;一个关卡的UTexture对象(200-个字节的描述贴图的UObject衍生类),当它所在关卡被加载时这些UTexture对象将总是存在于内存中,然而和贴图相关的大的mipmap数据可能要进行动态载入。换句话说,动态载入支持的目标支持内容是大块的内容,而不是复杂的数据结构。

我们的唯一目标是使用这种方式来动态载入贴图mips,对其它的需要动态载入的内容使用包动态载入。我们计划不对音频进行动态载入。

无缝世界


虚幻引擎3支持无缝世界,无缝世界系统的目标是动态地在后台加载和卸载和那个关卡相关的复杂的面向对象数据。这个功能和动态载入结合使用,便可以使得和世界相关的所有数据都能动态地被载入。

虚幻引擎3中的世界(一个UWorld对象)可能有很多关卡(ULevel对象)组成;一个典型的游戏可能包含几百个独立的关卡。这些关卡可以基于接近度、精确的加载/卸载触发器以及其它的标准来动态地被加载及卸载。

关卡定义了一组actors和其它的面向对象数据,它们可以动态地进行加载及卸载。概念地讲,或者所有被给定关卡引用的对象都会被加载并对c++代码及脚本代码(也就是FindObject?)是可见的,或者什么都不能加载。这个原子性使得一个关卡引用的对象可以包含指向另一个任意对象的指针,这也是在actors、组件、材质以及游戏性脚本的复杂结构中经常发生的一种形式。因此C++和脚本代码永远不必处理所引用的物体还没有加载完成的情况。

尽管ULevel抽象的存在是支持加载及其它的基于每个文件的原子性操作,但是它们通常对于游戏性代码是不可见的。Actor及游戏性功能通过UWorld抽象化来进行暴露,这个抽象动作将会把当前加载的所有静态和动态actors都聚集到一个单独的列表中。因此,一般的游戏性代码不需要担心关卡的“边界”,并且actors在世界中到处移动时不会“改变关卡”。

异步包加载


无缝世界加载代码是基于在幕后完全加载包的能力的,它可以用于(预)加载任意其它数据的组合并使用垃圾回收来负责再次删除它。

详细解释


包加载

虚幻的加载代码的核心是它的包,它们和DLLs类似。一个包总会包含以下东西,按照这个顺序:

  • package file summary (包文件概要)
  • name table(名称表格)
  • import table(导入表格)
  • export table(导出表格)

包文件概要包含了包中存储的各种表格的某些基本信息以及 偏移/大小。名称表格包含了所有的序列化的名称。导出表格包含了序列化到包中的所有对象,而导入表格中包含了序列化到包中的对象的所有的直接依赖。

在导出表格的每个导出包含了到它的数据所在文件的偏移量。导出数据按照它们可以进行线性地创建/加载 的顺序被保存到包中。这意味这一个对象的类或外部容器总是在对象本身之前进行排序。

直接处理包的C++类是 ULinker的子类:ULinkerLoad和ULinkerSave。ULinkerLoad是把UObject捆绑到它从中载入的包上的东西。有几种UObjects,它们将永远都不会有连接器和它们相关联,比如,固有的类和高层次的包。对于这些UOjects来说,GetLinker()将会返回NULL。但这不是使得对象和它们的连接器分离的唯一时刻。使用同样的文件名保存包将会导致它和它的连接器分离,从而使得保存的代码可以确实地覆盖文件,重命名文件也会产生同样的效果。另一种使得加载器重置的情况是当把一个包加载到一个不同的外部容器时。在游戏机平台上,为了节约内存,连接器不会一直存在。

一般,当加载一个具有相关连接器的对象时将会导致加载操作返回已经在内存中的对象,而加载一个没有相关连接器的对象将会导致物体就地被从磁盘上替换掉。在实际的操作中代码是更加地复杂的,因为编辑器需要支持手动地重新加载包来取消改变,但是如果一个包在已经保存后间接地被引用时则不能彻底清除改变,因此将没有和它相关联的连接器。脚本编译也在这里添加了另一层复杂度。在游戏机平台上,引擎将总是首先尝试在内存中找到对象,并且当遇到通过StaticLoadObject加载单独对象的情况,将不能从磁盘中加载它,因为从DVD上加载时的块操作在这里不适用。

加载一个单独的对象和通过包加载对象所遵循的代码路径稍有不同,并且在本文档中没有这些内容进行讲解。以下列表描述了当已经加载整个包时所发生的操作。

  • UObject::LoadPackage 调用 BeginLoad
  • 调用UObject::GetPackageLinker ,它用于
    • 检查包是否有连接器。
    • 潜在地解决处理那些不能完全符合要求的文件名。
    • 使用“真实的”文件名称创建一个ULinkerLoad对象。
  • 返回的ULinkerLoad用于按照顺序“加载所有对象”/“创建所有输出”。
  • 调用EndLoad,它
    • 在真实的序列化和PostLoad调用函数所在的循环路径中。
    • 从导入表格中分离缓冲的导入物体,从而避免悬挂指针。
    • 从导出表格中分离缓冲的导出物体,从而避免悬挂指针。

我们将进一步详细说明当创建了一个ULinkerLoad时需要进行哪些操作:

  • 读取包文件概要
  • 读取名称表格
  • 读取导入表格
  • 读取导出表格
  • 在编辑器中,把现有对象潜在地和导出表格相挂钩。
  • 验证所有的导入(和它们各自的源连接中的导出索引相匹配)。

可以通过指定 LOAD_NoVerify 来延迟验证所有的导出,从而可以加速创建连接对象,尽管这也意味着对错误的处理将没有那么好。

异步的包加载

引擎支持以异步的形式来加载包。更加准确地说,引擎支持支持以异步的方式加载传入连接器中的所有对象。这个功能可以在UObject::PreloadLinkerAsync中找到,并且它做了以下事情:

  • 通过和在相应连接器上的ULinkerLoad::CreateExport相映射的ULinkerLoad::CreateImport创建所有导入。
  • 创建所有导出对象,并序列化它们的数据(一个接一个地)
  • 确保已经对所有的输出进行了PreLoad(实际的序列化)操作。
  • 对所有对象执行PostLoad操作。

这些操作通过几帧来展开,有时间限制。只有上一步已经完成,才能执行下一步。

这个过程是在一个循环中完成的,因为PostLoad可能会导致 构建/加载 新的对象。一旦产生了更多的工作,RF_AsyncLoading将会以异步的方式从已经被 加载/创建 的所有对象中被删除。然后分离导入对象和强制导出数据 (就像在EndLoad中那样),并调用调用完成回调函数。

在异步加载过程中创建的对象将被标记为RF_AsyncLoading,因为直到加载完成之前,对于引擎的其它部分来说,它们处于隐藏状态。异步加载代码不能用于现有对象的重新加载,如果您使用了这种方式,将会出现断言。

Seek-free(免搜索) 方面

在进行seek free加载时,需要确保所有的导入都已经加载,以便导入创建阶段不会导致任何的磁盘活动。加载包所需要的所有物体都可以在导出表中找到,即使它们可能不是包的一部分。这通过称为RF_ForceTagExp的对象标志来完成,该标志在保存时将强制把存在于导入表中的对象序列化到包中,然后它们将存在于导出表中。这些对象会被进行特殊的标记,所以当创建导出时它们也会被创建,就好像它们是从它们的原始包中被加载的一样,这意味着它们的"outer most(最外层容器)"不是加载的包。

在为“强制导出”创建导出前,引擎将会检查对象是否已经存在于内存中并使它保持一致。这允许在多个关卡中的几个地图共享仅需加载一次的相同内容。

关于异步加载的一个旁注是,当异步的后台加载正在发生时正常的加载则不能发生,这也是为什么当有非异步加载请求时,在异步加载完成之前任何后台活动都被阻塞的原因。同样对于垃圾回收也是一样的,在异步加载的过程中不能进行垃圾回收,因此将会出现堵塞,直到那个加载完成后才会执行垃圾回收操作。当有任何不终止加载时的阻塞请求时,自动计时的垃圾回收代码将会避免调用垃圾回收器。

Seek free加载依赖于那些在创建导入解析依赖对象时的那些不能保证被加载到了要加载的包中来避免可能的搜索的重复的依赖对象。大部分的搜索都是由于小的帮助对象导致的,比如UMaterialExpression,它实质上没有负荷,并且复制它的性能消耗非常便宜。另一方面,从占用内存大小的角度来看,静态网格物体和声音数据的复制可能是一般最昂贵的复制数据。

贴图通常不会复制它们的 负载/大块 数据复制,但是它把UObject数据和最低的miplevels放置到seek free包中,并且通过贴图动态载入代码是其它的东西进行动态载入。

可以通过创建具有共享内容的包并首先加载这些包来降低复制的次数。这一般是个手动的过程,在 战争机器中,除了多玩家地图的音频外,我们并没有太多地使用这个功能。

网络

网络代码依赖于称为包地图的结构,它用于通过索引来使对象引用之间进行通信。包地图代码使用对象连接器索引和高层次的包名称来把对象转换为索引,及再反过来把索引转换为对象。通过seek free加载代码加载的对象没有和它们相关联的连接器,所以代码需要在seek free包中存储原始的连接索引,并使用它来构建包地图。

初始加载

初始加载需要更多的信息。在烘焙步骤过程中:

  • 完全加在每个包含native类的脚本包。
  • 通过强制导出把它引用的所有内容放到它的内部。

代码在某种程度上这样来执行,在混合 脚本/内容 包的所有正常导出后可以存储这些强制导出。这允许从一个单独的内存块中为正常的导出加载所有数据,不必处理不能解决的交叉引用。

CreateExport将会在所有的脚本包上导出被调用,这将会创建并序列化类,同时也会创建内容但不会序列化内容。然后,一旦创建了所有的类后,便可以释放脚本块的内存,并且创建内容的导出,可以通过使用滑动窗口像正常的seek free包那样来在它上面调用序列化函数,从而数据的异步预取。

所有这些是在用于确保所有native类都被加载的必需代码之前进行的。这个操作可以确保被native类引用的所有内容都以seek free的方式进行加载,并且在这个阶段应该不会有任何磁盘访问。一个重要的注意事项是,所有的native类在引擎启动时将总是被加载,这意味着内容引用(比如默认属性或直接代码引用的内容)也将会被加载。Native类将不会被垃圾回收,所以任何有native类引用的内容将总是存在。所以,如果内容引用不是native的,那么确保不要在native类中引用那些不总是需要的内容是很重要的。这种情况的较好的解决方案是,设计一个那个native类的子类,该子类存在于另一个脚本包中,然后在子类中引用内容。

在最初加载中,所有的包含native类的脚本包都会被完全地加载。脚本将放在那些不包含native类的包中,并且内容引用将会被放在使用它们的地图文件中。有很多方法来降低复制的次数,比如,如果内容一定会在给定世界中的所有地图上加载,那么只要把内容移动到世界的永久性关卡中就可以了或者把它们放到总是要加载的包中。

游戏机平台烘焙

关于烘焙游戏机平台数据的情况,从强制导出中分离大块数据要求正在烘焙的正常包必须在烘焙需要它们的地图之前进行,并且引擎也会跟踪在这些包中的大块数据的偏移、大小以及其它的各种信息,所以它可以正确地修补序列化到seek free关卡包中的大块数据。这也要求当一个包改变时,所有依赖这个包的地图都要进行重新烘焙。烘焙器通过允许在命令行中指出一系列的地图名称来达到这个目的,并且它将执行所有需要的工作来仅烘焙那些需要烘焙的内容,并确保可以自动地处理所有依赖。

请参照内容烘焙包获得更多信息。

其它注意事项

加载标志的观念有几个和它们的继承传递相关的问题,并且将预定 删除/整理 这些标志。最主要的一个问题是,用于加载包内部的每个对象的加载标志是那个最初创建连接时所使用的标志,比如,当序列化一个包并且该包具有到先前提到的包的引用时将会隐含地出现这种情况。

通过提高UObject::Serialize、衍生的版本以及各种<<操作符的性能,可以降低最初加载以及每帧的工作量。引擎支持块序列化的概念,它依赖于要进行序列化的每个单独的struct(结构体)成员,它们按照在内存中的声明顺序进行序列化,由于排列也要进行序列化,所以要被序列化的数据直接映射到内存中的布局意味着潜在的差距,并且不同的字节顺序并不是问题。这依赖于保存的代码来执行任何可能的字节顺序转换,同时处理不同平台的struct(结构体)成员排列顺序可能不同的事实。在块序列化中的所有东西是一个非常脆弱的构造,但是对于简单的数据类型(比如颜色数组或向量)来说它可以工作的很好。目前引擎在几个地方使用它,并且理想状态下,我们将会扩展TTypeInfo的概念,从而允许natively(本地)序列化的数组在简单的情况下自动地使用块序列化。

另一个有效的优化是目前通过SerializedTaggedProperties进行序列化的一般数据类型的处理的特殊情况,比如FMatrix使用脚本序列化的情况。'immutablewhencooked'属性标志可以在很多情况下使用来自动执行,不需要数据类型的任何自定义的序列化代码。

贴图动态载入

对于静态对象,贴图动态载入代码查看那个对象上应用的边界框的屏幕空间尺寸,考虑UV缩放因素,并把游戏机平台上的磁盘上的压缩的贴图数据动态在入到显卡内存中。

动态对象基于可见性进行贴图的动态载入,并且有各种的覆盖来轻质动态载入对象的miplevels,从而避免贴图弹出。目前,我们在骨架网格物体上扩展贴图动态载入代码来更好地适应像虚幻竞技场2007这样的具有很多独特地图角色的快速游戏。

音频动态载入

现在还没有办法来动态载入音频内容。但是,实现了一个烘焙解决方案,可以获得更好的对话框多样性,这可能会满足您的要求。