【UE5】World Partition
伯特兰·罗素说他人生由三大激情支配着:对爱的渴望、对知识的探求、对人类痛苦的怜悯
早两天,Epic发布了UE5 Early Access,其中提到的一个重要的feature是World Partition,那么这个系统是做什么的,要如何使用,以及跟UE4的World Composition有什么区别呢,下面我们一起来看一下。
上面几张图是从Early Access的官方宣传视频中截取的主要包含了几个主要的特征:
- Level Managing
- Actor Editing
- Runtime Level Streaming
- Data Layers
下面我们分别来对这些内容进行展开介绍。
1. Level Managing
如上图所示,右侧小地图被划分成了一个个Cell。
在UE4中,地图是美术同学手动划分成一个个的Level(Level分为Persistent Level以及Streaming Level两类,前者是常驻的,可以理解为地图的初始Level,后者则是在运行时根据需要动态加载卸载的),之后通过World Composition来完成这些Level的管理,不过这种做法有比较多的不方便的地方,比如UE4中LevelBound会考虑天空盒的影响,使用起来会需要手动对其进行调整,比较繁琐。
在UE5中,地图不再需要划分成单个的Level进行编辑,也不需要手动对每个Level的位置进行调整,而是统一在一张大的完整的地图中进行编辑,Unreal会自动按照一定的size将之分割成一个个规整的Cell,这里的Cell就相当于此前UE4中的Level。
相对于UE4,UE5的地图编辑方式省去了美术同学手动分割分开存储的时间消耗,同时也方便了场景的编辑与管理,避免了场景支离破碎的开发方式,且由于Cell是规整的,因此在运行时进行Cell Streaming的时候就可以按照一套统一的规则算法进行处理,而不需要开发人员为每个Level设定对应的加卸载规则。此外,UE5的处理方式可以覆盖到地形的同步Streaming,而UE4中如果希望进行地形的Streaming,则需要将地形拆分成一个个的地块,分别存入到Streaming Level中。
1.1 World Partition 开关
要想启用这个功能,需要在设置界面打开开关,一些配置也是需要在这个地方进行调整:
实际测试发现,即使打开上述开关,在World Partition窗口中依然没有出现如宣传视频中所展示的小地图划分,这是为什么呢?从代码中发现,这个Partition是发生在地图创建的时候,而我们这里测试的地图是已经创建好了,之后再打开Partition功能,这就导致Partition在创建的时候没有生效,这种设定显然是不够人性化的,那么有没有办法对已经创建好的map启用partition呢?
答案是可以的,只需要重新创建一个空level,之后再打开此前创建好的level,就会弹窗提示是否需要转换成partition map,此时勾选是即可。
1.2 World Partition Cell尺寸设置
通过勾选Show Actors开关,可以在Partition小地图中显示各个Actor的Boundingbox边界。
这里的一个问题是,在运行时通过对配置中的AutoCellLoadingMaxWorldSize进行修正,貌似Cell并没有同步缩放,这是怎么回事呢?从代码中查看到AutoCellLoadingMaxWorldSize指的是当当整个地图的尺寸小于这个数值时,就将所有的Cell一次性加载,不再做Streaming。而控制Cell尺寸的实际上是UWorldPartitionEditorSpatialHash中的CellSize才是,这个数值是可以通过对配置文件进行修改来实现调整。但是找了半天没找到要怎么配置,虽然在每个地图所在的文件夹下都有一个配置文件:ProjDir\Content\Maps\LevelName.ini,其中有一个[/Script/Engine.WorldPartitionEditorSpatialHash] Section中间存储了CellSize的数值,也就是说,这个数值是针对地图进行设置的,需要修正的话,就要去对应的Level文件所在目录的ini文件中进行设定,但实际上我们测试发现,修改这个数值,在代码中断点调试得到的CellSize还是51200的初始值,这是为什么呢?
这是因为,在创建的时候,会使用Config(项目Config目录下的DefaultEngine.ini文件中的配置)来对相关属性进行设置,但是在创建完成后(UWorldPartition::CreateWorldPartition接口中)会调用SetDefaultValues来对这个数值进行重置(那这个数值要怎么开放给项目开发同学使用呢,总不能每个level都手动修改一遍代码吧?),而之前创建过World Partition并保存的地图,这一属性会直接序列化到umap文件中,在加载的时候就直接从文件中读取并赋予给相应的对象了,而不会再调用SetDefaultValues来进行重置,从这个设定来说,UWorldPartitionEditorSpatialHash的CellSize就固定为创建Level时代码中的DefaultValue了,而无法通过其他方式进行干预设定了,这种使用方式很明显是不人性的,这是不是UE的一个设计问题呢?
实际上,UE中的地图划分有两套,一套是编辑器使用,对应的是UWorldPartition中的EditorHash结构,另一套是运行时使用,对应的是UWorldPartition中的RuntimeHash结构。前面说的不支持手动调整CellSize的实际上指的是EditorHash结构,而RuntimeHash结构的CellSize是可以在编辑器中的WorldSettings界面进行调整的。
有时候可能会发现RuntimeHash中的参数不支持调整,没有找到具体原因,后面通过打开其他可以编辑RuntimeHash的Level再返回此前的Level就可以正常编辑了。
为什么UE要提供两套方案而不将两者合二为一呢?如何判断运行时的World Partition是符合需要的呢?
1.3 World Partition Runtime 配置
先来说下,UE为什么不将Editor Partition跟Runtime Partition统一起来,而是拆成两套,个人以为,首先运行时与编辑状态下的关注点不一样(比如Editor状态下就没有LoadingRange的考虑),所以相关的实现逻辑必然有所区别;其次,因为需求不同,编辑时的Cell粒度与运行时的Cell粒度也不必完全一致。不过不管怎么说,我认为都应该将编辑时的Cell Size参数开放出来供开发同学调整,而不应该代码写死。
运行时的World Partition与Loading状态可以通过Debug Layer来输出,打开命令为wp.Runtime.ToggleDrawRuntimeHash2D,下面给出的是不同Runtime Cell Size下的输出效果:
Cell Size 6400 Cell Size 3200 Cell Size 1600其中白色圆圈表示的是对应的Loading Range(这个数值在World Settings中也是可以调整的)。
为了处理运行时的Partition逻辑,UE新增了一个UWorldPartitionSubsystem,这个可以看成是UWorldPartition Manager,具有Tick & Draw函数,前者负责逻辑的驱动,后者负责Debug效果的输出。
Partition的数据存储在了UWorldPartitionRuntimeSpatialHash结构中,从代码可以看到,这个结构可以存储多套Partition Grid,每个Grid都可以看成是一个World Partition,具有自己的Grid Name(可以在World Settings设置)跟划分参数(Cell Size & LoadingRange),至于为什么会有多套Partition Grid,暂时没有头绪(推测可能是因为支持Sub Level,而每个Sub Level都有自己对应的World Partition导致吧?)。
每套Grid的数据存储在了FSpatialHashStreamingGrid结构中,由于Cell Grid是支持分层合并的(类似八叉树,不过运行时使用的貌似是四叉树),而每个Layer(最底层的是叶子Layer,再上一层则是四个叶子Cell合并成一个Parent Cell)都对应一个FSpatialHashStreamingGridLevel结构。
FSpatialHashStreamingGridLevel则包含了众多的FSpatialHashStreamingGridLayerCell,这个FSpatialHashStreamingGridLayerCell是稀疏存储的,即当Cell中不包含Actor,那么这个位置的Cell就不会被创建(跟编辑时一致)。
每个FSpatialHashStreamingGridLayerCell又包含了多个UWorldPartitionRuntimeSpatialHashCell结构,每个UWorldPartitionRuntimeSpatialHashCell结构对应一个具有不同的LayerID的HashCell,LayerID是通过多个Data Layer的LayerName排序后Hash计算得到的,也就是说,在同一个空间Cell中,根据Actor所对应的Data Layer的不同,会对应多个UWorldPartitionRuntimeSpatialHashCell,至于为什么这么设计,目前暂时没有头绪。
1.4 World Partition Actor归属
在UE5的演示中,自动划分的Cell看起来是将场景的内容按照范围进行了切割,那么就会令人产生一个疑问,对于坐落在两个Cell边缘的物件,UE会对其做什么样的处理呢?会将之沿着边界分割成两个Actor吗?通过测试以及简单的代码浏览,我们发现,并不是这样的,UE会为每个Cell维护一份Actor列表,在Load Cell时会将与之关联的Actor都加载出来,此外,对于一些特殊的需求,我们可能会期望即使在Cell卸载的情况下,部分Actor依然保持加载,UE也兼顾了这类需求,提供了一个AlwaysLoad的选项,当某个Actor的Boundingbox的Cell Level大于4时会自动触发这个开关,此外也可以通过Detail面板对物件的加载类型进行手动调整:
按照什么方式判断某个Actor是否归属于此Cell呢?Actor与Cell的关联是一一对应吗?Actor与Cell的绑定是在UWorldPartitionEditorSpatialHash::HashActor接口中完成的,通过分析发现,当需要新增Actor时,会将这个Actor的BoundingBox进行Cell覆盖测试,将与其相交的所有Cell取出来,并将此Actor塞入其中,也就是说,Actor是否属于某个Cell,是通过Actor的Boundingbox与Cell的Boundingbox是否相交来判断的,因此Actor与Cell是多对多的关系。
1.5 World Partition 刷新与Cell创建
此外,出于对使用效率的考虑,每当新增或者移除Actor的时候,并不会立马触发World Partition的更新,而是会在Save Level的时候进行一遍刷新,下面给出了刷新前后的Partition示意图。
刷新前 刷新后World Partition的Cell是按需创建的,对于没有物件的区域,是不会创建对应的Cell的,如下图所示,没有Actor的位置没有创建对应的Cell,因此通过鼠标框选会发现无法选中任何Cell:
1.6 World Partition Minimap
我们前面的截图中,World Partition的示意图中只给出了Actor的Boundingbox,而没有像宣传视频中的,给出了整个地图的MiniMap,这是为什么呢?
从代码中可以看到,UE是提供了MiniMap绘制功能的,MiniMap重新加载入口为SWorldPartitionEditorGridSpatialHash::ReloadMiniMap(初次打开时的更新接口为SWorldPartitionEditorGridSpatialHash::UpdateWorldMiniMapDetails,但这个接口传入的参数是不会创建MiniMap的),这个API是通过按钮触发的,MiniMap数据来自于FWorldPartitionMiniMapHelper::GetWorldPartitionMiniMap接口,而按钮打开则是由命令控制的,理论上只需要开启wp.MiniMap.ShowReloadButton 1即可看到Minimap按钮了,但实际上测试发现按钮并没有出现。
从UE官方的宣传视频看,按钮也是不存在的,但是小地图是可以正常绘制的,尝试将初始化时候传入的参数设置为true,允许绘制小地图,结果发现MiniMap依然未能显示,通过源码分析发现SWorldPartitionEditorGridSpatialHash::UpdateWorldMiniMapDetails接口中WorldMiniMap->MiniMapTexture为Null,而这个结构是通过FWorldPartitionMiniMapHelper::CaptureWorldMiniMapToTexture接口完成赋值的,从断点表现来看,确实没有被调用过。
从调用入口分析结果来看,支持MiniMap生成的唯一有用接口为SWorldPartitionEditorGridSpatialHash::ReloadMiniMap,而这个接口则是通过Reload按钮触发,而现在这个按钮是不可用状态,看来要想解决MiniMap显示问题,还需要从按钮入手。
经过多次尝试发现,将ReloadMiniButton按钮提到整行的最前面,就可以正常展示了(看起来是UE UI绘制的一个bug,Slot在调用了FillWidth接口后,后续的Slot如果使用AutoWidth就会存在显示问题。。。只是不知道这个是个例还是底层机制实现的一个通病,时间关系不做深究了,直接将Show Actors的FillWidth替换成AutoWidth):
在使用的过程中,还发现一个问题,那就是Partition面板上绘制的Actor的Bounds会存在异常,比如一个64作为CellSize的Partition,当一个Size为64的Cube的Location为0,0的时候,绘制的Bounds处于Grid中心,而将Location移动到32,理论上应该会有一条边与Cell的边缘重合,但实际上验证发现跟Location为0,0时的表现并无区别,这是什么原因呢?
Actor Bounds的绘制入口在SWorldPartitionEditorGrid2D::PaintActors,而绘制时传入数据的接口为UWorldPartitionEditorSpatialHash::ForEachIntersectingActor,数据来自于UWorldPartitionEditorSpatialHash的HashCells Map Value的Actors成员,经过验证发现这个数据来源的更新是实时的,即每次修改都会立马生效,那为什么Partition面板会表现错误呢,经过排查发现,一个Cube默认尺寸是100 x 100(即边长1m),此时缩放到64之后,正好对应一个Cell的大小,但是Location从0,0移动到32,32,相对于半个Cell的边长3200还有很大差距,因此并没有很明显的看出来是被移动了,将Location改动到3200,3200之后就正常了。
另外,在使用的过程中发现,当打开WorldPartition Window时,整个编辑器会变得很卡,不知道是什么原因导致,看看后面有没有办法优化一下。
2. Actor Editing
在对场景进行修正的时候,数据是以Actor作为基本的修改单位进行的,而非以Level为基本单位,这就使得这边允许多个美术同学对同一个场景进行修改,比如分别负责光照、场景布置以及其他细节修正,最终保存的结果不会引起冲突(需要了解下资源存放,看看最终的改动是否可以合并到一起。),具体可以参考后面One File Per Actor(OFPA)部分的介绍。
3. Runtime Streaming
出于性能考虑,通常在游戏运行的时候,我们不会将地图的所有数据都加载到内存里,而是根据需要只加载能够看到的数据,在看到的数据中,近景处会加载高清数据,而远景则会使用一些低精度的数据。
UE5在这个上面提供了一套针对Cell设置的Streaming策略,根据玩家的视野自动对Cell进行加载与卸载,Cell的Size以及加载范围都是可以通过配置进行灵活调整的,这样就可以满足不同项目的不同需要。
Cell Streaming的逻辑是由UWorldPartitionLevelStreamingPolicy(Parent Class -> WorldPartitionStreamingPolicy)控制的,调用入口为 UWorldPartition::UpdateStreamingState -> WorldPartitionStreamingPolicy::UpdateStreamingState,根据当前代码执行是在DS(UWorldPartitionRuntimeSpatialHash::GetAllStreamingCells)还是客户端(UWorldPartitionRuntimeSpatialHash::GetStreamingCells)中进行,其Streaming的策略有所不同,DS会在初始化时一次性将所有的Cell都加载并保持常驻,而客户端则会根据设定的规则(LoadingRange作为加载半径,不同Player的位置作为加载Center点,得到一个LoadingSphere,之后查询与这个Sphere相交的Cell)对Cell进行加载。
对Cell的加载是按照增量方式进行的,即会先计算出当前需要处于Activated & Loaded状态的Cell,之后与实际已经处于这两个状态的Cell进行求差,输出需要加载的Cell,而对两者进行反向求差(A - B -> B - A)就得到了需要卸载的Cell(UWorldPartitionStreamingPolicy::UpdateStreamingState)。
这里还有两个疑问,第一个是为什么会有Activated跟Loaded两个状态呢?两者的区别是什么?
第二个,在Cell加载完成后,内部的资源是否还有进一步的LOD Streaming策略,这部分策略是如何运转的?
先说第一个,UE提供了一个枚举类型EWorldPartitionRuntimeCellState,用于标识Cell当前的状态(Unloaded, Activated, Loaded),整个流程可以通过状态机来表述:
EWorldPartitionRuntimeCellState枚举的结果稍微有点粗糙,ULevelStreaming.ECurrentState提供了更为精细的Cell State,上图右侧示意图给出了两者之间的对应关系。
这里需要说明一下的是,出于性能考虑,每帧Activate/Loaded的Cell数目是有限的,这个数值可以通过UWorldPartitionLevelStreamingPolicy::GetMaxCellsToLoad接口获取到,而为了能够得到更好的表现,就需要优先加载那些具有更高优先级的Cell(比如更近的等),这就需要对希望Activate的Cells进行排序,这个逻辑通过UWorldPartitionRuntimeSpatialHash::SortStreamingCellsByImportance接口完成,整个实现逻辑在UWorldPartitionLevelStreamingPolicy::SetCellsStateToActivated/UWorldPartitionLevelStreamingPolicy::SetCellsStateToLoaded接口中可以找到。
再来回答前面的问题,Activated表示当前Cell处于加载完成并可见状态,而Loaded则只是表明当前Cell是Loading完成,但是并不可见。
再来看下Cell内部的LOD Streaming策略,主要包含以下几方面:
- HLOD Cell
后面会介绍,UE为World Partition提供了一套HLOD方案(不支持Landscape & Water),用于实现远距离场景的低成本绘制,HLOD Cell的Cell Size & Loading Range跟普通Cell是不一样的,可以单独进行设置。另外,World Partition中的HLOD也支持多个Level之间的切换,Level越高(物件越粗糙),优先级越高。
前面说过,普通Cell是按照相机位置+LoadingRange决定哪些Cell需要加载的,那么如果HLOD也是如此的话,在近景处不就会出现两套Assets了吗?理论上不会出现这种问题,仿照UE4的HLOD实现逻辑,只有那些Detail数据被卸载的远景区域才会打开HLOD显示,因此不会存在两者并存的情况,只是这边并没有检索到对应的实现逻辑,所以具体细节这里就不介绍了,后面遇到了再补上。
- Asset Streaming
这部分主要包括StaticMesh的Streaming与Texture等资源的Streaming,其基本实现逻辑跟UE4的类似,不过因为UE5使用的是Nanite,因此对于StaticMesh而言,其Streaming的实现将有所不同,从Nanite.FResources的成员变量以及其对应的注释来看,每个Cluster会有一个最粗糙版本的RootPage,其他更高精度的Page数据则是在需要的时候通过Streaming载入内存(粗略扫了一下,相关逻辑在如下接口中应该可以找到:FStreamingManager::AsyncUpdate -> FStreamingManager::RegisterStreamingPage)。
4. Data Layers
Data Layer是UE5提供的一种对资源进行分组管理的方式,比如可以将建筑放到一个Layer中,将植被放到另一个Layer中(或者将场景数据放到一个Layer,将游戏逻辑相关的数据放到另一个Layer等,通过这种方式,策划同学跟美术同学就可以在自己对应的Layer下进行开发,而不会互相干涉了),之后根据需要分别加载不同的Layer(比如视频中展示的,在同一个地图中分别制作两套场景,一套是黄色地表的荒野,另一套则是暗黑风格的城堡废墟,根据不同的需要在两套资源中进行切换,就可以得到更为丰富多样的游戏体验。),推测这套Data Layer机制应该是使用了类似UE4的Streaming Level的做法,通过将两个Level叠加到一起,在运行时进行加载与卸载,就能够得到同样的表现。
Data Layer的加载与卸载可以通过蓝图进行控制,UE官方文档(参考文献[4])中也说明了,实际上这个Data Layer就对应于UE4中的Layers System。
在Window菜单中,我们可以打开Data Layer面板:
上面用数字标出了各个要素,下面做下简要分析。
1表示的是Data Layer的可见性,从官方文档的描述来看,一个Actor可以同时绑定多个Data Layer,而只有所关联的所有Layer的可见性都是Hidden,这个Actor才会被Hidden
2指的是这个Layer的类型,Static还是Dynamic,Dynamic Layer的加载可以通过蓝图或者代码动态触发,这种类型的Layer会影响到运行时Actor的加载表现
3指的是Data Layer在编辑器下是否加载,当加载时,就会同时加载对应的Actors数据
4会展开Data Layer下的Actor数据,其中可以单独指定某个Actor的可见性
5指定了Data Layer初始加载时的可见性
6为Data Layer的HLOD属性,当勾选时表明此Layer需要生成对应的HLOD Actors,这个选项只在Dynamic Layer中生效。
7指定了Dynamic Layer在初始化时的加载状态
8则指定了当前的Layer是否是Dynamic Layer。
对Data Layer的一些额外操作可以通过右键方式调出,具体这里就不展开了:
Actor与Data Layer的绑定可以通过Detail面板来实现:
为了实现与UE4的兼容,UE5还提供了一套从Layer System向Data Layer的转换机制,这个转换过程是自动完成的(当发生World到World Partition的转换时,就会顺带完成这一步),只是转换后的Data Layer其Dynamically Loaded属性是关闭的。
5. HLOD
World Partition实现了场景的Cell划分,并增加了Cell的加载与卸载机制,那么对于一些远景大尺寸物件(比如很大的山脉等),如果直接卸载会使得画面失真,对于这种情况我们常见的做法是使用HLOD,那么在World Partition状态下,我们要如何集成HLOD呢?
UE是考虑过这个问题的,参考文献[3]中就针对这一点做过专门叙述,下面是HLOD关开时的表现对比:
那么在World Partition状态下,HLOD是如何使用的,具体表现与消耗又是怎么样的呢?
首先,从[3]中给出的信息描述来看,目前Landscapes以及Water Component是不支持HLOD的,其次对于其他的Component,World Partition的HLOD是通过HLOD Layer asset实现的,这些asset(注意,每个Level可以创建多个HLOD Layers)中包含了HLOD的相关配置数据:
HLOD Layer创建方式 HLOD Layer参数编辑World Partition的HLOD Actor结构为AWorldPartitionHLOD,从其中的数据判断,每个Cell中具有相同Data Layer ID的Actor会被放入到一个HLOD Actor中(根据FHLODActorDesc::Init接口中为HLOD Actor生成CellHash判断)。
5.1 相关参数
5.1.1 Layer Type
Layer Type主要有三种:
- Instancing,在当前Layer Type下的StaticMesh物件会被ISM(Instanced Static Mesh)Component替代,而每个ISM Component中的Mesh使用的则是最低级别的LOD数据,这种Layer对于那些Imposter物件(树木、植被等)会比较合适。
- Merged Mesh, 这种Layer下,Static Mesh物件会被Merge成一个Proxy Mesh
- Simplified Mesh, 这种Layer下的Static Mesh物件同样会被Merge成一个Proxy Mesh,不同的是,这里会对Mesh进行减面处理
对于需要Merge的HLOD Layer,UE还提供了一系列的Merge Settings与Proxy Settings:
具体细节就不展开了,需要了解的可以参考文献[3]。
5.1.2 Always Loaded选项
这个开关用于指定生成的HLOD Proxy(替代物)是否会常驻加载
5.1.3 Cell Size选项
当HLOD Layer不是常驻加载时,就绪一套按需加载卸载逻辑来处理HLOD Proxy物件,跟World Partition中正常的Cell加载卸载逻辑相似,只不过这里为HLOD单独提供了一套Cell Size参数,可以实现不同于正常Partition中物件的加载卸载表现。
5.1.4 Loading Range
这个参数跟Cell Size一样,也是区别于正常Runtime Partition下的Loading Range而增加的,与Cell Size共同作用于Cell Streaming逻辑。
5.1.5 Parent Layer
允许将多个HLOD Layer按照树状结构进行组织,只是不清楚在这种组织结构之下,各个HLOD Layer的加载卸载策略是否有一些新的限制或者变化。
5.1.6 HLODMaterial
Merged/Simplified Mesh在合并的时候会需要一个Base Material,后续生成的Mesh所使用的的材质就是这个Base Material的Instance,而这里的HLODMaterial就是指这个BaseMaterial
5.2 HLOD创建流程
5.2.1 添加Actors
- 在Actor的Detail面板下可以查看HLOD属性
- 每个Data Layer下有一个默认的HLOD Layer属性
- World Settings下也有一个默认的HLOD Layer属性
5.2.2 生成HLODs
HLODs是通过WorldPartitionHLODsBuilder命令行工具实现的,根据之前设定的一些参数,调用这个命令行工具后就能生成对应的HLOD Actors了,命令行工具的使用步骤给出如下:
- 打开命令行窗口(Windows下Win+R调出运行界面,输入cmd)
- 导航到UE4Editor.exe所在目录
- 通过exe+参数的方式运行对应的命令行,如下图所示:
- 根据需要添加下面几个命令行参数:
- -SetupHLODs,对HLOD Actors进行配置(比如Update/Create/Delete对应的Actor),但是不创建对应Geometry数据。
- -BuildHLODs,为所有的HLOD Actor的创建Geometry数据
- -DeleteHLODs,删除所有的HLOD Actor
5.3 查看HLODs
UE还提供了HLOD Layer的可视化功能,在编辑器状态下,只需要将View Mode更改Level of Detail Coloration,之后选择HLOD Coloration,就可以看到的HLOD Layer中的近景Actor是绿色,远景Actor是蓝色:
在运行时,通过命令wp.Runtime.ToggleDrawRuntimeHash2D可以查看到加载完成的HLOD Cells是绿色的:
6. One File Per Actor(OFPA)
最后在地图上传的时候,发现在Game目录下增加了一个叫做ExternalActors的新目录,从命名方式上来看,这个目录像是临时目录,打开查看其中的内容发现,是按照地图目录(即Level目录)结构存储的,众多的一个个以hash值标注的二进制文件,从文件数目推测,每个文件应该对应于Level中的一个Actor,尝试将整个文件夹删除,发现重新打开之前存储的地图后,Level中的所有数据就丢失了。因此,在上传的时候,还是需要将这个文件夹也添加到上传目录。
为什么UE5要增加这样一个目录呢,相对于UE4直接将所有的数据塞入到.umap文件中有什么好处呢(从数据尺寸上来看,.umap从617KB降低到了20KB,而新增的文件夹其尺寸为618KB,似乎没有明显增减),看起来似乎给编辑数据上传增加了一道额外手续?
从参考文献[5]中的描述可以看到,此前如果多个开发同学要对同一个Level进行编辑,就会触发冲突,这是因为所有数据都是存储在Level中的,而在UE5中,Level中的每个Actor都是分拆成单个的文件进行存储,相当于如果只是对某个Actor进行属性修改的话,其冲突将只停留在Actor File层面,如果多个同学分别对不同的Actor进行编辑,就不会触发冲突,而如果需要在Level下新增或者删除Actor的话,则只是在ExternalActors目录下增加或者移除一个目录,而不需要再对.umap文件进行修改,极大的降低了文件修改的冲突。
当然,如果希望某个Actor依然保存在.umap文件中,还是可以通过Detail面板进行手动设置的:
不过,实际测试中发现这个选项不支持修改:
这个属性的可编辑配置是通过FActorDetails::AddActorCategory接口进行控制的,而这个接口中对这个属性的设置使用了两个Enable控制,一个是FActorDetails::IsActorPackagingModeEditable,另一个则是!bIsPartitionedWorld,而后面这个目前直接是false,也就是因为开启了bIsPartitionedWorld,通过测试发现,这种两个Enable函数级联的结果是以后一个Enable函数为准(不知道这是UE的bug还是设计如此?从函数设置来看,UE的设计应该是两者求交集才是,想要修正这个,可以将bIsPartitionedWorld对Enable的设置提到外面来,用于控制Text&Comb的可边界状态),而在当前的模式下,这项属性默认是false,也就不支持对选项进行修改了。
如果觉得单个Actor设置太过繁琐,这里还支持对整个Level进行统一设置:
不过,如果某个Level中包含了多个Sub Level,这里的设置只会对当前的Level生效,对于Sub Level则需要针对每个Sub Level的World Settings进行修正,如果想要进行批次处理,UE也提供了对应的命令行工具。
有时也会发现这个选项被disabled了,经过试验发现,当Level创建完成并保存后,是可以修改的,如果关闭之后再重新打开,就不支持修改了,而在那之前,是支持在external(OFPA)与internal(传统Packaging方式)之间进行切换的,切换实现可以在FWorldSettingsDetails::OnUseExternalActorsChanged接口中找到。
关于这个文件夹,从代码中还拿到了额外的一些信息:
- 这个文件夹的数据是不参与Cook的(可以理解,毕竟只是Level中的Reference数据)
- 当资源被删除时,可以尝试从这个文件夹进行恢复(FFileTreeItem),从前面不参与Cook可以知道,这个文件夹中存储的肯定不是StaticMesh之类的数据,实际上也没必要,那么从这个文件夹中肯定是无法找回被删除的StaticMesh等数据的,所以这里的恢复感觉不可行,经过测试发现,将某个StaticMesh删除后,Level重新打开也只是保留一个空的Actor,其对应的StaticMesh也是为None的,这个表现跟UE4并无两样,那么这里的恢复是什么用法呢?
在OFPA模式下,Level中的Actor在文件夹中的对应文件名是经过编码的,无法直接通过版本管理软件区分具体哪些Actor被改动过,UE专门为这种情况开发了一个Source Control的工具,可以直观的知道Actor文件的修改状态:
不过这个工具目前只支持perforce。。。
参考文献
[1] A First Look at the World Partition System in UE5
[2] Unreal Engine 5 Early Access Release Notes
[3] World Partition - Hierarchical Level of Detail
[4] World Partition - Data Layers
[5] One File Per Actor