跨关卡传递 Actor 实例

因为 UnrealEngine 在切换关卡 (OpenLevel) 时会把当前关卡的所有对象全部销毁,但是常常我们需要保存某些对象到下一关卡中,今天读了一下相关的代码,本篇文章讲一下如何来实现。
其实 Unreal 的文档是有说明的(Travelling in Multiplayer),实现起来也并不麻烦,但是 UE 文档的一贯风格是资料是不详细的,中文资料更是十分匮乏(多是机翻,而且版本很老),在搜索中也没有查到相关的靠谱的东西,我自己在读代码实现的过程中就随手记了一下,就当做笔记了。

UE 在 C++ 中提供了这些功能,需要在 GameMode 中开启 bUseSeamlessTravel=true, 然后使用GetSeamlessTravelActorList 来获取需要保存的 Actor 列表的。
但是 ,请注意,直接使用UGameplayStatics::OpenLevel 是不行的,因为 OpenLevel 调用的是 GEngine->SetClientTravel(World,*Cmd,TravelType),所以不会执行AGameMode::GetSeamlessTravelActorList 去获取要留存到下一关卡的 Actor。
在 UE 文档的 Travelling in Multiplayer 中的 Persisting Actors across Seamless Travel 有写到只有 ServerOnly 的 GameMode 才会调用 AGameModeAGameMode::GetSeamlessTravelActorList,所以要使用UWorld::ServerTravel 来进行关卡切换。但 UE 并没有把 UWorld::ServerTravel 暴露给蓝图,所以我在测试代码中加了个暴露给蓝图的包裹函数 ACppGameMode::Z_ServerTravel,AGameMode::GetSeamlessTravelActorList 也同理,也有一个暴露给蓝图的包裹函数 ACppGameMode::GetSaveToNextLevelActors
读了一下 UWorld::ServerTravel 的代码,其调用栈为:

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

最终保留 Actor 的操作是在 FSeamlessTravelHandler::Tick 中做的,相关代码如下(前后均有省略):

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;
}
};

// ......
}

下面是我写的测试用的 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);
}

然后就可以在继承自 ACppGameMode 的 Blueprint 中 Override Function 里重写 GetSaveToNextLevelActors 来在蓝图中指定哪些 Actor 可以保留到下一关卡。
记得一定要在原始关卡 (切换之前的关卡) 中选择继承并实现了 GetSaveToNextLevelActorsGameMode


最终就可以在蓝图中使用 Z_ServerTravel 来替代 OpenLevel 来切换关卡了(上图),从而实现在 UE 中切换关卡时传递 Actor 到目标关卡。

相关链接: