Ue4_无缝关卡切换与切换过程中游戏框架类生命周期

6

主题

8

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2023-1-17 14:16:20 | 显示全部楼层
大家好,我是iAnimer,一个入门UE4客户端开发,这篇笔记主要记录在无缝切换关卡需求中产生的相关问题
正文如下
注:引擎版本4.26
一.问题的产生

在做某个跨关卡需求时,部分逻辑功能放在PlayerControl中BeginPlay中用于初始化,在实现关卡无缝切换过程中,进入关卡后PlayerController的BeginPlay 函数并未调用?
经调式断点发现,无缝切换关卡过程中PlayerController 销毁生成分两种情况
情况一:如前后关卡中GameMode下PlayerController 配置相同时,上个关卡中PlayerController 不会进行销毁。
情况二:GameMode 下PlayerController 配置不同时,PlayerController重新生成。

笔记主要记录哪些问题?
1.如何设置跨关卡方式 为 Seamless Travel ?
2.无缝跨关卡中 CurrentMap - TransitionMap- DestinationMap?
3.Ue4 无缝切换流程Seamless Travel 函数调用流程 ?
4.无缝切换流程Seamless Travel如何实现服务器不与客户端断开连接?
5.关卡切换中,如何实现服务器将客户端带入DestinationMap?
6.切换关卡过程中,常见游戏框架类生命周期,何时GC,何时生成
7.PlayerController GameState PlayerState  GameMode Character Hud 生成顺序

1.如何设置关卡方式为Seamless Travel

class AGameModeBase
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=GameMode)
        uint32 bUseSeamlessTravel : 1;


GameMode

2.关卡切换中 CurrentMap(当前关卡) - TransitionMap (过渡关卡) - DestinationMap(目标关卡)  ?

    进行无缝关卡过渡的过程中,会产生一个临时过渡关卡为TransitionMap ,从CurrentMap先过渡到TransitionMap 中,再从TransitionMap 中到DestinationMap中。
3.切换关卡中函数调用顺序

函数调用主要如下几个过程
3.1 GetWorld()->ServerTravel()
3.2 AGameModeBase::ProcessServerTravel()
3.3 UWorld::SeamlessTravel()
3.4 SeamlessTravelHandler.StartTravel
3.5 FSeamlessTravelHandler::Tick()  

3.1 GetWorld()->ServerTravel()
//FURL 为切换的关卡名
//bShouldSkipGameNotify 是否跳过通知客户端
UWorld::ServerTravel(const FString& FURL, bool bAbsolute, bool bShouldSkipGameNotify)
{
   GameMode->ProcessServerTravel(FURL, bAbsolute);
}
3.2 AGameModeBase::ProcessServerTravel()
//通过RPC方式通知客户端Travel
void AGameModeBase::ProcessServerTravel(const FString& URL, bool bAbsolute)
{
    //获取NextURL NextMapGuid
    FURL NextURL = FURL(&WorldContext.LastURL, *URL, bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
    FGuid NextMapGuid = UEngine::GetPackageGuid(FName(*NextURL.Map), GetWorld()->IsPlayInEditor());
    // Notify clients we're switching level and give them time to receive.
    FString URLMod = NextURL.ToString();
    APlayerController* LocalPlayer = ProcessClientTravel(URLMod, NextMapGuid, bSeamless, bAbsolute);
    if (bSeamless)
    {
        World->SeamlessTravel(World->NextURL, bAbsolute);
     }
}
3.3 UWorld::SeamlessTravel()
void UWorld::SeamlessTravel(const FString& SeamlessTravelURL, bool bAbsolute, FGuid MapPackageGuid)
{
    FSeamlessTravelHandler &SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld( this );
    SeamlessTravelHandler.StartTravel(this, NewURL, MapPackageGuid)
}
3.4 FSeamlessTravelHandler::StartTravel()
//修改 PendingTravelURL / bTransitionInProgress /CurrentWorld  创建临时Dummy world
//bTransitionInProgress = true 后 进入TickWorldTravel 中 Context.SeamlessTravelHandler.Tick();
bool FSeamlessTravelHandler::StartTravel(UWorld* InCurrentWorld, const FURL& InURL, const FGuid& InGuid)
{
   //设置CurrentWorld
   CurrentWorld = InCurrentWorld;
   //设置PendingTravelURL  Guid
   PendingTravelURL = InURL;
   PendingTravelGuid = InGuid;
   //bTransitionInProgress  = true 当前为关卡过渡
   bSwitchedToDefaultMap = false;
   bTransitionInProgress = true;
   //通过GameMapsSettings 获取是否设置过渡关卡
   FString TransitionMap = GetDefault<UGameMapsSettings>()->TransitionMap.GetLongPackageName();
   //如果未设置生成一个dummy world,将临时生成dummy world,
   // LoadedWorld = dummy world
   SetHandlerLoadedData(NULL, UWorld::CreateWorld(EWorldType::None, false));
}
3.5 FSeamlessTravelHandler::Tick()
UEngine::TickWorldTravel()
void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
{
        //Tick 每帧进行检测
        if (Context.SeamlessTravelHandler.IsInTransition())
        {
                //StartTravel 中 设置bTransitionInProgress = true
                // Note: SeamlessTravelHandler.Tick may automatically update Context.World and GWorld internally
                Context.SeamlessTravelHandler.Tick();
        }
}
UWorld* FSeamlessTravelHandler::Tick()  
   主要处理 CurrentMap- TransitionMap  与 TransitionMap -DestinationMap 两个阶段  
   第一阶段  CurrentMap- TransitionMap过程
UWorld* FSeamlessTravelHandler::Tick()
{
    //第一阶段 CurrentMap - TransMap
    //在StartTravel中 设置LoadedWorld 为临时创建的dummyWorld
    //第二阶段 TransMap - DesMap
    //在SeamlessTravelLoadCallback 回调函数中设置 LoadedWorld
    if ( ( LoadedPackage != nullptr || LoadedWorld != nullptr ))
    {
      //服务器保留Actor方法
      //CurrentMap - TransMap 中//保留GameMode GameState PlayerState GameSession
      AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
      //客户端保留Actor方法
      //CurrentMap - TransMap 中保留 Hud 与PlayerCameraManager
      It->PlayerController->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
      //在CurrentMap - TransMap过程中,拷贝当前的GameMode 与GameState 至LoadedWorld
      LoadedWorld->CopyGameState(KeptGameMode, KeptGameState);
      //拷贝CurrentWorld中的netDriver 到LoadedWorld 中
      //SeamlessTravel 服务器与客户端不会断开
      CopyWorldData();
      //接下来进行垃圾回收
     CurrentWorld->RemoveFromRoot();
     //将LoadedWorld AddToRoot防止垃圾回收
     LoadedWorld->AddToRoot();
     //设置当前world为LoadedWorld
     CurrentContext.SetCurrentWorld(LoadedWorld);
     //进行垃圾回收
     CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS, true );

    //第一阶段中 CurrentMap - TransMap
    bSwitchedToDefaultMap = true;
    CurrentWorld = LoadedWorld;
    //在StartLoadingDestination()方法中设置加载完成回调
    FLoadPackageAsyncDelegate::CreateRaw(this, &FSeamlessTravelHandler::SeamlessTravelLoadCallback)
    //将LoadedPackage 与 LoadedWorld 置为Nullptr
    LoadedPackage = nullptr;
    LoadedWorld = nullptr;
    //Tick中每次检测LoadedWorld ,
    //直至在DesMap加载完成回调函数中SeamlessTravelLoadCallback 设置 LoadedWorld
    }
}
第二阶段 TransitionMap- DesMap过程
   该过程发生在SeamlessTravelLoadCallback后
   此时CurrentWorld为TransitionMap
UWorld* FSeamlessTravelHandler::Tick()
{
    //在TransMap中到DesMap 过程中
    //在TransMap - DesMap 过程中 KeepActors 仅记录GameState下保存的PlayerState
    AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
    //即TransMap - DesMap 过程中 客户端保留Actor不发生变化
    //如不进行重写,默认为Hud 与PlayerCameraManager
    It->PlayerController->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors)
    for (AActor* const TheActor : ActuallyKeptActors)
    {
         //会看到如下LoadedWorld添加被保留的PlayerControl
         LoadedWorld->AddController(static_cast<AController*>(TheActor));
         //在GameState 与 GameMode在TransMap过程中不进行保留
         KeptGameMode = static_cast<AGameModeBase*>(TheActor);
         KeptGameState = static_cast<AGameStateBase*>(TheActor);
        //CurrentMap- TransMap 与 TransMap到DesMap过程中 不同的是
        //GameState 与GameMode 在CurrentMap到TransMap过程中保留,并通过CopyGameState方法
        //进行GameState 与GameMode拷贝
        if (KeptGameMode)
        {
                LoadedWorld->CopyGameState(KeptGameMode, KeptGameState);
                bCreateNewGameMode = false;
        }
     }
      //该过程两个阶段不会发生变化,仍然是拷贝NetDriver
      CopyWorldData();
      //垃圾回收部分
      CurrentWorld->RemoveFromRoot();
      CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS, true );
      //在InitWorld方法中
      //初始化Subsystems /CreatePhysicsScene/CreateAISystem /InitializeRenderingResources ...
      LoadedWorld->InitWorld();
      //如GameMode未进行保留则重新创建GameMode
      if (bCreateNewGameMode)
      {
           LoadedWorld->SetGameMode(PendingTravelURL);
       }
}
4.无缝切换流程Seamless Travel如何实现服务器不与客户端断开连接?

在上述跨关卡流程中 UWorld* FSeamlessTravelHandler::Tick() 中 CopyWorldData() 进行NetDriver拷贝
5.关卡切换中,如何实现服务器将客户端带入DestinationMap?

通过RPC方式通知客户端调用 SeamlessTravel
void AGameModeBase::ProcessServerTravel(const FString& URL, bool bAbsolute)
{
    APlayerController* LocalPlayer = ProcessClientTravel(URLMod, NextMapGuid, bSeamless, bAbsolute);
}
通过RPC使客户端进行关卡切换 ,服务器 存在一个 客户端的UNetConnection 对象
//通关判断PlayerController 下Player,判断当前为Remote 或localplayer
//关于PlayerController 下的Player
//UPlayer associated with this PlayerController.  Could be a local player or a net connection. */
APlayerController* AGameModeBase::ProcessClientTravel(FString& FURL, FGuid NextMapGuid, bool bSeamless, bool bAbsolute)
{
    if (APlayerController* PlayerController = Iterator->Get())
    {
        if (Cast<UNetConnection>(PlayerController->Player) != nullptr)
        {
           // Remote player
           PlayerController->ClientTravel(FURL, TRAVEL_Relative, bSeamless, NextMapGuid);
        }
        else
        {
           // Local player
           LocalPlayerController = PlayerController;
           PlayerController->PreClientTravel(FURL, bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative, bSeamless);
        }
      }
}
客户端进行调用ClientTravelInternal
void APlayerController::ClientTravel(const FString& URL, ETravelType TravelType, bool bSeamless, FGuid MapPackageGuid)
{
    ClientTravelInternal(URL, TravelType, bSeamless, MapPackageGuid);
}

void APlayerController::ClientTravelInternal_Implementation(const FString& URL, ETravelType TravelType, bool bSeamless, FGuid MapPackageGuid)
{
    UWorld* World = GetWorld();

        // Warn the client.
        PreClientTravel(URL, TravelType, bSeamless);
        if (bSeamless && TravelType == TRAVEL_Relative)
        {
                World->SeamlessTravel(URL);
        }
}

二.切换关卡过程中,常见游戏框架类生命周期,何时GC,何时生成,生成顺序等等?


1.PlayerController

PlayerController 在CurrentMap - TransitionMap 过程中被保留
CurrentMap - TransitionMap 过程中通过GameMode下的PlayerController进行判断
如果不同,主动Destory当前PlayerController,重新生成PlayerController
相同,则PlayerController保留至下一个关卡中
//Current - TransMap 过程中 保留至ActuallyKeptActors 列表中
//TransMap - 过程中会PC->GetClass() != PCClassToSpawn 判断,前后Map中的GameMode下PlayerController是否相同
//相同则不创建,不相同则旧PlayerControl销毁,生成新的PlayerController
UWorld* FSeamlessTravelHandler::Tick()
{
    for( FConstControllerIterator Iterator = CurrentWorld->GetControllerIterator(); Iterator; ++Iterator )
    {
        KeepAnnotation.Set(Player);
    }
    for (FConstPlayerControllerIterator Iterator = CurrentWorld->GetPlayerControllerIterator(); Iterator; ++Iterator)
    {
       if (APlayerController* Player = Iterator->Get())
       {
            ProcessActor(Player);
       }
    }
}

void AGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
    //判断前后关卡GameMode下的PlayerController是否相同
    //如不同,则重新创建
    if (PC && PC->GetClass() != PCClassToSpawn)
    {
        APlayerController* const NewPC = SpawnPlayerControllerCommon(PC->IsLocalPlayerController() ? ROLE_SimulatedProxy : ROLE_AutonomousProxy, PC->GetFocalLocation(), PC->GetControlRotation(), PCClassToSpawn);
        PC->SeamlessTravelTo(NewPC);
        NewPC->SeamlessTravelFrom(PC);
        SwapPlayerControllers(PC, NewPC);
        PC = NewPC;
        C = NewPC;
    }
    //CurrentMap与DesMap GameMode 下关卡不同时
    //该过程不进行PlayerController 重新生成
    else
    {
       //该过程将PlayerState 进行Reset
       //首先保留一份OldPlayerState
       C->PlayerState->Reset();
       // create a new PlayerState and copy over info; this is necessary because the old GameMode may have used a different PlayerState class
       //创建一个新的PlayerState 进行函数重写进行数据拷贝
       APlayerState* OldPlayerState = C->PlayerState;
       C->InitPlayerState()
       OldPlayerState->Destroy();
    }
}


void AController::InitPlayerState()
{
    //创建PlayerState
    PlayerState = World->SpawnActor<APlayerState>(PlayerStateClassToSpawn, SpawnInfo);
}
2.PlayerState 生成/销毁

CurrentMap  - TransMap 中过程中 保留Actor方法进行PlayerState 保留
TransMap - DesMap 中,主动销毁并重新创建PlayerState
//PlayerState
//Current - TransMap 与TransMap - DesMap PlayerState 通过 GetSeamlessTravelActorList 保留
//Current - TransMap 过程中 PlayerState保留
//TransMap - DesMap 过程中 PlayerState 主动进行Destory销毁并重新创建
UWorld* FSeamlessTravelHandler::Tick()
{
    AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
    //仅在TransMap - DesMap 过程中执行以下逻辑
    if (bSwitchedToDefaultMap)
    {
        AGameModeBase* const GameMode = LoadedWorld->GetAuthGameMode();
        if (GameMode)
        {
            // inform the new GameMode so it can handle players that persisted
            GameMode->PostSeamlessTravel();                                       
        }
    }
}
//通过GameState保留
// 这里可能有一个疑问GameState在Trans - DesMap 过程会被垃圾回收处理,如何将PlayerState保留
// 调用顺序为 先调用GetSeamlessTravelActorList 先PlayerState加入后保留Actor列表
// 后调用 CollectGarbage ,GameState被GC
void AGameModeBase::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
    ActorList.Append(GameState->PlayerArray);
}
//通过PostSeamlessTravel 进行销毁并重新生成
void AGameModeBase::PostSeamlessTravel()
{
    for (auto It = GetWorld()->GetControllerIterator(); It; ++It)
    {
        OldControllerList.Add(It->Get());
    }
    HandleSeamlessTravelPlayer(Controller);
}

void AGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
   if(PC && PC->GetClass() != PCClassToSpawn)
   {
        NewPC->SeamlessTravelFrom(PC);
    }
    else
    {
         C->InitPlayerState();
         OldPlayerState->Destroy();
    }
}

void APlayerController::SeamlessTravelFrom(APlayerController* OldPC)
{
    OldPC->PlayerState->Destroy();
    OldPC->PlayerState = NULL;
}
3.GameMode 生成/销毁

GameMode 生成与销毁主要与GetSeamlessTravelActorList 方法有关
默认情况下Current -TransMap 该过程GameMode保留,并CopyGameState 拷贝至TransMap 中
TransMap - DesMap过程中,GameMode 不进行保留,被GC销毁,销毁后重新创建GameMode
//CurrentMap - TransMap
//GetSeamlessTravelActorList 保留至LoadedWorld ,并设置LoadedWorld 中的GameState 与GameMode
//该过程GameMode 不会销毁

//关于GameMode生成与销毁主要受如下
//在GetSeamlessTravelActorList 中是否将GameMode 保留
//如GameMode保留,则GameMode不重新创建
//在默认情况下 CurrentMap - TransMap 过程中进行GameMode保留仅CopyGameState
//TransMap - DesMap 过程中 GameMode不进行保留 ,通过TheActor->IsA<AGameModeBase>() 方法判断
//当GameMode为空后,bCreateNewGameMode True, 后重新创建GameMode
UWorld* FSeamlessTravelHandler::Tick()
{
    AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
   
    bool bCreateNewGameMode = !bIsClient;
    for (AActor* const TheActor : ActuallyKeptActors)
    {
        else if (TheActor->IsA<AGameModeBase>())
        {
            KeptGameMode = static_cast<AGameModeBase*>(TheActor);
        }
     }
    if (KeptGameMode)
    {
        LoadedWorld->CopyGameState(KeptGameMode, KeptGameState);
        bCreateNewGameMode = false;
    }
   
    if (bCreateNewGameMode)
    {
        LoadedWorld->SetGameMode(PendingTravelURL);
    }
}
4.GameState 生成/销毁

CurrentMap - TransMap 过程中,通过GetSeamlessTravelActorList 方法保留 并调用CopyGameState
TransMap - DesMap 过程中 ,GameState 被GC后,重新进行Spawn Actor
//CurrentMap - TransMap 过程中
//通过该方法进行保留至TransMap中
AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
//TransMap - DesMap过程中
//当发起GC的过程中
CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS, true );
GameState类进行销毁

//Trans -DesMap 中 GameState 生成过程
//UWorld* FSeamlessTravelHandler::Tick()中
LoadedWorld->InitializeActorsForPlay(PendingTravelURL, false);
Level->RouteActorInitialize();
void AGameModeBase::PreInitializeComponents()
{
    GameState = World->SpawnActor<AGameStateBase>(GameStateClass, SpawnInfo);
}
5.HUD  生成/销毁

CurrentMap  - TransMap 过程中  通过PlayerController 下GetSeamlessTravelActorList 保留至TransMap
如不想保留至TransMap 可对GetSeamlessTravelActorList 进行重写,该方法主要处理客户端的Actor 保留
在TransMap  - CurrentMap 过程中 ,Hud主动销毁,并重新创建
//CurrentMap - TransMap 过程中
//在默认情况,HUD 会被保留带入TransMap
It->PlayerController->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
//第二阶段 TransMap - DesMap 过程中
//函数调用顺序
void AGameMode::PostSeamlessTravel()
void AGameMode::HandleSeamlessTravelPlayer(AController*& C)
virtual void GenericPlayerInitialization(AController* C);
void InitializeHUDForPlayer(APlayerController* NewPlayer);
void ClientSetHUD(TSubclassOf<AHUD> NewHUDClass);
//在PlayerController 中ClientSetHUD 实现可以看到如下
void APlayerController::ClientSetHUD_Implementation(TSubclassOf<AHUD> NewHUDClass)
{
    //Hud 在TransMap 保留至DesMap
    //在 GetSeamlessTravelActorList方法中,是否保留至DesMap中
    //Hud都会被销毁,重新生成Hud类
    if ( MyHUD != NULL )
    {
        MyHUD->Destroy();
        MyHUD = NULL;
    }
        FActorSpawnParameters SpawnInfo;
        SpawnInfo.Owner = this;
        SpawnInfo.Instigator = GetInstigator();
        SpawnInfo.ObjectFlags |= RF_Transient;        // We never want to save HUDs into a map
        MyHUD = GetWorld()->SpawnActor<AHUD>(NewHUDClass, SpawnInfo );
}
6.PlayerCharacter 生成/销毁

PlayerCharacter 在CurrentMap - TransMap 过程中 被GC 销毁
在TransMap - DesMap 过程中 在生成HUD后生成PlayerCharacter
//Current - TransMap
//PlayerCharacter 被GC 回收
//TransMap  - DesMap 重新生成
void AGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
   //首先先生成Hud
    GenericPlayerInitialization(C)
    //生成CharacterPlayer
    HandleStartingNewPlayer(PC);
}
void AGameModeBase::RestartPlayerAtPlayerStart(AController* NewPlayer, AActor* StartSpot)
{
    NewPlayer->SetPawn(SpawnDefaultPawnFor(NewPlayer, StartSpot));
}
7.AGameSession 生成与销毁

第一阶段 CurrentMap  - TransMap 中
//通过AuthGameMode->GetSeamlessTravelActorList 保留至TransMap中
第二阶段 TransMap  - DesMap 过程中 在垃圾回收后进行销毁并重新创建
//GameSession 类创建
//在FSeamlessTravelHandler::Tick()
LoadedWorld->InitializeActorsForPlay(PendingTravelURL, false);
AuthorityGameMode->InitGame
//GameSession Spawn过程
void AGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
    GameSession = World->SpawnActor<AGameSession>(GetGameSessionClass(), SpawnInfo);
}
8.GameInstance

仅拷贝GameInstance指针
void FSeamlessTravelHandler::CopyWorldData()
{
    LoadedWorld->SetGameInstance(CurrentWorld->GetGameInstance());
}
9.PlayerController GameState PlayerState  GameMode Character Hud 默认生成顺序?

注: TransMap - DesMap过程
按照函数调用顺序GameMode  GameState PLayerController PlayerState HUD PlayerCharacter 依次生成
//相关类生成顺序
UWorld* FSeamlessTravelHandler::Tick()
{
    //GameMode 生成
    if (bCreateNewGameMode)
    {
        LoadedWorld->SetGameMode(PendingTravelURL);
    }
    //GameState 生成
     LoadedWorld->InitializeActorsForPlay(PendingTravelURL, false);
    //生成PlayerController PlayerState HUD  PlayerCharacter
    GameMode->PostSeamlessTravel();               
   
}
三.参考文档:

UE4 Server无缝地图切换_南京周润发
流关卡与无缝地图切换_Jerish
多人游戏中的关卡切换 | 虚幻引擎文档 (unrealengine.com)
回复

举报 使用道具

0

主题

4

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-1-17 14:16:56 | 显示全部楼层
很棒!
回复

举报 使用道具

1

主题

4

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-1-17 14:17:32 | 显示全部楼层
谢谢!
回复

举报 使用道具

您需要登录后才可以回帖 登录 | 立即注册
快速回复 返回顶部 返回列表