关卡系统四、无缝切换

0 前言

对于地图切换(也即关卡切换),UE还提供了无缝切换(Seamless Travel)和非无缝切换(Non-Seamless Travel),无缝切换使用异步加载关卡资源,是非阻塞式切换,而非无缝切换即为前面介绍的同步加载关卡资源,是阻塞式切换(传送门),在网络联机游戏中,无缝切换不会导致网络断开,而非无缝会导致网络断开后重连,UE推荐在网络联机游戏中使用无缝切换,感兴趣可以看看官方文档(有点晦涩难懂T_T,因此需要深入研究一番)。

注意,无缝切换和关卡流送都是异步加载关卡资源的,但它们是不同的,关卡流送只是在当前World添加SubLevel,并不会切换World,而无缝切换会销毁之前的World而创建新的World。

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

1 使用指南

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

先手动创建一个空的或者有少量actor的过渡地图,然后通过ProjectSettings来设置(也可以不自己创建,UE会自动创建一个“dummy World”来作为过渡地图)

至于为什么需要设置过渡地图?试想如果没有过渡地图,那么在当前地图中加载(异步加载的,后面会详细介绍)的新地图之后,此时World同时存在新旧两个地图,如果都比较大的话,内存告急!

开启SeamlessTravel,通过代码或蓝图都可以

通过蓝图开启

修改自己的项目的Character类(我的是ThirdPersonMPCharacter),添加SeamlessTravelTo,这里是为了能在蓝图中用而对SeamlessTravel进行了简单的封装

//在ThirdPersonMPCharacter.h文件中添加
UFUNCTION(BlueprintCallable, Category = "SeamlessTravel")
    void SeamlessTravelTo(FString URL);
 
//在ThirdPersonMPCharacter.cpp里实现
void  AThirdPersonMPCharacter::SeamlessTravelTo(FString URL)
{
    //服务器执行
    //if (GetLocalRole() == ROLE_Authority)
    //{
    //  UWorld* World = GetWorld();
    //  World->ServerTravel(URL, false);
    //}
    //客户端执行
    if (IsLocallyControlled())
    {
        UWorld* World = GetWorld();
        //本例以客户端无缝切换作为例子。
        World->GetFirstPlayerController()->ClientTravel(URL, ETravelType::TRAVEL_Relative, true);
    }
}

在关卡里放一个触发器,然后编辑ThirdPersonCharacter蓝图

ok了,例子很简单,当角色跑到TriggerSphere附近就会触发无缝切换。

2 底层实现

UE提供了ClientTravel和ServerTravel两个无缝切换相关的接口,分别用于客户端和服务端的无缝切换。

2.1 ClientTravel

void APlayerController::ClientTravel(const FString& URL, ETravelType TravelType, bool bSeamless, FGuid MapPackageGuid)
{
    // 最终执行ClientTravelInternal_Implementation
    ClientTravelInternal(URL, TravelType, bSeamless, MapPackageGuid);
}
 
void APlayerController::ClientTravelInternal_Implementation(const FString& URL, ETravelType TravelType, bool bSeamless, FGuid MapPackageGuid)
{
    UWorld* World = GetWorld();
    if (bSeamless && TravelType == TRAVEL_Relative)
    {
        //无缝切换
        World->SeamlessTravel(URL); 
    }
    else
    {
        //非无缝切换,会在UEngine::TickWorldTravel中调用UEngine::Browse进行非无缝切换
        GEngine->SetClientTravel(World, *URL, (ETravelType)TravelType);
    }
}

如上代码所示,使用ClientTravel时要注意,TravelType要为TRAVEL_Relative、bSeamless为true才能进行无缝切换,否则就是非无缝切换(非无缝切换),代码如下:

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
    // 处理客户端travel.
    if( !Context.TravelURL.IsEmpty() )
    {  
        if (Browse( Context, FURL(&Context.LastURL,*TravelURLCopy,(ETravelType)Context.TravelType), Error ) == EBrowseReturnVal::Failure)
        {
            if (Context.World() == NULL)
            {
                BrowseToDefaultMap(Context);
            }
        }
        return;
    }
}

2.2 ServerTravel

bool UWorld::ServerTravel(const FString & FURL, bool bAbsolute, bool bShouldSkipGameNotify)
{
    AGameModeBase* GameMode = GetAuthGameMode();
    // Set the next travel type to use
    NextTravelType = bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative;
    if (NextURL.IsEmpty() && (!IsInSeamlessTravel() || bShouldSkipGameNotify))
    {
        NextURL = FURL;
        if (!bShouldSkipGameNotify)
        {
            //这里面会将NextURL置空!
            GameMode->ProcessServerTravel(FURL, bAbsolute);
        }
        else
        {  
            NextSwitchCountdown = 0;
        }
    }
    return true;
}

注意,ServerTravel和ClientTravel类似,如果没有执行到ProcessServerTravel,会在TickWorldTravel里根据NextTravelType和NextURL调用UEngine::Browse方法,这是非无缝切换。代码如下:

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
    // 处理服务端的travel
    if( !Context.World()->NextURL.IsEmpty() )
    {
        if( Context.World()->NextSwitchCountdown <= 0.f )
        {
            FString NextURL = Context.World()->NextURL;
            EBrowseReturnVal::Type Ret = Browse( Context, FURL(&Context.LastURL,*NextURL,(ETravelType)Context.World()->NextTravelType), Error );
            //...
        }
    }
}

再来看看ProcessServerTravel

void AGameModeBase::ProcessServerTravel(const FString& URL, bool bAbsolute)
{
    StartToLeaveMap(); //这里可以写一些切换前的逻辑
    UWorld* World = GetWorld();
    bool bSeamless = (bUseSeamlessTravel && GetWorld()->TimeSeconds < 172800.0f); // 172800 seconds == 48 hours
    //这里会通过RPC调用客户端的ClientTravel
    APlayerController* LocalPlayer = ProcessClientTravel(URLMod, NextMapGuid, bSeamless, bAbsolute);
    if (bSeamless)
    {  
        //无缝切换
        World->SeamlessTravel(World->NextURL, bAbsolute);
        World->NextURL = TEXT("");   //如果执行到这里,就是上面提到的,TickWorldTravel里就不会再Browse了!
    }
}

它通过调用ProcessClientTravel,先通知客户端进行切换(即针对Remote player调用ClientTravel,逻辑较为简单,不再展开),然后开始SeamlessTravel。

可以看到,不管是ClientTravel还是ServerTravel,都有无缝和非无缝两种方式,非无缝通过调用UEngine::Browse实现,而无缝通过调用SeamlessTravel实现。

2.3 SeamlessTravel

通过上面的代码也可以知道,不管是ClientTravel还是ServerTravel的无缝切换都调用了SeamlessTravel。因为逻辑较为复杂,UE专门封装了一个FSeamlessTravelHandler类来处理。其大致流程为:

具体的代码流程:

void UWorld::SeamlessTravel(const FString & SeamlessTravelURL, bool bAbsolute, FGuid MapPackageGuid)
{
    FURL NewURL(&GEngine->LastURLFromWorld(this), *SeamlessTravelURL, bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
    FSeamlessTravelHandler& SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld(this);
    //开始切换
    if (!SeamlessTravelHandler.StartTravel(this, NewURL, MapPackageGuid) && !SeamlessTravelHandler.IsInTransition())
    {
        //...
    }
}

在StartTravel中先检查是否有TransitionMap,如果没有就默认创建一个World作为过渡,最后开始异步加载TransitionMap资源。

bool FSeamlessTravelHandler::StartTravel(UWorld * InCurrentWorld, const FURL & InURL, const FGuid & InGuid)
{
    FWorldContext& Context = GEngine->GetWorldContextFromWorldChecked(InCurrentWorld);
    //缓存DestinationMap的URL,在Tick的时候会用到
   PendingTravelURL = InURL;
    bSwitchedToDefaultMap = false;
 
    FName DestinationMapName = FName(*PendingTravelURL.Map);
    if (DefaultMapFinalName == CurrentMapName || DefaultMapFinalName == DestinationMapName)
    {
        //如果此时已经在TransitionMap中了,直接开始加载DestinationMap即可
       StartLoadingDestination();
        return;
    }
    //获取ProjectSettings里配置的TransitionMap
    FString TransitionMap = GetDefault<UGameMapsSettings>()->TransitionMap.GetLongPackageName();
    if (TransitionMap.IsEmpty())
    {
        //如果没有配置TransitionMap,就默认创建一个的World给SeamlessTravelHandler
      SetHandlerLoadedData(NULL, UWorld::CreateWorld(EWorldType::PIE, false));
     }
    else
    {
        //异步加载TransitionMap
        LoadPackageAsync(TransitionMap,FLoadPackageAsyncDelegate::CreateRaw(this, &FSeamlessTravelHandler::SeamlessTravelLoadCallback),0,(CurrentWorld->WorldType == EWorldType::PIE ? PKG_PlayInEditor : PKG_None),Context.PIEInstance);
    }
    return true;
}

异步加载TransitionMap资源完毕,将World设置到SeamlessTravelHandler。

void FSeamlessTravelHandler::SeamlessTravelLoadCallback(const FName & PackageName, UPackage * LevelPackage, EAsyncLoadingResult::Type Result)
{
    if (IsInTransition())
    {
        UWorld* World = UWorld::FindWorldInPackage(LevelPackage);
        //将LoadWorld设置给SeamlessTravelHandler
        SetHandlerLoadedData(LevelPackage, World);
    }
}

在Tick中完成CurrentMap切换到TransitionMap,完成之后开始异步加载DestinationMap

void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
    // 处理无缝travel.
    if (Context.SeamlessTravelHandler.IsInTransition())
    {
        Context.SeamlessTravelHandler.Tick();
    }
}
//这里执行CurrentMap -> TransitionMap,TransitionMap -> DestinationMap的处理
 UWorld* FSeamlessTravelHandler::Tick()
{
    if ((LoadedPackage != nullptr || LoadedWorld != nullptr) && CurrentWorld->NextURL == TEXT(""))
    {
        // 一些常规判断,比如是否正在加载中?是否加载完的World和当前World是同一个?
        
        //保存通过AGameModeBase::GetSeamlessTravelActorList和APlayerController::GetSeamlessTravelActorList处理的Actor,将它们带到下一个地图去
        TArray<AActor*> KeepActors;
        if (AGameModeBase* AuthGameMode = CurrentWorld->GetAuthGameMode())
        {
            AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
        }
        for (FLocalPlayerIterator It(GEngine, CurrentWorld); It; ++It)
        {
            if (It->PlayerController != nullptr)
            {
                It->PlayerController->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
            }
        }
        //用于存放实际需要保存到下一个地图的actor
        TArray<AActor*> ActuallyKeptActors;
     
        //匿名函数,将需要保存到下一个Map的actor放到ActuallyKeptActors,其他的删掉
        auto ProcessActor = [this, &KeepAnnotation, &ActuallyKeptActors, NetDriver](AActor* TheActor) -> bool
        {
            //...
        };
        // 开始清理当前World
        CurrentWorld->CleanupWorld(bSwitchedToDefaultMap);
 
        // 设置当前World
        FWorldContext& CurrentContext = GEngine->GetWorldContextFromWorldChecked(CurrentWorld);
        CurrentContext.SetCurrentWorld(LoadedWorld);
        CurrentWorld = nullptr;
 
        //给当前加载的World设置GameMode
        if (bCreateNewGameMode)
        {
            LoadedWorld->SetGameMode(PendingTravelURL);
        }
 
        if (bSwitchedToDefaultMap)
        {
            // remember the last used URL
            CurrentContext.LastURL = PendingTravelURL;
        }
        else
        {
            bSwitchedToDefaultMap = true;
            CurrentWorld = LoadedWorld;
          //开始加载DestinationMap
            StartLoadingDestination();
        }
    }  
    return OutWorld;
}

异步加载DestinationMap,加载完后同样调用SeamlessTravelLoadCallback,然后在Tick中完成从TransitionMap切换到DestinationMap(代码同上)。

void FSeamlessTravelHandler::StartLoadingDestination()
{
    if (bTransitionInProgress && bSwitchedToDefaultMap)
    {  
        //加载DestinationMap
        LoadPackageAsync(URLMapPackageName,PendingTravelGuid.IsValid() ? &PendingTravelGuid : NULL,*URLMapPackageToLoadFrom,FLoadPackageAsyncDelegate::CreateRaw(this, &FSeamlessTravelHandler::SeamlessTravelLoadCallback),
            PackageFlags,PIEInstanceID);
    }
}

地图无缝切换的流程图

3 使用注意事项

1.使用ClientTravel时,TravelType一定要为TRAVEL_Relative,并且bSeamless一定要为true

2. ServerTravel在编辑器运行模式下运行是不生效的,具体代码如下:

bool AGameModeBase::CanServerTravel(const FString& FURL, bool bAbsolute)
{
    if (World->WorldType == EWorldType::PIE && bUseSeamlessTravel && !FParse::Param(FCommandLine::Get(), TEXT("MultiprocessOSS")))
    {  
        //在PIE模式下,返回false
        return false;
    }
    return true;
}

3.在travel到新地图后,由于会清理当前world后生成新的World,因此如果有想要保留到下一个World的actor,需要手动添加,具体做法是重载下面两个接口:

//在服务端上执行
virtual void GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList);
//在客户端上执行
virtual void GetSeamlessTravelActorList(bool bToEntry, TArray<class AActor*>& ActorList);

注意,这里有两个过程,如果bToTransition/bToEntry为true是指保存到Transition的world,而为false是保存到Destination的World。

4.Browse是非无缝切换,它不但会同步加载地图资源(参考关卡切换流程),而且还会断开网络连接,具体代码:

EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
    //在LoadMap会干掉NetDriver!
    LoadMap(URL);
}
 
bool UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
  // Clean up networking
    ShutdownWorldNetDriver(WorldContext.World());
}

4 总结

本文简单的介绍了地图无缝切换的使用,然后从无缝的两个入口ClientTravel、ServerTravel入手,详细介绍了SeamlessTravel的代码实现。其基本原理为用异步加载的方式分别加载TransitionMap和DestinationMap,然后在FSeamlessTravelHandler::Tick中完成CurrentMap到TransitionMap和TransistionMaap到DestinationMap的切换。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值