因为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的代码,其调用栈为:
// 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); } }
// 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;
// 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); returntrue; } 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()); }
// otherwise, set to be deleted KeepAnnotation.Clear(TheActor); // close any channels for this actor if (NetDriver != nullptr) { NetDriver->NotifyActorLevelUnloaded(TheActor); } returnfalse; } };