关卡系统二、关卡流送机制

0 前言

UE引擎的一大特色就是通过关卡流送机制提供了大世界的解决方案。什么是关卡流送?我的理解是将超大关卡划分为若干小关卡,然后通过一定的策略动态加载/卸载小关卡,而置身其中的玩家感觉不到加载/卸载过程,仿佛它们本来就长这样。那么它是如何实现的呢?下面就让我们来一步步揭开它的神秘面纱吧。

为了避免大篇幅代码,文中涉及的代码仅包含关键部分

1 流式关卡的操作指南

创建流关卡有手动添加和WorldComposition自动生成两种方式,手动方式比较适用于创建少量且易于编辑的关卡,如果关卡太多太大,手动编辑无疑是非常麻烦的,此时就需要用到WorldComposition方式了,它会把N个关卡用类似于拼图的形式拼接成大世界。

1.1 手动方式

首先创建几个SubLevel,然后按照下面的操作即可完成。

其中SubLevel两种加载方式,一种是会随着PersistentLevel(主关卡)一起加载(Always Loaded),一种是通过代码动态加载(Blueprint)。

1.2 WorldComposition方式

在PersistentLevel(主关卡)的WorldSettings中开启WorldComposition

注意,在开启WorldComposition前不要手动给PersistentLevel添加SubLevel,否则无法开启WorldComposition,弹出如下MessageBox。

开启之后,会自动将PersistentLevel所在文件夹下的所有Level添加进PersistentLevel,此时添加进去的SubLevel是没有加载的,可以右击SubLevel选择Load来加载。

点击如上红框内按钮打开WorldComposition编辑界面,你可以在这里任意编排SubLevel(前提是先Load要编排的SubLevel)!

可以通过滑动鼠标滚轮来放大该界面,其中箭头表示玩家位置,黄色框表示选中的SubLevel,StreamingDistance表示该关卡距离玩家50000cm内会加载。

2 流式关卡的代码实现

前面介绍了流关卡的创建方式,接下来看看他的代码实现

World是所有关卡的管理者,不管是通过手动方式还是WorldComposition方式,流式关卡最终都会被添加到World的StreamingLevels和StreamingLevelsToConsider容器中,这将用于之后的流式关卡加载。

下面是两种创建流式关卡方式的代码实现。

2.1 手动方式

手动创建方式会在添加SubLevel时创建一个ULevelStreamingDynamic类型的流式关卡,创建完毕之后调用World类的AddStreamingLevel方法添加进StreamingLevelsToConsider。

void FStreamingLevelCollectionModel::HandleAddExistingLevelSelected(const TArray<FAssetData>& SelectedAssets, bool bRemoveInvalidSelectedLevelsAfter)
{
    //...
    //AddedLevelStreamingClass为ULevelStreamingDynamic::StaticClass()
    EditorLevelUtils::AddLevelsToWorld(CurrentWorld.Get(), MoveTemp(PackageNames), AddedLevelStreamingClass);
}
 
//创建StreamingLevel并调用AddStreamingLevel方法
ULevelStreaming* UEditorLevelUtils::AddLevelToWorld_Internal(UWorld* InWorld, const TCHAR* LevelPackageName, TSubclassOf<ULevelStreaming> LevelStreamingClass, const FTransform& LevelTransform)
{
    //...
    StreamingLevel = NewObject<ULevelStreaming>(InWorld, LevelStreamingClass, NAME_None, RF_NoFlags, NULL);
    // Add the new level to world.
    InWorld->AddStreamingLevel(StreamingLevel);
    return StreamingLevel;
}
 
//添加到StreamingLevels和StreamingLevelsToConsider
void UWorld::AddStreamingLevel(ULevelStreaming* StreamingLevelToAdd)
{
    //...
    StreamingLevels.Add(StreamingLevelToAdd);
    StreamingLevelsToConsider.Add(StreamingLevelToAdd);
}

2.2 WorldComposition方式

当开启WorldComposition时,引擎会遍历PersistentLevel(即WorldRoot)所在文件下的所有关卡,生成WorldCompositionTile,给每个Tile生成对应的StreamingLevel,最终添加进StreamingLevelsToConsider

bool FUnrealEdMisc::EnableWorldComposition(UWorld* InWorld, bool bEnable)
{
    //创建WorldCompostion会调用Rescan()
    auto WorldCompostion = NewObject<UWorldComposition>(InWorld);
    InWorld->WorldComposition = WorldCompostion;
    return true;
}
 
void UWorldComposition::Rescan()
{  
    //收集Tile packageNames
    Gatherer.BuildTileCollection(WorldRootFilename);
    for (const FString& TilePackageName : Gatherer.TilesCollection)
    {  
        //读取WorldTileInfo信息
        FWorldTileInfo Info;
        if (!FWorldTileInfo::Read(TileFilename, Info))
        //创建WorldCompositionTile
        FWorldCompositionTile Tile;
        Tiles.Add(Tile);
    }
    // 为每个tile创建StreamingLevel
    PopulateStreamingLevels();
}
// 为每个tile创建StreamingLevel
void UWorldComposition::PopulateStreamingLevels()
{
    for (const FWorldCompositionTile& Tile : Tiles)
    {   //创建StreamingLevel
        TilesStreaming.Add(CreateStreamingLevel(Tile));
    }
}
//创建StreamingLevel
ULevelStreaming* UWorldComposition::CreateStreamingLevel(const FWorldCompositionTile& InTile) const
{
    ULevelStreamingDynamic* StreamingLevel = NewObject<ULevelStreamingDynamic>(OwningWorld, NAME_None, RF_Transient);    
    return StreamingLevel;
}
//打开WorldComposition编辑界面或者运行游戏时添加到World的StreamingLevelsToConsider
void UWorldComposition::PostLoad()
{
    World->SetStreamingLevels(TilesStreaming);
}

3 流式关卡的加载/卸载

3.1 流式关卡加载/卸载状态机

流式关卡的加载/卸载通过一个状态机实现,状态包括当前状态和目标状态,这些状态会在UpdateStreamingState中更新。

//当前状态
enum class ECurrentState : uint8
{
    Removed,
    Unloaded,
    FailedToLoad,
    Loading,
    LoadedNotVisible,
    MakingVisible,
    LoadedVisible,
    MakingInvisible
};
//目标状态
enum class ETargetState : uint8
{
    Unloaded,
    UnloadedAndRemoved,
    LoadedNotVisible,
    LoadedVisible,
};

EngineLoop的Tick调用World的UpdateLevelStreaming,前面多次提到的StreamingLevelsToConsider在此处排上用场了。

void UWorld::UpdateLevelStreaming()
{
    for (int32 Index = StreamingLevelsToConsider.GetStreamingLevels().Num() - 1; Index >= 0; --Index)
    {
        if (ULevelStreaming* StreamingLevel = StreamingLevelsToConsider.GetStreamingLevels()[Index])
        {
            //这里会调用ULevelStreaming的UpdateStreamingState
            FStreamingLevelPrivateAccessor::UpdateStreamingState(StreamingLevel, bUpdateAgain, bRedetermineTarget);
        }
    }
}

更新关卡加载/卸载的状态机

void ULevelStreaming::UpdateStreamingState(bool& bOutUpdateAgain, bool& bOutRedetermineTarget)
{
    //匿名函数,开启异步加载关卡的线程
    auto UpdateStreamingState_RequestLevel = [&]()
    {
        //发起加载关卡的请求
        RequestLevel(World, bAllowLevelLoadRequests, (bBlockOnLoad ? ULevelStreaming::AlwaysBlock : ULevelStreaming::BlockAlwaysLoadedLevelsOnly));
    };
    //状态机,由于代码比较多,不展开
    switch(CurrentState)
    {
    case ECurrentState::MakingVisible:
        break;
    case ECurrentState::MakingInvisible:
        break;
    case ECurrentState::Loading:
        // Just waiting
        break;
    case ECurrentState::Unloaded:
        switch (TargetState)
        {
            case ETargetState::LoadedNotVisible:
            {  
                //当前状态为未加载,目标状态为加载后不显示
                UpdateStreamingState_RequestLevel();
            }
            break;
            case ETargetState::UnloadedAndRemoved:
                break;
            default:
                ensure(false);
        }
        break;
    case ECurrentState::LoadedNotVisible:
        switch (TargetState)
        {
        case ETargetState::LoadedVisible:
            break;
        case ETargetState::Unloaded:
            //卸载关卡
            DiscardPendingUnloadLevel(World);
            break;
        case ETargetState::LoadedNotVisible:
            //加载关卡
            UpdateStreamingState_RequestLevel();
            break;
        default:
            ensure(false);
        }
        break;
    case ECurrentState::LoadedVisible:
        switch (TargetState)
        {
        case ETargetState::LoadedNotVisible:
            break;
        case ETargetState::LoadedVisible:
            UpdateStreamingState_RequestLevel();
            break;
        default:
            ensure(false);
        }
        break;
    case ECurrentState::FailedToLoad:
        break;
    default:
    }
}

状态机图

3.2 关卡加载流程

发起异步加载资源的请求

bool ULevelStreaming::RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, EReqLevelBlock BlockPolicy)
{  
    //开启异步加载线程
    LoadPackageAsync(DesiredPackageName.ToString(), nullptr, *PackageNameToLoadFrom, FLoadPackageAsyncDelegate::CreateUObject(this, &ULevelStreaming::AsyncLevelLoadComplete), PackageFlags, PIEInstanceID, GetPriority());        
    return true;
}

资源加载完毕的回调

void ULevelStreaming::AsyncLevelLoadComplete(const FName& InPackageName, UPackage* InLoadedPackage, EAsyncLoadingResult::Type Result)
{
    ULevel* Level = World->PersistentLevel;
    if (Level)
    {
        SetLoadedLevel(Level);
    }
}

3.3 关卡卸载流程

标记需要卸载的关卡

void ULevelStreaming::DiscardPendingUnloadLevel(UWorld* PersistentWorld)
{
    if (PendingUnloadLevel->bIsVisible)
    {
        PersistentWorld->RemoveFromWorld(PendingUnloadLevel);
    }
    if (!PendingUnloadLevel->bIsVisible)
    {
        //标记要卸载的关卡 
        FLevelStreamingGCHelper::RequestUnload(PendingUnloadLevel);
    }
}

卸载关卡

void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
    //调用PrepareStreamedOutLevelsForGC卸载关卡
    GEngine->ConditionalCollectGarbage();
}
//卸载关卡
void FLevelStreamingGCHelper::PrepareStreamedOutLevelsForGC()
{
    // Iterate over all level objects that want to be unloaded.
    for (int32 LevelIndex = 0; LevelIndex < LevelsPendingUnload.Num(); LevelIndex++)
    {
        ULevel* Level = LevelsPendingUnload[LevelIndex].Get();
        Level->CleanupLevel();
    }
}

4 关卡流送机制

UE提供了三种关卡流送机制:LevelStreamingVolume、WorldComposition和代码(LoadStreamLevel/UnloadStreamLevel)

4.1 LevelStreamingVolume

这种方式比较简单,类似在关卡放一个Trigger,当玩家进入/离开该区域,会自动加载/卸载指定的关卡。

LevelStreamingVolume继承自Actor,我们可以像其他Actor一样放入指定的关卡(通常指PersistentLevel),还可以编辑它的大小来调整有效区域。

将LevelStreamingVolume拖动关卡指定位置,并调整大小

编辑SubLevel,将刚拖到关卡里面的LevelStreamingVolume赋给SubLevel即可。

在运行时,引擎通过判断玩家位置是否在LevelStreamingVolume内来动态加载/卸载指定SubLevel

void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
    ProcessLevelStreamingVolumes();
}
//每次Tick获取玩家ViewLocation信息
void UWorld::ProcessLevelStreamingVolumes(FVector* OverrideViewLocation)
{
    FVector ViewLocation(0,0,0);
    FRotator ViewRotation(0,0,0);
    PlayerActor->GetPlayerViewPoint( ViewLocation, ViewRotation );
    //判断是不是在LevelStreamingLevel的区域内
    bViewpointInVolume = StreamingVolume->EncompassesPoint( ViewLocation );
    if ( bViewpointInVolume )
    {
        StreamingSettings |= FVisibleLevelStreamingSettings( (EStreamingVolumeUsage) StreamingVolume->StreamingUsage );
        VisibleLevelStreamingObjects.Add( LevelStreamingObject, StreamingSettings );
    }
}

判断是不是在LevelStreamingLevel的Bounds内

bool AVolume::EncompassesPoint(FVector Point, float SphereRadius/*=0.f*/, float* OutDistanceToPoint) const
{
    FBoxSphereBounds Bounds = GetBrushComponent()->CalcBounds(GetBrushComponent()->GetComponentTransform());
    const float DistanceSqr = Bounds.GetBox().ComputeSquaredDistanceToPoint(Point);
    return DistanceSqr >= 0.f && DistanceSqr <= FMath::Square(SphereRadius);
}

4.2 WorldComposition

前面已经介绍了如何利用WorldComposition制作流式关卡,在运行时,引擎通过判断关卡与玩家的距离(默认50000cm)来动态加载/卸载关卡,做开放世界大地图推荐用这种方式。

void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
  if (WorldComposition)
   {
        WorldComposition->UpdateStreamingState();
    }
}

计算玩家的ViewLocation

void UWorldComposition::UpdateStreamingState()
{
    TArray<FVector, TInlineAllocator<16>> Locations;   
    for (int32 PlayerIndex = 0; PlayerIndex < NumPlayers; ++PlayerIndex)
    {
        ULocalPlayer* Player = GEngine->GetGamePlayer(PlayWorld, PlayerIndex);
        if (Player && Player->PlayerController)
        {
           FVector ViewLocation;
            FRotator ViewRotation;
            Player->PlayerController->GetPlayerViewPoint(ViewLocation, ViewRotation);
            Locations.Add(ViewLocation);
        }
    }
    if (Locations.Num())
    {
        UpdateStreamingState(Locations.GetData(), Locations.Num());
    }
}

根据玩家位置来加载/卸载关卡

void UWorldComposition::UpdateStreamingState(const FVector* InLocations, int32 Num)
{
    TArray<FDistanceVisibleLevel> DistanceVisibleLevels;
    TArray<FDistanceVisibleLevel> DistanceHiddenLevels;
    //根据玩家位置来将关卡放到可见和不可见列表
    GetDistanceVisibleLevels(InLocations, Num, DistanceVisibleLevels, DistanceHiddenLevels);
    // 不可见的关卡卸载
    for (const FDistanceVisibleLevel& Level : DistanceHiddenLevels)
    {
        CommitTileStreamingState(OwningWorld, Level.TileIdx, false, false, bShouldBlock, Level.LODIndex);
    }
    // 可见的关卡加载
    for (const FDistanceVisibleLevel& Level : DistanceVisibleLevels)
    {
        CommitTileStreamingState(OwningWorld, Level.TileIdx, true, true, bShouldBlock, Level.LODIndex);
    }
}

距离计算玩家与Level的距离,AABB包围盒算法

void UWorldComposition::GetDistanceVisibleLevels(const FVector* InLocations,int32 NumLocations,TArray<FDistanceVisibleLevel>& OutVisibleLevels,TArray<FDistanceVisibleLevel>& OutHiddenLevels) const
{  
    for (int32 TileIdx = 0; TileIdx < Tiles.Num(); TileIdx++)
    {
        const FWorldCompositionTile& Tile = Tiles[TileIdx];
        bool bIsVisible = false;
        // 判断Sphere是否与LevelBounds组成Box相交
        FIntPoint LevelPositionXY = FIntPoint(Tile.Info.AbsolutePosition.X, Tile.Info.AbsolutePosition.Y);
        FIntPoint LevelOffsetXY = LevelPositionXY - WorldOriginLocationXY;
        FBox LevelBounds = Tile.Info.Bounds.ShiftBy(FVector(LevelOffsetXY));
        
        int32 TileStreamingDistance = Tile.Info.GetStreamingDistance(LODIdx);
        for (int32 LocationIdx = 0; LocationIdx < NumLocations; ++LocationIdx)
        {
            FSphere QuerySphere(InLocations[LocationIdx], TileStreamingDistance);
            if (FMath::SphereAABBIntersection(QuerySphere, LevelBounds))
            {
                bIsVisible = true;
                    break;
            }
        }
        if (bIsVisible)
        {
            OutVisibleLevels.Add(VisibleLevel);
        }
        else
        {
            OutHiddenLevels.Add(VisibleLevel);
        }
    }
}

4.3 LoadStreamLevel/UnloadStreamLevel

该方式可由程序员在特定逻辑下通过代码来加载/卸载流式关卡,比如在触发某个Actor的Overlap事件时加载/卸载关卡。

首先在PersistentLevel添加需要Load的子关卡(也可以通过代码动态添加进PersistentLevel),因为在调用加载/卸载前会去查找存放与World里的StreamingLevels,找不到返回NULL

ULevelStreaming* FStreamLevelAction::FindAndCacheLevelStreamingObject( const FName LevelName, UWorld* InWorld )
{
    for (ULevelStreaming* LevelStreaming : InWorld->GetStreamingLevels())
    {
        if (LevelStreaming && LevelStreaming->GetWorldAssetPackageName().EndsWith(SearchPackageName, ESearchCase::IgnoreCase))
        {
            return LevelStreaming;
        }
    }
    return NULL;
}

然后在蓝图或者c++代码里调用非常简单,不再详述。

5 总结

本文从创建流式关卡的操作指南开始,一步一步详细的介绍了流式关卡的代码实现,资源加载与卸载,以及引擎提供的三种流送机制。至此,对UE中的流式关卡有了一定的认识。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值