NiceTry12138 5ec29a22c7 feat: 添加 LevelStreaming 加载流程 hace 6 meses
..
Image 5ec29a22c7 feat: 添加 LevelStreaming 加载流程 hace 6 meses
README.md 5ec29a22c7 feat: 添加 LevelStreaming 加载流程 hace 6 meses

README.md

LevelStreaming

UE 中 Level 是 Actor 的一种组织形式

一个 World 可以包含多个 Level,一个 Level 可以包含多个 Actor

为了创造更大的世界,资源加载和占用是巨大的,如果一开始就直接加载全部的场景,卡顿和资源消耗是不可接受的

LevelStreaming 就是用于动态加载和卸载游戏世界中的不同部分,可以保证在玩家移动时能动态加载和卸载 Level

加载 LevelStreaming

参数 作用
Level 期望加载的关卡
Location 关卡原点所在的世界坐标写
Rotation 关卡旋转
OptionalLevelNameOverride 如果存在同名关卡,则本次不加载
OptionalLevelStreamingClass 继承自 ULevelStreamingDynamic 类,如果配置加载地图时 UClass 选择配置的类,默认使用 ULevelStreamingDynamic

最后会执行 ULevelStreamingDynamic::LoadLevelInstance_Internal

// 收集信息
// 处理数据

// 存在名称 OptionalLevelNameOverride 同名 ULevelStreaming 则不加载
if (Params.World->GetStreamingLevels().ContainsByPredicate([&ModifiedLevelPackageName](ULevelStreaming* LS) { return LS && LS->GetWorldAssetPackageFName() == ModifiedLevelPackageName; }))
{
    return nullptr;
}

// 创建 ULevelStreamingDynamic
UClass* LevelStreamingClass = Params.OptionalLevelStreamingClass != nullptr ? Params.OptionalLevelStreamingClass.Get() : ULevelStreamingDynamic::StaticClass();
ULevelStreamingDynamic* StreamingLevel = NewObject<ULevelStreamingDynamic>(Params.World, LevelStreamingClass, NAME_None, RF_Transient, NULL);

// 设置 StreamingLevel 参数

Params.World->AddStreamingLevel(StreamingLevel);

UWorld::AddStreamingLevel 中对应操作就是添加到 StreamingLevelsStreamingLevelsToConsider 数组中

void UWorld::AddStreamingLevel(ULevelStreaming* StreamingLevelToAdd)
{
	if (StreamingLevelToAdd)
	{
        // 一些条件判断
        StreamingLevels.Add(StreamingLevelToAdd);
        FStreamingLevelPrivateAccessor::OnLevelAdded(StreamingLevelToAdd);
        if (FStreamingLevelPrivateAccessor::UpdateTargetState(StreamingLevelToAdd))
        {
            StreamingLevelsToConsider.Add(StreamingLevelToAdd);
        }
    }
}

UWorld::UpdateLevelStreaming

前面一顿操作也只是将 FStreamingLevelDynamic 添加到 UWorld 的容器中,并没有真正的加载到场景中

为了防止 Level 内容过多影响游戏运行帧率,UE 的解决方案还是经典 分帧

每帧 UWorld::UpdateLevelStreaming 都会遍历 StreamingLevelsToConsider 数组,从中取出待加载的 LevelStreaming,执行加载逻辑

  • 如果本次加载完毕,那么 bShouldContinueToConsider 值为 false,该 LevelStreaming 会从 StreamingLevelsToConsider 数组中删除
  • 如果没有加载完毕,根据 bUpdateAgain 值判断,是否需要跳出 while 循环,执行下一个 LevelStreaming 的加载操作

因为涉及删除操作,所以遍历需要后序遍历

一个 LevelStreaming 加载涉及多个阶段,每个阶段结束都会去判断一下所用时间是否超过限制时间,如果超过那么 bUpdateAgain 为 false,等待下一帧再执行后续加载阶段

for (int32 Index = StreamingLevelsToConsider.GetStreamingLevels().Num() - 1; Index >= 0; --Index)
{
    // do something ...
    if (ULevelStreaming* StreamingLevel = StreamingLevelsToConsider.GetStreamingLevels()[Index])
    {
        bool bUpdateAgain = true;
        bool bShouldContinueToConsider = true;
        while (bUpdateAgain && bShouldContinueToConsider)
        {
            bool bRedetermineTarget = false;
            FStreamingLevelPrivateAccessor::UpdateStreamingState(StreamingLevel, bUpdateAgain, bRedetermineTarget);

            if (bRedetermineTarget)
            {
                bShouldContinueToConsider = FStreamingLevelPrivateAccessor::UpdateTargetState(StreamingLevel);
            }
        }

        if (!bShouldContinueToConsider)
        {
            StreamingLevelsToConsider.RemoveAt(Index);
        }
    }

LevelStreaming 的阶段

前面 UWorld::UpdateLevelStreaming 在处理 StreamingLevelsToConsider 调用的是 FStreamingLevelPrivateAccessor::UpdateStreamingState 来更新 LevelStreaming 的状态

LevelStreaming 总共包含下面八个状态

enum class ELevelStreamingState : uint8
{
	Removed,
	Unloaded,
	FailedToLoad,
	Loading,
	LoadedNotVisible,
	MakingVisible,
	LoadedVisible,
	MakingInvisible
};
状态 作用
Removed 已经被删除
Unloaded 未加载
FailedToLoad 加载失败
Loading 正在被加载
LoadedNotVisible 加载完毕,但是还没显示
MakingVisible 标记想要显示
LoadedVisible 加载完毕,并且显示
MakingInvisible 标记不显示

ULevelStreaming::UpdateStreamingState 可见一斑

Unloaded -> Loading -> LoadedNotVisible -> MakingVisible -> LoadedVisible -> MakingInvisible -> LoadedNotVisible -> Unloaded

除了上述这些当前状态之外,还需要一个目标状态 TargetState

  • 比如当前都是 LoadedNotVisible
    • 如果当前是加载流程,那么 TargetState 应该是 LoadedVisible
    • 如果当前时卸载流程,那么 TargetState 应该是 Unloaded
enum class ELevelStreamingTargetState : uint8
{
	Unloaded,
	UnloadedAndRemoved,
	LoadedNotVisible,
	LoadedVisible,
};

代码其实写的很简单粗暴,两个 switch 通过 CurrentStateTargetState 决定执行哪些函数

case ELevelStreamingState::LoadedNotVisible:
    switch (TargetState)
    {
    case ELevelStreamingTargetState::LoadedVisible:
        // Do Something ...
        break;
    case ELevelStreamingTargetState::Unloaded:
        // Do Something ...
        break;
    case ELevelStreamingTargetState::LoadedNotVisible:
        // Do Something ...
        break;
    default:
        ensure(false);
    }
当前 目标 操作 作用 下一个状态
Unloaded LoadedNotVisible UpdateStreamingState_RequestLevel 异步加载资源 Loading
Loading 无操作 无操作 等待资源加载完毕 LoadedNotVisible 或者 LoadedVisible
LoadedNotVisible LoadedVisible BeginClientNetVisibilityRequest MakingVisible
MakingVisible LoadedVisible World->AddToWorld 加载场景对象 LoadedVisible

LevelStreamingLoadedVisible 的时候,就表示加载完毕并且显示内容

AddToWorld

当 Level 资产加载完毕之后,需要将其显示到场景中

先记录两个数据:TimeLimit 时间限制 和 StartTime 开始时间

const double StartTime = FPlatformTime::Seconds();
double TimeLimit = 0.0;

总共有六步操作

每次操作完之后都通过 IsTimeLimitExceeded 根据 StartTimeTimeLimit 来判断操作时间超过时间限制

  • 没超过时间,继续向后进行
  • 超过时间,就先跳过后续操作
  1. 移动 Actor 的坐标,毕竟 加载时有设置 LevelStreamingLocation
FLevelUtils::FApplyLevelTransformParams TransformParams(Level, LevelTransform);
TransformParams.bSetRelativeTransformDirectly = true;
FLevelUtils::ApplyLevelTransform(TransformParams);
  1. 修正所有 Actor 中的位置,确保新 Level 在世界原点(World Origin)下的正确位置
if (WorldComposition)
{
    WorldComposition->OnLevelAddedToWorld(Level);
}
  1. 更新 Actor 身上的组件
do
{
    Level->IncrementalUpdateComponents( NumComponentsToUpdate, bRerunConstructionScript, ContextPtr);
    // Process AddPrimitives if threshold is reached
    if (Context.Count() > GLevelStreamingAddPrimitiveGranularity)
    {
        Context.Process();
    }
}
while (!Level->bAreComponentsCurrentlyRegistered && !IsTimeLimitExceeded(TEXT("updating components"), StartTime, Level, TimeLimit));
  1. 初始化Actor网络相关的设置
if (!Level->bAlreadyInitializedNetworkActors)
{
    Level->InitializeNetworkActors();
}
else
{
    Level->ClearActorsSeamlessTraveledFlag();
}
  1. 完成所有 Actor 的初始化操作,其中包括各种初始化逻辑,最终会调用到 Actor 的 BeginPlay
do 
{
    Level->RouteActorInitialize(NumActorsToProcess);
} while (!Level->IsFinishedRouteActorInitialization() && !IsTimeLimitExceeded(TEXT("routing Initialize on actors"), StartTime, Level, TimeLimit));
  1. Actor 排序,将 WorldSettings 放在第一个,将需要联网的 Actor 放在前面,不联网的 Actor 放在后面
Level->SortActorList();
Level->bAlreadySortedActorList = true;
  1. 完成处理,设置可见性,广播事件
Level->bIsVisible = true;
IStreamingManager::Get().AddLevel(Level);
FWorldDelegates::LevelAddedToWorld.Broadcast(Level, this);
BroadcastLevelsChanged();
ULevelStreaming::BroadcastLevelVisibleStatus(this, Level->GetOutermost()->GetFName(), true);