Pass Actor To Next Level Through Seamless Travel

UE无缝地图:传递Actor到下个关卡

Because Unreal Engine destroys all objects in the current level when switching levels (OpenLevel), we often need to retain certain objects for the next level. Today, I read some relevant code, and this article will explain how to achieve this. Unreal’s documentation does mention this (Travelling in Multiplayer), and it’s not too complicated to implement. However, the UE documentation is consistently lacking in detail, especially in Chinese, where resources are very scarce (mostly machine-translated and outdated). I couldn’t find any reliable information during my search, so I took some notes while reading the code implementation.

UE provides these functionalities in C++. You need to enable bUseSeamlessTravel=true in GameMode, and then use GetSeamlessTravelActorList to obtain the list of Actors to be saved. However, please note that directly using UGameplayStatics::OpenLevel will not work because OpenLevel calls GEngine->SetClientTravel(World,*Cmd,TravelType), so it won’t execute AGameMode::GetSeamlessTravelActorList to get the Actors that need to persist to the next level. In the UE documentation’s Travelling in Multiplayer, under Persisting Actors across Seamless Travel, it mentions that only ServerOnly GameModes will call AGameMode::GetSeamlessTravelActorList, so you should use UWorld::ServerTravel for level switching. However, UE does not expose UWorld::ServerTravel to Blueprints, so I added a wrapping function exposed to Blueprints in the test code called ACppGameMode::Z_ServerTravel, and similarly, there is a wrapping function ACppGameMode::GetSaveToNextLevelActors for AGameMode::GetSeamlessTravelActorList.

After reading the code for UWorld::ServerTravel, its call stack is:

1
UWorld::ServerTravel -> AGameModeBase::ProcessServerTravel -> UWorld::SeamlessTravel -> SeamlessTravelHandler::Tick

Ultimately, the operation to retain Actors is done in FSeamlessTravelHandler::Tick. The relevant code is as follows (with some parts omitted):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
UWorld* FSeamlessTravelHandler::Tick()
{
// ......
// mark actors we want to keep
FUObjectAnnotationSparseBool KeepAnnotation;
TArray<AActor*> KeepActors;

if (AGameModeBase* AuthGameMode = CurrentWorld->GetAuthGameMode())
{
AuthGameMode->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
}

const bool bIsClient = (CurrentWorld->GetNetMode() == NM_Client);

// always keep Controllers that belong to players
if (bIsClient)
{
for (FLocalPlayerIterator It(GEngine, CurrentWorld); It; ++It)
{
if (It->PlayerController != nullptr)
{
KeepAnnotation.Set(It->PlayerController);
}
}
}
else
{
for( FConstControllerIterator Iterator = CurrentWorld->GetControllerIterator(); Iterator; ++Iterator )
{
AController* Player = Iterator->Get();
if (Player->PlayerState || Cast<APlayerController>(Player) != nullptr)
{
KeepAnnotation.Set(Player);
}
}
}

// ask players what else we should keep
for (FLocalPlayerIterator It(GEngine, CurrentWorld); It; ++It)
{
if (It->PlayerController != nullptr)
{
It->PlayerController->GetSeamlessTravelActorList(!bSwitchedToDefaultMap, KeepActors);
}
}
// mark all valid actors specified
for (AActor* KeepActor : KeepActors)
{
if (KeepActor != nullptr)
{
KeepAnnotation.Set(KeepActor);
}
}

TArray<AActor*> ActuallyKeptActors;
ActuallyKeptActors.Reserve(KeepAnnotation.Num());

// Rename dynamic actors in the old world's PersistentLevel that we want to keep into the new world
auto ProcessActor = [this, &KeepAnnotation, &ActuallyKeptActors, NetDriver](AActor* TheActor) -> bool
{
const FNetworkObjectInfo* NetworkObjectInfo = NetDriver ? NetDriver->GetNetworkObjectInfo(TheActor) : nullptr;

const bool bIsInCurrentLevel = TheActor->GetLevel() == CurrentWorld->PersistentLevel;
const bool bManuallyMarkedKeep = KeepAnnotation.Get(TheActor);
const bool bDormant = NetworkObjectInfo && NetDriver && NetDriver->ServerConnection && NetworkObjectInfo->DormantConnections.Contains(NetDriver->ServerConnection);
const bool bKeepNonOwnedActor = TheActor->Role < ROLE_Authority && !bDormant && !TheActor->IsNetStartupActor();
const bool bForceExcludeActor = TheActor->IsA(ALevelScriptActor::StaticClass());

// Keep if it's in the current level AND it isn't specifically excluded AND it was either marked as should keep OR we don't own this actor
if (bIsInCurrentLevel && !bForceExcludeActor && (bManuallyMarkedKeep || bKeepNonOwnedActor))
{
ActuallyKeptActors.Add(TheActor);
return true;
}
else
{
if (bManuallyMarkedKeep)
{
UE_LOG(LogWorld, Warning, TEXT("Actor '%s' was indicated to be kept but exists in level '%s', not the persistent level. Actor will not travel."), *TheActor->GetName(), *TheActor->GetLevel()->GetOutermost()->GetName());
}

TheActor->RouteEndPlay(EEndPlayReason::LevelTransition);

// otherwise, set to be deleted
KeepAnnotation.Clear(TheActor);
// close any channels for this actor
if (NetDriver != nullptr)
{
NetDriver->NotifyActorLevelUnloaded(TheActor);
}
return false;
}
};

// ......
}

Below is the code for my test GameMode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// CppGameMode.h
#pragma once

#include "CoreMinimal.h"
#include "UnrealString.h"
#include "Engine/World.h"
#include "Kismet/KismetSystemLibrary.h"
#include "GameFramework/GameMode.h"
#include "CppGameMode.generated.h"

UCLASS()
class ACppGameMode : public AGameMode
{
GENERATED_BODY()
ACppGameMode();
void GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList);
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable,Category="GameCore|GameMode|SeamlessTravel")
void GetSaveToNextLevelActors(TArray<AActor*>& ActorList);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable,Category="GameCore|GameMode|SeamlessTravel")
bool Z_ServerTravel(const FString& FURL, bool bAbsolute, bool bShouldSkipGameNotify);
};

// CppGameMode.cpp

#include "CppGameMode.h"

ACppGameMode::ACppGameMode()
{
bUseSeamlessTravel = true;
}

void ACppGameMode::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
GetSaveToNextLevelActors(ActorList);
}

void ACppGameMode::GetSaveToNextLevelActors_Implementation(TArray<AActor*>& ActorList)
{
UKismetSystemLibrary::PrintString(this,FString("ACppGameMode::GetSaveToNextLevelActors"),false,true);
}

bool ACppGameMode::Z_ServerTravel_Implementation(const FString& FURL, bool bAbsolute, bool bShouldSkipGameNotify)
{
UWorld* WorldObj = GetWorld();
return WorldObj->ServerTravel(FURL, bAbsolute, bShouldSkipGameNotify);
}

Then you can override the GetSaveToNextLevelActors function in the Blueprint inheriting from ACppGameMode to specify which Actors can be retained for the next level. Make sure to select the GameMode that inherits and implements GetSaveToNextLevelActors in the original level (the level to switch from).


Finally, you can use Z_ServerTravel in Blueprints to replace OpenLevel for level switching (as shown in the image above), thus achieving the transfer of Actors to the target level when switching levels in UE.

Related Links:

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Pass Actor To Next Level Through Seamless Travel
Author:LIPENGZHA
Publish Date:2018/04/28 00:43
World Count:2.1k Words
Link:https://en.imzlp.com/posts/19376/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!