关卡系统三、关卡切换流程

0 前言

在前面的文章中介绍了Level与World的概念以及它们的关系,也介绍了UE实现开放大世界的关卡流送机制,现在对Level与World以及开放大世界有了一定的了解,那么,接下来让我们思考这样一个问题,那种关卡类的游戏(即带有传送门这种)应该如何切换关卡?这里说的切换关卡是指切换PersistentLevel,根据前面的介绍,流式关卡只是在PersistentLevel(主关卡)上添加进SubLevel,并没有切换PersistentLevel,接下来让我们来看看UE中是如何实现关卡切换的。

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

1 关卡切换的接口

在分析关卡切换的底层实现前,我们先来看看如果要实现关卡切换,应该怎么做。

按照一贯的套路,先会用再探究原理

创建两个Level,在其中一个Level放一个Trigger(或其他带有碰撞盒的Actor)

编辑Character的蓝图

运行游戏,当角色跑到Trigger所在的地方就会切换关卡,用法很非常简单。

2 关卡切换流程分析

前面已经会使用关卡切换的API了,那么接下来我们看看引擎中是如何实现关卡切换的。

2.1 OpenLevel接口

在分析关卡流程前,我们先来看看OpenLevel接口干了什么

void UGameplayStatics::OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute, FString Options)
{
    const ETravelType TravelType = (bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
    FWorldContext &WorldContext = GEngine->GetWorldContextFromWorldChecked(World);
    FURL TestURL(&WorldContext.LastURL, *Cmd, TravelType);
    //设置将要切换的关卡信息,包括Url,TravelType
    GEngine->SetClientTravel( World, *Cmd, TravelType );
}
//设置将要切换的关卡信息
void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
{
    FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);
    // 设置URL和Type,下一帧UGameEngine::Tick()时候会处理这些信息
    Context.TravelURL    = NextURL;
    Context.TravelType   = InTravelType;
}

其中FURL即关卡的文件路径,有一定的格式,可以用来表示本地或者远端的关卡路径,如果是本地的关卡,那么只需要填Map字段

struct ENGINE_API FURL
{
    FString Protocol; //可以是"unreal" or "http"
    FString Host;//可以是"204.157.115.40" or "unreal.epicgames.com"这种
    int32 Port; //端口
    FString Map; //地图名词,比如MainCity
    //...
};

TravelType表示绝对路径还是相对路径

enum ETravelType
{
    /** Absolute URL. */
    TRAVEL_Absolute,
    /** Partial (carry name, reset server). */
    TRAVEL_Partial,
    /** Relative URL. */
    TRAVEL_Relative,
    //...
};

还有一个比较重要的类FWorldContext,它在关卡切换中扮演着非常重要的角色,它保存了关卡切换的上下文,下面将详细介绍。

2.2 关卡切换流程

从前面的OpenLevel入口可知,它仅仅是设置了TravelURL和TravelType,那么关卡到底是如何切换的?因此我们第一时间应该会想到可能是在下一次Tick的时候才真正切换的。

话不多说,直接上调用堆栈

如图所见,EngineLoop:Tick经过层层调用,最终调到了UEngine:TickWorldTravel这个函数,这就是我们要关注的重点——关卡切换的起点!至于为啥是从UUnrealEdEngine:Tick中调用是因为在PIE模式下调试的,但这不是本文的重点,感兴趣可以看看下面的代码

int32 FEngineLoop::Init()
{
    //如果不是在编辑器中的话,GEngine为UGameEngine的实例
    if( !GIsEditor )
    {  
        EngineClass = StaticLoadClass( UGameEngine::StaticClass(), nullptr, *GameEngineClassName);
        GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
    }
    else
    {   //UUnrealEdEngine继承自UEditorEngine
        EngineClass = StaticLoadClass(UUnrealEdEngine::StaticClass(), nullptr, *UnrealEdEngineClassName);
        GEngine = GEditor = GUnrealEd = NewObject<UUnrealEdEngine>(GetTransientPackage(), EngineClass);
    }
    return 0;
}

关卡切换流程图

TickWorldTravel作为关卡切换的入口,这里会涉及到无缝切换(SeamlessTravelHandler)、服务端切换和客户端切换,其中无缝切换比较复杂,不在此处展开,而服务端和客户端的逻辑基本一致,只是传的参数不一样,所以这里就只看客户端流程。

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
    //处理无缝切换相关
    if (Context.SeamlessTravelHandler.IsInTransition())
    {
        Context.SeamlessTravelHandler.Tick();
    }
    //处理服务端切换关卡
    if( !Context.World()->NextURL.IsEmpty() )
    {
        Context.World()->NextSwitchCountdown -= DeltaSeconds;
        if( Context.World()->NextSwitchCountdown <= 0.f )
        {
            EBrowseReturnVal::Type Ret = Browse( Context, FURL(&Context.LastURL,*NextURL,(ETravelType)Context.World()->NextTravelType), Error );
            if (Ret != EBrowseReturnVal::Success )
            {
              BrowseToDefaultMap(Context); //切换到默认关卡
            }
            return;
        }
    }
    //处理客户端切换关卡
    if( !Context.TravelURL.IsEmpty() )
    {  
        if (Browse( Context, FURL(&Context.LastURL,*TravelURLCopy,(ETravelType)Context.TravelType), Error ) == EBrowseReturnVal::Failure)
        {
            if (Context.World() == NULL)
            {
                BrowseToDefaultMap(Context);//切换到默认关卡
            }
        }
        return;
    }
    return;
}

URL相关的处理

EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
    WorldContext.TravelURL = TEXT("");
    if( URL.IsLocalInternal() )
    {
        //加载map
        return LoadMap( WorldContext, URL, NULL, Error ) ? EBrowseReturnVal::Success : EBrowseReturnVal::Failure;
    }
}

开始加载map

bool UEngine::LoadMap( FWorldContext& WorldContext, FURL URL, class UPendingNetGame* Pending, FString& Error )
{  
    //干掉当前World
    if( WorldContext.World())
    {
        //清空Player相关
        for(auto It = WorldContext.OwningGameInstance->GetLocalPlayerIterator(); It; ++It)
        {  
            ULocalPlayer *Player = *It;
            WorldContext.World()->DestroyActor(Player->PlayerController->GetPawn(), true);
            WorldContext.World()->DestroyActor(Player->PlayerController, true);
        }
        //清空Level,AISystem、PhysicScene等
        WorldContext.World()->CleanupWorld();
        WorldContext.SetCurrentWorld(nullptr);
    }
    //开始切换到新World
    if (NewWorld == NULL)
    {
        //先看看内存中是否已经有这Map了
        WorldPackage = FindPackage(nullptr, *URL.Map);
        //如果没有的话,LoadPackage
       if (WorldPackage == nullptr)
        {
            WorldPackage = LoadPackage(nullptr, *URL.Map, (WorldContext.WorldType == EWorldType::PIE ? LOAD_PackageForPIE : LOAD_None));
        }
        //从map的packge中取World
        NewWorld = UWorld::FindWorldInPackage(WorldPackage);
    }
    //创建新World之后
    GWorld = NewWorld;
    WorldContext.SetCurrentWorld(NewWorld);
}

这里可以看出,通过OpenLevel的方式切换关卡(PersistentLevel),实际上会将整个World切换,这里World与PersistentLevel是一一对应的。

2.3 需要注意的点

其一,在切换之前,会干掉当前世界以及其中的任何对象,因此上一个World的对象是不会保存到下一个World,只能在下一个World中重新创建!

其二,LoadPackage是在主线程中执行的(非异步线程),也就是主线程阻塞的,在切换过程中是不能做其他逻辑的,那么,我们要实现LoadingScreen这样的功能应该怎么做?

有两种解决思路:

一种是是先异步加载关卡资源,然后切换关卡的时候发现已经在内存中了(上面代码中的FindPackage),就直接切换了,但是这个有一点需要考虑,在当前关卡加载另外一个关卡,如果关卡资源很大的话,建议用一个过渡关卡(UE中叫TransistionMap)来做;

一种是UE推荐的方式,就是在切换关卡前,开启一个异步线程来做LoadingScreen相关的逻辑,大致就是用Slate框架来写(Slate是独立框架,不依赖Engine的Tick),UE提供的MoviePlayer就是用来做LoadingScreen的,具体实现不在此详述,感兴趣的可以参考官方demo ARPG工程里LoadingScreen的实现,还有一个很好用的插件也可以研究研究(AsyncLoadingScreen)。

3 总结

本文从使用OpenLevel接口开始,介绍了关卡的切换流程,用这种方式切换关卡,真正的切换时机是下一次Tick,切换关卡会导致World生成,也提到了几个注意的点和解决LoadingScreen的方案。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值