【UE】大世界子关卡StreamingLevel加载流程源码浅析-虚幻4
受限于硬件,当项目需要制作大世界的时候,整张大地图无法也没必要全部加载进内存。和所有支持大世界的引擎一样,UE采取了分块加载的方式:除了一个持久关卡(PersistentLevel)的加载以外,采用的都是运行时动态加载的方式,我们称这些关卡为子关卡或者流关卡(StreamingLevel)。当玩家到达子关卡边界的时候它们才开始加载。
受限于硬件,当项目需要制作大世界的时候,整张大地图无法也没必要全部加载进内存。和所有支持大世界的引擎一样,UE采取了分块加载的方式:除了一个持久关卡(PersistentLevel)的加载以外,采用的都是运行时动态加载的方式,我们称这些关卡为子关卡或者流关卡(StreamingLevel)。当玩家到达子关卡边界的时候它们才开始加载。
本文基于UE4.27,简述流关卡加载的流程和时机。除非解释原理必要,不会涉及如何使用。具体使用方式可以参考:YakSue的《学习UE4的WorldComposition基础》
虚幻4中的子关卡编辑
先来直观体验一下子关卡的编辑界面
Window-》Level打开“关卡”选项卡
我这里新建了三个关卡作为子关卡。可以看到除了默认的level作为持久关卡以外,其它的关卡都属于子关卡。游戏刚开始不会加载到内存,而是根据玩家位置和关卡距离,在运行时进行动态加载
单击调出世界场景构成。
世界场景构成中可以看到一个默认layer:“Uncategorized”,这里我新建了一个layer叫“我的图层”
简单解释一下layer的概念:可以看到在编辑器中编辑子关卡的时候,每个子关卡都属于一个layer,layer可以设置流送距离,这个距离是判断关卡可见性的一个重要属性,我们后文会提到
如果想让关卡距离玩家近一些才加载,就新建一个layer,把目标关卡放入新layer并调小新layer的流送距离即可
流关卡加载涉及到的类
一个大世界(World)由多个关卡(Level)组成,其中与流关卡加载有关的关键类及其属性如下:
ULevelStreaming:存放与流关卡加载相关的标记和方法,如当前状态(加载中、已加载、加载但不可见、加载且可见…)、目标状态等。
UWorld:StreamingLevelsToConsider存放当前待加载的流关卡,StreamingLevels存放已经加载的流关卡。属于运行时动态更新的两个容器
UWorldComposition:加载流关卡的主要负责类,其中Tiles存放其所属World的子关卡Summary信息,是流关卡加载的最小单位;TilesStreaming存放其当前状态。两个TArray通过下标一一对应。这两个数据结构基本只在开始的时候初始化一次,存储所属World下所有的流关卡信息。
UWorldComposition的Tiles可以看作是一个TArray<FWorldCompisitionTile>
,其关键属性如下:
// WorldComposition.h
// Helper structure which holds information about level package which participates in world composition
struct FWorldCompositionTile
{
FName PackageName;
FWorldTileInfo Info;
}
而FWorldTileInfo包括这个Tile的位置(Position)、绝对位置(AbsolutePosition)、边界(Bounds)和ZOrder等信息
加载流程
加载流程分为三个部分:
- 收集关卡资源
- 根据玩家位置标记流关卡状态
- 执行关卡的加载or卸载
收集流关卡资源
在UWorldComposition构造即将完成的时候,会调用UWorldComposition::Rescan
进行流关卡资源的收集。这个函数会查找当前World的根目录,将其中流关卡的Summary信息反序列化到内存。
注意这里只是反序列化了各个关卡的一点必要信息如关卡位置,给后续判断关卡什么时候应该加载用。并没有把整个流关卡都反序列化进来。
收集完信息到Tiles这个结构体以后,调用UWorldComposition::PopulateStreamingLevels
给每个流关卡都赋上初始状态。
这一步的结果就是完成了流关卡信息的收集,具体到结构体就是Tiles和TilesStreaming的初始化。
这一步骤基本只在构造即将结束的时候执行一次。接下来的两个步骤是在游戏进行中反复执行的。
根据玩家位置计算Tiles的可见性
UWorld::Tick()
会调用到UWorldComposition::UpdateStreamingState
这里根据一些必须加载的位置如玩家当前位置、PersistentLevel的位置计算应该加载、卸载的关卡。
这一步也只是做了关卡状态的计算,标记了关卡的TargetState,并将待加载的关卡放入UWorld的StreamingLevelsToConsider容器中。并没有真正加载关卡
关卡可见性计算方式:
源码注释中写道:
Check if tile bounding box intersects with a sphere with origin at provided location and with radius equal to tile layer distance settings
即判断 关卡(Tile) 的bounding box与指定位置为圆心的球体是否相交,这里球体的半径是这个 关卡(Tile) 所在layer设置好的流送距离。
关键部分源码如下:
void UWorldComposition::GetDistanceVisibleLevels(
const FVector* InLocations,
int32 NumLocations,
TArray<FDistanceVisibleLevel>& OutVisibleLevels,
TArray<FDistanceVisibleLevel>& OutHiddenLevels) const
{
……
for (int32 TileIdx = 0; TileIdx < Tiles.Num(); TileIdx++)
{
……
for (int32 LocationIdx = 0; LocationIdx < NumLocations; ++LocationIdx)
{
FSphere QuerySphere(InLocations[LocationIdx], TileStreamingDistance);
if (FMath::SphereAABBIntersection(QuerySphere, LevelBounds))
{
VisibleLevel.LODIndex = LODIdx;
bIsVisible = true;
break;
}
}
}
}
函数传入一些关键点位置InLocations
,如玩家位置。
FSphere QuerySphere(InLocations[LocationIdx], TileStreamingDistance)
:以关键点InLocations[LocationIdx]
为球心、TileStreamingDistance
为半径构造一个球体,判断这个球与关卡边界是否相交。如果相交则认为该关卡距离玩家已经很近了,需要载入内存中。
执行流关卡加载
终于到了真正加载的地方了。客户端绘制到屏幕的时候 根据上一步收集来的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。如果要做屏蔽操作也是服务器和客户端都需要执行的。
否则会出现服务器屏蔽了地表但客户端仍会显示,而由于碰撞的裁决在服务器,这块地表会没有碰撞:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)