Pass Actor To Next Level Through Seamless Travel

因为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到目标关卡。

相关链接:

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:Pass Actor To Next Level Through Seamless Travel
文章作者:ZhaLiPeng
发布时间:2018年04月28日 00时43分
本文字数:本文一共有942字
更新历史: Blame, History  文本模式: .md Raw
原始链接:https://imzlp.me/posts/19376/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!