关卡系统一、Level与World

本文详细探讨了UnrealEngine中Level和World的概念,以及它们在关卡切换、Actor管理、加载屏幕和资源文件中的作用,帮助开发者理解如何有效地使用这些机制来构建游戏世界。
摘要由CSDN通过智能技术生成

0 前言

最近在做关卡切换功能遇到了一些问题,比如引擎提供的OpenLevel、LoadStreamLevel、SeamlessTravel和non-seamlessTravel等接口皆与关卡切换有关,那么问题来了,他们有什么区别?在切换关卡的时候怎么显示loadingscreen界面?要弄清楚这些问题,需要先搞清楚UE宇宙中的Level和World这两个概念以及他们的关系。

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

1 Level和World

我们知道,UE利用Actor构建出了绚丽多彩的世界,并用ActorComponent赋予了Actor各种各样的行为,就像真实世界中的人、动物、植物等一样构建了整个世界,但是这样构建的世界是巨大的,不仅对计算机的性能是一种极大的挑战,而且也没有必要这样做,因为你会发现,在某个区域的Actor们的活动范围大部分时间都是在这个区域内(比如生活在亚洲的动物可能永远也不会与生活在欧洲的动物发生关系),UE把这个更细粒度的区域称为Level,Level管理着形形色色的Actor们,使它们在这片区域内井然有序,又由N(>=1)个Level构成了精彩纷呈的World。

1.1 Level

从上图可以看到,ULevel继承自UObject, 因此可以被序列化,在UE中Level被序列化为.umap格式的文件保存起来。

每个Level持有一个LevelScriptBlueprint,它是一类特殊的蓝图(官方介绍),使得我们可以像其他蓝图一样编辑它,它的创建时机为在Editor中第一次点击Open LevelBlueprint按钮

void FLevelEditorActionCallbacks::OpenLevelBlueprint( TWeakPtr< SLevelEditor > LevelEditor )
{
    if( LevelEditor.Pin()->GetWorld()->GetCurrentLevel() )
    {
        ULevelScriptBlueprint* LevelScriptBlueprint = LevelEditor.Pin()->GetWorld()->PersistentLevel->GetLevelScriptBlueprint();
        //...
    }
}
//创建关卡蓝图
ULevelScriptBlueprint* ULevel::GetLevelScriptBlueprint(bool bDontCreate)
{
    if( !LevelScriptBlueprint && !bDontCreate)
    {
        // If no blueprint is found, create one.
        LevelScriptBlueprint = Cast<ULevelScriptBlueprint>(FKismetEditorUtilities::CreateBlueprint(GEngine->LevelScriptActorClass, this, FName(*LevelScriptName), BPTYPE_LevelScript, ULevelScriptBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass()));
        //...
    }
}

每个Level持有一个LevelScriptActor,它在游戏中始终看不见(没有SceneComponent),它具有一般Actor的能力(BeginPlay、Tick等),这意味着我们可以像其他Actor一样添加逻辑和Component,虽然可以(比如引擎的UInputComponent就是直接在LevelScriptActor添加的),但是尽量不要这么做,不要让Level逻辑变得复杂,让它安安静静的做个Actor容器。

LevelScriptActor的创建时机为每次关卡蓝图发生变化时

void ULevel::OnLevelScriptBlueprintChanged(ULevelScriptBlueprint* InBlueprint)
{
    //...
    if( LevelScriptActor )
    {
        if (InBlueprint->GetObjectBeingDebugged() == LevelScriptActor)
        {
            bResetDebugObject = true;
            InBlueprint->SetObjectBeingDebugged(nullptr);
        }
        LevelScriptActor->MarkPendingKill();
        LevelScriptActor = nullptr;
    }
    //创建LevelScriptActor
    LevelScriptActor = OwningWorld->SpawnActor<ALevelScriptActor>( SpawnClass, SpawnInfo );               
}  

每个Level持有一个TArray<AActor*> Actors,它用于存放关卡内的所有Actor,因此关卡实际上是一个Actor容器。如果你有看过SpawnActor方法(所有Actor都会经过该方法创建)的代码就一目了然了,它会将创建的Actor放入Level的Actors容器内。

AActor* UWorld::SpawnActor( UClass* Class, FTransform const* UserTransformPtr, const FActorSpawnParameters& SpawnParameters )
{
    ULevel* CurrentLevel = PersistentLevel;
    ULevel* LevelToSpawnIn = SpawnParameters.OverrideLevel;
    if (LevelToSpawnIn == NULL)
    {
        // 如果没有传入指定的关卡,就会指定CurrentLevel.
        LevelToSpawnIn = (SpawnParameters.Owner != NULL) ? SpawnParameters.Owner->GetLevel() : CurrentLevel;
    }
    // 真正创建actor的地方
    AActor* const Actor = NewObject<AActor>(LevelToSpawnIn, Class, NewActorName, ActorFlags, Template, false/*bCopyTransientsFromClassDefaults*/, nullptr/*InInstanceGraph*/, ExternalPackage);
     
    //Add到Level的Actors容器中
    LevelToSpawnIn->Actors.Add(Actor);
    return Actor;
}

为了提高网络相关Actor在进行网络复制的查找速度,Level还给Acttors进行了排序,将非网络相关的排在网络相关的前面,其中WorldSettings这个Actor(ActorInfo类)始终排在第一。

/**
    * Sorts the actor list by net relevancy and static behaviour. First all not net relevant static
    * actors, then all net relevant static actors and then the rest. This is done to allow the dynamic
    * and net relevant actor iterators to skip large amounts of actors.
*/
void ULevel::SortActorList()
{
    TArray<AActor*> NewActors;
    TArray<AActor*> NewNetActors;
    if (WorldSettings)
    {
        // The WorldSettings tries to stay at index 0
        NewActors.Add(WorldSettings);
        if (OwningWorld != nullptr)
        {
            //添加进网络相关的list
            OwningWorld->AddNetworkActor(WorldSettings);
        }
    }
    // Add non-net actors to the NewActors immediately, cache off the net actors to Append after
    for (AActor* Actor : Actors)
    {
        if (Actor != nullptr && Actor != WorldSettings && !Actor->IsPendingKill())
        {
            if (IsNetActor(Actor))
            {
                NewNetActors.Add(Actor);
                if (OwningWorld != nullptr)
                {
                    //添加进网络相关的list
                    OwningWorld->AddNetworkActor(Actor);
                }
            }else
            {
                NewActors.Add(Actor);
            }
        }
    }
    NewActors.Append(MoveTemp(NewNetActors));
    Actors = MoveTemp(NewActors);
}

1.2 World

从上图可以看到,World比较复杂,不仅管理着所有的Level/StreamingLevel,所有的PlayerController,还持有物理系统(PhysicsScene)、世界组成信息(WorldComposition)以及GameMode等指针。

每个World至少持有一个PersistentLevel和大于等于零个其他Level,其中PersistentLevel(被视为主关卡)是在World创建之后创建的,它主要是用来管理其他关卡的流入与流出。

//创建world的代码
UWorld* UWorld::CreateWorld(const EWorldType::Type InWorldType, bool bInformEngineOfWorld, FName WorldName, UPackage* InWorldPackage, bool bAddToRoot, ERHIFeatureLevel::Type InFeatureLevel)
{
    UWorld* NewWorld = NewObject<UWorld>(WorldPackage, *WorldNameString);
    NewWorld->InitializeNewWorld(UWorld::InitializationValues().ShouldSimulatePhysics(false).EnableTraceCollision(true).CreateNavigation(InWorldType == EWorldType::Editor).CreateAISystem(InWorldType == EWorldType::Editor));
    return NewWorld;
}
 
//创建PersistentLevel
void UWorld::InitializeNewWorld(const InitializationValues IVS)
{
    //创建PersistentLevel
   PersistentLevel = NewObject<ULevel>(this, TEXT("PersistentLevel"));
}

World的Levels容器保存了所有已经加载完毕的Level(当然包括PersistentLevel),而StreamingLevels保存了PersistentLevel的所有子关卡信息实例(ULevelStreamingDynamic)

//将PersistentLevel添加进Levels
void UWorld::InitWorld(const InitializationValues IVS)
{
    Levels.Empty(1);
    Levels.Add(PersistentLevel);
}

World还持有一个CurrentLevel指针,在编辑的时候CurrentLevel可以指向其他关卡,但是在运行时,CurrentLevel只能指向PersistentLevel。

#if WITH_EDITORONLY_DATA
    /** Pointer to the current level being edited. Level has to be in the Levels array and == PersistentLevel in the game. */
    UPROPERTY(Transient)
    class ULevel* CurrentLevel;
#endif

//获取World的当前关卡
ULevel* UWorld::GetCurrentLevel() const
{
#if WITH_EDITORONLY_DATA
    return CurrentLevel;
#else
    return PersistentLevel;
#endif
}

通过编辑器可以手动给PersistentLevel添加Sublevel。

点击AddExisting即可添加现有的Sublevel,代码流程如下:

void FStreamingLevelCollectionModel::HandleAddExistingLevelSelected(const TArray<FAssetData>& SelectedAssets, bool bRemoveInvalidSelectedLevelsAfter)
{   //...
    EditorLevelUtils::AddLevelsToWorld(CurrentWorld.Get(), MoveTemp(PackageNames), AddedLevelStreamingClass);
}
 
ULevelStreaming* UEditorLevelUtils::AddLevelToWorld_Internal(UWorld* InWorld, const TCHAR* LevelPackageName, TSubclassOf<ULevelStreaming> LevelStreamingClass, const FTransform& LevelTransform)
{
    if (bIsPersistentLevel || FLevelUtils::FindStreamingLevel(InWorld, LevelPackageName))
    {
        //...
    }
    else
    {   //创建StreamingLevel
        StreamingLevel = NewObject<ULevelStreaming>(InWorld, LevelStreamingClass, NAME_None, RF_NoFlags, NULL);
        //add到StreamingLevels
        InWorld->AddStreamingLevel(StreamingLevel);
    }
    return StreamingLevel;
}

SubLevel加载完毕后,会将Level添加进Levels容器

void ULevelStreaming::AsyncLevelLoadComplete(const FName& InPackageName, UPackage* InLoadedPackage, EAsyncLoadingResult::Type Result)
{
    //...
    UWorld* World = UWorld::FindWorldInPackage(LevelPackage);
    ULevel* Level = World->PersistentLevel;
    UWorld* LevelOwningWorld = Level->OwningWorld;
    if (ensure(LevelOwningWorld) && LevelOwningWorld->WorldType == EWorldType::Editor)
    {
        //添加到Levels
        LevelOwningWorld->AddLevel(Level);
    }
}

还有一种方式添加SubLevel,通过开启WorldComposition,编辑器会自动搜索当前Level所在文件夹下的所有Level作为它的SubLevel。

此时添加进来的SubLevel是没有加载的,它是通过玩家可见范围来动态加载,更多关于WorldCompositiond的操作细节,请参考官方文档

本文主要讨论Level与World及其之间的关系,其他的内容诸如GameMode和物理系统等不在此展开。

2 .umap文件

在编辑器中创建的Level都会被序列化为.umap格式的文件保存在本地

那么它是怎么来的呢?接下来看看UE代码实现

先看看调用堆栈

在当前ContentBrowser中创建asset并保存,贴这个代码主要是为了强调Factory为UWorldFactory,.umap资源的生成与一般.uasset不同,它是经过UWorldFactory::FactoryCreateNew生成的(具体见后面的CreateAsset方法)

FContentBrowserItemData UContentBrowserAssetDataSource::OnFinalizeCreateAsset(const FContentBrowserItemData& InItemData, const FString& InProposedName, FText* OutErrorMsg)
{
    //创建的为Level的话,Factory为UWorldFactory
    UFactory* Factory = CreationContext->GetFactory();
    if (AssetClass || Factory)
    {
        Asset = AssetTools->CreateAsset(InProposedName, CreationContext->GetAssetData().PackagePath.ToString(), AssetClass, Factory, FName("ContentBrowserNewAsset"));
    }
    //保存到当前ContentBrowser
    return CreateAssetFileItem(FAssetData(Asset));
}

创建package,至于package是什么不是本文的重点,只要知道它是用来保存资源数据即可。

UObject* UAssetToolsImpl::CreateAsset(const FString& AssetName, const FString& PackagePath, UClass* AssetClass, UFactory* Factory, FName CallingContext)
{
    //先判断能不能创建package,如果已经在内存中了,直接find,否在create
    if ( !CanCreateAsset(AssetName, PackageName, LOCTEXT("CreateANewObject", "Create a new object")) )
    {
        return nullptr;
    }
    UClass* ClassToUse = AssetClass ? AssetClass : (Factory ? Factory->GetSupportedClass() : nullptr);
    //如果已经在内存中了,直接find,否在create
    UPackage* Pkg = CreatePackage(*PackageName);
    //创建UObject,并指定它的Outer为Pkg
    if ( Factory )
    {  
        //.umap资源的创建会走这里
        NewObj = Factory->FactoryCreateNew(ClassToUse, Pkg, FName( *AssetName ), Flags, nullptr, GWarn, CallingContext);
    }
    else if ( AssetClass )
    {
        //.uasset文件会走这里
        NewObj = NewObject<UObject>(Pkg, ClassToUse, FName(*AssetName), Flags);
    }
    return NewObj;
}

这里的FactoryCreateNew实际上是UWorldFactory::FactoryCreateNew方法

UObject* UWorldFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
    //为创建的Level资源package创建World
    UWorld* NewWorld = UWorld::CreateWorld(WorldType, bInformEngineOfWorld, Name, Cast<UPackage>(InParent), bAddToRoot, FeatureLevel);
    return NewWorld;
}

如你所见,我们创建的每个Level资源文件(即.umap文件)都会带一个World,至此Level与World的关系明了了,因此切换关卡会导致World切换,同时在切换关卡过程中的UEngine::LoadMap中有如下代码也不足为怪:

bool UEngine::LoadMap( FWorldContext& WorldContext, FURL URL, class UPendingNetGame* Pending, FString& Error )
{
    if (NewWorld == NULL)
    {
        WorldPackage = FindPackage(nullptr, *URL.Map);
        if (WorldPackage == nullptr)
        {
            WorldPackage = LoadPackage(nullptr, *URL.Map, (WorldContext.WorldType == EWorldType::PIE ? LOAD_PackageForPIE : LOAD_None));
        }
        // 从package中取出World
        NewWorld = UWorld::FindWorldInPackage(WorldPackage);
    }
}

3 总结

本文着重介绍了Level和World的结构以及它们之间的关系。Level维护了一个Actor容器,World维护了一个Level容器,一方面支持了Level的动态加载,另一方面也允许了团队的实时协作,大家可以同时并行编辑不同的Level。并通过.umap文件的生成过程介绍Level与World在资源中的关系。

好的,下面是用UE4 C++编写异步加载关卡的基本步骤: 1. 创建一个继承自AActor的C++类,这个类将被用来加载关卡。 2. 在.h文件中,添加一个函数声明,用于异步加载关卡。函数原型为:`void LoadLevelAsync(const FString& LevelName);` 3. 在.cpp文件中,实现`LoadLevelAsync`函数。首先,使用`FStreamableManager`类的`LoadLevelAsync`函数来异步加载关卡。这个函数需要两个参数:要加载的关卡名称和一个回调函数,在关卡加载完成后自动调用。 4. 在回调函数中,将加载的关卡传递给一个由你创建的函数,该函数将关卡添加到世界中。 5. 最后,在你的游戏中,调用`LoadLevelAsync`函数来异步加载关卡。 下面是一个简单的代码示例: ```cpp // MyLevelLoader.h #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "MyLevelLoader.generated.h" UCLASS() class MYGAME_API AMyLevelLoader : public AActor { GENERATED_BODY() public: AMyLevelLoader(); UFUNCTION(BlueprintCallable, Category = "Level Loading") void LoadLevelAsync(const FString& LevelName); private: void OnLevelLoaded(ULevel* LoadedLevel); FStreamableManager LevelLoader; }; // MyLevelLoader.cpp #include "MyLevelLoader.h" AMyLevelLoader::AMyLevelLoader() { PrimaryActorTick.bCanEverTick = false; } void AMyLevelLoader::LoadLevelAsync(const FString& LevelName) { LevelLoader.LoadLevelAsync(LevelName, FStreamableDelegate::CreateUObject(this, &AMyLevelLoader::OnLevelLoaded)); } void AMyLevelLoader::OnLevelLoaded(ULevel* LoadedLevel) { if (LoadedLevel) { UWorld* World = GetWorld(); if (World) { World->AddLevelToWorld(LoadedLevel); } } } ``` 在你的游戏中,可以创建AMyLevelLoader实例并调用`LoadLevelAsync`来异步加载关卡。例如: ```cpp AMyLevelLoader* LevelLoader = GetWorld()->SpawnActor<AMyLevelLoader>(); LevelLoader->LoadLevelAsync("MyLevel"); ``` 这里,我们异步加载了名为"MyLevel"的关卡。当关卡加载完成时,`OnLevelLoaded`函数会自动被调用,并将关卡添加到世界中。 希望这可以帮助你开始编写异步加载关卡的代码!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值