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