受限于硬件,当项目需要制作大世界的时候,整张大地图无法也没必要全部加载进内存。和所有支持大世界的引擎一样,UE采取了分块加载的方式:除了一个持久关卡(PersistentLevel)的加载以外,采用的都是运行时动态加载的方式,我们称这些关卡为子关卡或者流关卡(StreamingLevel)。当玩家到达子关卡边界的时候它们才开始加载。
本文基于UE4.27,简述流关卡加载的流程和时机。
文章目录
- 流关卡加载涉及到的类
- 加载流程
- 收集流关卡资源
- 根据玩家位置计算Tiles的可见性
- 执行流关卡加载
- 应用:屏蔽部分子关卡的加载
流关卡加载涉及到的类
一个大世界(World)由多个关卡(Level)组成,其中与流关卡加载有关的关键类及其属性如下:
ULevelStreaming:存放与流关卡加载相关的标记和方法,如当前状态(加载中、已加载、加载但不可见、加载且可见…)、目标状态等。
UWorldComposition:加载流关卡的主要负责类,其中Tiles存放其所属World的子关卡Summary信息,是流关卡加载的最小单位,TilesStreaming存放其当前状态。两个TArray通过下标一一对应。这两个数据结构基本只在开始的时候初始化一次,存储所属World下所有的流关卡信息。
UWorld:StreamingLevelsToConsider存放当前待加载的流关卡,StreamingLevels存放已经加载的流关卡。属于运行时动态更新的两个容器
加载流程
加载流程分为三个部分:
- 收集关卡资源
- 根据玩家位置标记流关卡状态
- 执行关卡的加载or卸载
收集流关卡资源
在UWorldComposition构造即将完成的时候,会调用UWorldComposition::Rescan
进行流关卡资源的收集。这个函数会查找当前World的根目录,将其中流关卡的Summary信息反序列化到内存。
注意这里只是反序列化了各个关卡的一点必要信息如关卡位置,给后续判断关卡什么时候应该加载用。并没有把整个流关卡都反序列化进来。
收集完信息到Tiles这个结构体以后,调用UWorldComposition::PopulateStreamingLevels
给每个流关卡都赋上初始状态。
这一步的结果就是完成了流关卡信息的收集,具体到结构体就是Tiles和TilesStreaming的初始化。
这一步骤基本只在构造即将结束的时候执行一次。接下来的两个步骤是在游戏进行中反复执行的。
根据玩家位置计算Tiles的可见性
UWorld::Tick()
会调用到UWorldComposition::UpdateStreamingState
这里根据一些必须加载的位置如玩家当前位置、PersistentLevel的位置计算应该加载、卸载的关卡。
这一步也只是做了关卡状态的计算,标记了关卡的TargetState,并将待加载的关卡放入UWorld的StreamingLevelsToConsider容器中。并没有真正加载关卡
执行流关卡加载
终于到了真正加载的地方了。客户端绘制到屏幕的时候 根据上一步收集来的StreamingLevelsToConsider进行关卡的载入内存和显示。
实际的加载在ULevelStreaming::RequestLevel
中进行:
bool ULevelStreaming::RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy)
{
... ...
const FName DesiredPackageName = bIsGameWorld ? GetLODPackageName() : GetWorldAssetPackageFName();
if (bAllowLevelLoadRequests)
{
const FName DesiredPackageNameToLoad = bIsGameWorld ? GetLODPackageNameToLoad() : PackageNameToLoad;
const FString PackageNameToLoadFrom = DesiredPackageNameToLoad != NAME_None ? DesiredPackageNameToLoad.ToString() : DesiredPackageName.ToString();
if (FPackageName::DoesPackageExist(PackageNameToLoadFrom))
{
... ...
LoadPackageAsync(DesiredPackageName.ToString(), nullptr, *PackageNameToLoadFrom, FLoadPackageAsyncDelegate::CreateUObject(this, &ULevelStreaming::AsyncLevelLoadComplete), PackageFlags, PIEInstanceID, GetPriority());
... ...
}
... ...
}
应用:屏蔽部分子关卡的加载
如果本地有子关卡资源,但运行时不希望加载,需要进行子关卡的屏蔽。在UWorldComposition::Rescan
收集的时候对指定关卡进行屏蔽即可。
注意由于子关卡的信息收集和加载是在Server和Client各自进行的,并不是通过网络同步从Server下发到Client。如果要做屏蔽操作也是服务器和客户端都需要执行的。
否则会出现服务器屏蔽了地表但客户端仍会显示,而由于碰撞的裁决在服务器,这块地表会没有碰撞: