受限于硬件,当项目需要制作大世界的时候,整张大地图无法也没必要全部加载进内存。和所有支持大世界的引擎一样,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等信息

加载流程

加载流程分为三个部分:

  1. 收集关卡资源
  2. 根据玩家位置标记流关卡状态
  3. 执行关卡的加载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。如果要做屏蔽操作也是服务器和客户端都需要执行的。

否则会出现服务器屏蔽了地表但客户端仍会显示,而由于碰撞的裁决在服务器,这块地表会没有碰撞:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐