因为 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 可以保留到下一关卡。
记得一定要在原始关卡 (切换之前的关卡) 中选择继承并实现了 GetSaveToNextLevelActors 的GameMode


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