WorldPartition中的LevelStreaming
简述
无论在Editor还是Game模式,WorldPartition都是以StreamingLevel的方式加载Cell。不过FStreamingLevelsToConsider中持有的是UWorldPartitionLevelStreamingDynamic类型的的StreamingLevel。
WorldPartitionStreamingSource
StreamingSource可以理解为一个视角数据,指定要加载哪个位置的Cell,例如我可以在远处放一个带StreamingSourceProvider的Actor, 然后将这个Actor设成Aways Load:

左上角绿色是玩家所在位置,右下角是放置的StreamingSource, 可以看到它周围的Cell也被加载了。
//WorldPartitionStreamingSource.h
struct ENGINE_API FWorldPartitionStreamingSource
{
FVector Location;
FRotator Rotation;
EStreamingSourceTargetState TargetState;
float Velocity;
//可认为是视线的形状,默认是个球
TArray<FStreamingSourceShape> Shapes;
}
它由一个Provider来提供:
//WorldPartitionStreamingSource.h
struct ENGINE_API IWorldPartitionStreamingSourceProvider
{
virtual bool GetStreamingSource(FWorldPartitionStreamingSource& StreamingSource) const = 0;
};
例如一个PlayerController就可以提供一个StreamingSource:
//PlayerController.h
class ENGINE_API APlayerController : public AController, public IWorldPartitionStreamingSourceProvider
{
}
//PlayerController.cpp
bool APlayerController::GetStreamingSource(FWorldPartitionStreamingSource& OutStreamingSource) const
{
//用PlayerViewPoint的数据来填充StreamingSource的数据,代表玩家视角。
}
每个Provider需要注册到WorldPartitionSubsystem中才能使用:
//WorldPartitionSubsystem.h
class ENGINE_API UWorldPartitionSubsystem : public UTickableWorldSubsystem
{
void RegisterStreamingSourceProvider(IWorldPartitionStreamingSourceProvider* StreamingSource);
bool UnregisterStreamingSourceProvider(IWorldPartitionStreamingSourceProvider* StreamingSource);
}
在每帧的UWorldPartitionStreamingPolicy::UpdateStreamingState中,会更新当前系统中的StreamingSource, 大致逻辑如下:
//WorldPartitionStreamingPolicy.cpp
void UWorldPartitionStreamingPolicy::UpdateStreamingSources()
{
...
for (IWorldPartitionStreamingSourceProvider* StreamingSourceProvider : WorldPartitionSubsystem->GetStreamingSourceProviders())
{
FWorldPartitionStreamingSource StreamingSource;
if (StreamingSourceProvider->GetStreamingSource(StreamingSource))
{
// Transform to Local
StreamingSource.Location = WorldToLocal.TransformPosition(StreamingSource.Location);
StreamingSource.Rotation = WorldToLocal.TransformRotation(StreamingSource.Rotation.Quaternion()).Rotator();
StreamingSources.Add(StreamingSource);
}
}
}
WorldPartitionStreamingPolicy
哪些Cell需要加载,哪些需要卸载也是每帧需要检测的:
UWorldPartitionStreamingPolicy::UpdateStreamingState()
UWorldPartition::UpdateStreamingState()
UWorldPartitionSubsystem::UpdateStreamingState()
UWorld::InternalUpdateStreamingState()
UWorld::Tick(ELevelTick TickType, float DeltaSeconds)
可以看到,最终逻辑是由UWorldPartitionStreamingPolicy来实现的。UE中Policy的一个常用用法就是把Policy的Class写入配置,用户自定义Policy。但WoldPartition中貌似没有实现这个功能。这个Policy创建堆栈:
NewObject<UWorldPartitionStreamingPolicy>(...)
UWorldPartition::GenerateStreaming(TArray<FString,TSizedDefaultAllocator<32>> * OutPackagesToGenerate)
UWorldPartition::OnBeginPlay()
UWorldPartition::OnPreBeginPIE(bool bStartSimulate)
相关逻辑:
//WorldPartitionStreamingGeneration.cpp
bool UWorldPartition::GenerateStreaming(TArray<FString>* OutPackagesToGenerate)
{
...
StreamingPolicy = NewObject<UWorldPartitionStreamingPolicy>(
const_cast<UWorldPartition*>(this),
WorldPartitionStreamingPolicyClass.Get(), //这个Class在构造函数中写死了。
NAME_None, bIsPIE ? RF_Transient : RF_NoFlags);
...
}
获取当前可见的Cell
//WorldPartitionRuntimeSpatialHash.cpp
void UWorldPartitionRuntimeSpatialHash::ForEachStreamingCellsSources(StreamingSource, Func) const
{
//如果没有source,获取aways load的cell
//如果有source,获取souce能看到的cell: FSpatialHashStreamingGrid::GetCells
}
获取可见Cell的函数是:
//WorldPartitionRuntimeSpatialHash.cpp
void FSpatialHashStreamingGrid::GetCells(...)
{
//根据loadingRand和Source的Shape计算出一个可见区域
const float GridLoadingRange = GetLoadingRange();
for (const FWorldPartitionStreamingSource& Source : Sources)
{
Source.ForEachShape(GridLoadingRange, GridName, HLODLayer, /*bProjectIn2D*/ true, [&](const FSphericalSector& Shape){
//针对每个可见区域和Cell做交集
//将算出来的结果放到OutActivateCells和OutLoadCells中
});
}
//加载AlwaysLoad Cell
GetAlwaysLoadedCells(...);
}
其实就是拿可见区域跟Grid的每个层级中的Cell做交集:
//RuntimeSpatialHashGridHelper.cpp
int32 FSquare2DGridHelper::ForEachIntersectingCells(const FBox& InBox, ...) const
{
//遍历Grid的各个层级
for (int32 Level = InStartLevel; Level < Levels.Num(); Level++)
{
//找可见Cell
}
//这里对于每一个Level,都会返回一个Cell, 但如果这个Cell没有任何Actor,是不用做处理的
}
求交逻辑:
//RuntimeSpatialHashGridHelper.h
//InBox代表StreamingSource
int32 ForEachIntersectingCellsBreakable(const FBox& InBox, TFunctionRef<bool(const FGridCellCoord2&)> InOperation) const
{
//将StreamSource压扁到2D
const FBox2D Bounds2D(FVector2D(InBox.Min), FVector2D(InBox.Max));
//获取min点和max点所在的Cell坐标
if (GetCellCoords(Bounds2D, MinCellCoords, MaxCellCoords))
{
//遍历min点和max点所有的Cell, 回调
}
}
找到当前帧可见的Cell之后,跟上一帧的Cell信息对比,得到需要改变状态的Cell:
//WorldPartitionStreamingPolicy.cpp
void UWorldPartitionStreamingPolicy::UpdateStreamingState()
{
...
//diff出需要变为Activate的Cell
TArray<const UWorldPartitionRuntimeCell*> ToActivateCells;
{
TSet<const UWorldPartitionRuntimeCell*> ToActivateCellsUnsorted = FrameActivateCells.Difference(ActivatedCells);
ProcessCellsToActivate(ToActivateCellsUnsorted);
SortStreamingCells(StreamingSources, ToActivateCellsUnsorted, ToActivateCells);
}
...
//diff出需要unload的的Cell
TArray<const UWorldPartitionRuntimeCell*> ToUnloadCells;
{
TSet<const UWorldPartitionRuntimeCell*> ToUnloadCellsUnsorted = ActivatedCells.Union(LoadedCells).Difference(FrameActivateCells.Union(FrameLoadCells));
ProcessCellsToUnload(ToUnloadCellsUnsorted);
ToUnloadCells = ToUnloadCellsUnsorted.Array();
}
...
}
然后通过设置状态的方式把他们加入到World的StreamingLevelsToConsider中:
//WorldPartitionLevelStreamingDynamic.cpp
void UWorldPartitionLevelStreamingDynamic::Unload()
{
...
SetShouldBeLoaded(false);
}
void UWorldPartitionLevelStreamingDynamic::Activate()
{
...
PlayWorld->AddUniqueStreamingLevel(this);
}
关联Level
Editor模式下
跟WorldComposition一样, 加载Cell也是调用RequestLevel, 但在Editor模式下,并没有真正的关卡文件,所有Actor都是通过OFPA加载的。所以就需要创建一个在内存中UPackage以及ULevel来达到操作的统一:
CreatePackage(const wchar_t * PackageName) //UObjectGlobals.cpp
FWorldPartitionLevelHelper::CreateEmptyLevelForRuntimeCell(...)
UWorldPartitionLevelStreamingDynamic::CreateRuntimeLevel()
UWorldPartitionLevelStreamingDynamic::RequestLevel(...)
ULevelStreaming::UpdateStreamingState(bool & bOutUpdateAgain, bool & bOutRedetermineTarget)
UWorld::UpdateLevelStreaming()
具体逻辑:
//WorldPartitionLevelHelper.cpp
ULevel* FWorldPartitionLevelHelper::CreateEmptyLevelForRuntimeCell(...)
{
// 包名格式为: /Memory/UEDPIE_0_MainGrid_L1_X0_Y-1_DL0
CellPackage = CreatePackage(*PackageName);
//依据包名创建Level
UWorld* NewWorld = UWorld::CreateWorld(InWorld->WorldType, false, WorldName, CellPackage,...);
ULevel* NewLevel = NewWorld->PersistentLevel
//关联Level和Cell
NewLevel->WorldPartitionRuntimeCell = Cell;
}
创建Level之后,需要将WorldPartition中保存的Actor加载到Level中来:
//WorldPartitionLevelHelper.cpp
//InActorPackages就是Actor对应的External Package
bool FWorldPartitionLevelHelper::LoadActors(InOwningWorld, InDestLevel, InActorPackages, ...)
{
//加载ExternalPackage, LoadPackageAsync 或 LoadPackage
//获取里面的Actor信息,然后加入的InDestLevel->Actors中
}
Runtime模式下
对于类UWorldPartitionLevelStreamingDynamic, 它其实是ULevelStreaming:
//WorldPartitionLevelStreamingDynamic.h
class ENGINE_API UWorldPartitionLevelStreamingDynamic : public ULevelStreamingDynamic
{
#if WITH_EDITOR
// Override ULevelStreaming
virtual bool RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy) override;
#endif
}
它在Editor模式下重载了RequestLevel, 但在Runtime模式下,实际调用的是:
//LevelStreaming.cpp
bool ULevelStreaming::RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy)
{
...
}
这就和WorldComposition一样了。