// Copyright Epic Games, Inc. All Rights Reserved. #include "PlayLevel.h" #include "CoreMinimal.h" #include "Misc/MessageDialog.h" #include "Misc/CommandLine.h" #include "Misc/Paths.h" #include "Misc/Guid.h" #include "Stats/Stats.h" #include "GenericPlatform/GenericApplication.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" #include "UObject/ObjectMacros.h" #include "UObject/GarbageCollection.h" #include "UObject/Class.h" #include "UObject/UObjectIterator.h" #include "UObject/Package.h" #include "UObject/LazyObjectPtr.h" #include "UObject/SoftObjectPtr.h" #include "UObject/ReferenceChainSearch.h" #include "Misc/PackageName.h" #include "InputCoreTypes.h" #include "Layout/Margin.h" #include "Layout/SlateRect.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SOverlay.h" #include "Widgets/SWindow.h" #include "Layout/WidgetPath.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/SViewport.h" #include "Framework/Docking/TabManager.h" #include "EditorStyleSet.h" #include "Classes/EditorStyleSettings.h" #include "Engine/EngineTypes.h" #include "Async/TaskGraphInterfaces.h" #include "GameFramework/Actor.h" #include "Engine/Blueprint.h" #include "Engine/GameViewportClient.h" #include "Engine/GameInstance.h" #include "Engine/RendererSettings.h" #include "Engine/World.h" #include "Settings/LevelEditorPlaySettings.h" #include "AI/NavigationSystemBase.h" #include "Editor/EditorEngine.h" #include "Editor/UnrealEdEngine.h" #include "Settings/ProjectPackagingSettings.h" #include "GameMapsSettings.h" #include "GeneralProjectSettings.h" #include "Engine/NavigationObjectBase.h" #include "GameFramework/PlayerStart.h" #include "GameFramework/GameModeBase.h" #include "Components/AudioComponent.h" #include "Engine/Note.h" #include "Engine/Selection.h" #include "UnrealEngine.h" #include "EngineUtils.h" #include "Editor.h" #include "LevelEditorViewport.h" #include "EditorModeManager.h" #include "EditorModes.h" #include "UnrealEdMisc.h" #include "FileHelpers.h" #include "UnrealEdGlobals.h" #include "EditorAnalytics.h" #include "AudioDevice.h" #include "BusyCursor.h" #include "ScopedTransaction.h" #include "PackageTools.h" #include "Slate/SceneViewport.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/BlueprintEditorUtils.h" #include "LevelEditor.h" #include "IAssetViewport.h" #include "BlueprintEditorModule.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Interfaces/IMainFrameModule.h" #include "Logging/TokenizedMessage.h" #include "Logging/MessageLog.h" #include "Misc/UObjectToken.h" #include "Misc/MapErrors.h" #include "GameProjectGenerationModule.h" #include "SourceCodeNavigation.h" #include "Physics/PhysicsInterfaceCore.h" #include "AnalyticsEventAttribute.h" #include "Interfaces/IAnalyticsProvider.h" #include "EngineAnalytics.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Engine/LocalPlayer.h" #include "Slate/SGameLayerManager.h" #include "HAL/PlatformApplicationMisc.h" #include "Widgets/Input/SHyperlink.h" #include "Dialogs/CustomDialog.h" #include "IHeadMountedDisplay.h" #include "IXRTrackingSystem.h" #include "Engine/LevelStreaming.h" #include "Components/ModelComponent.h" #include "GameDelegates.h" #include "Net/OnlineEngineInterface.h" #include "Kismet2/DebuggerCommands.h" #include "Misc/ScopeExit.h" #include "IVREditorModule.h" #include "EditorModeRegistry.h" #include "PhysicsManipulationMode.h" #include "CookerSettings.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/SBoxPanel.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Async/Async.h" #include "StudioAnalytics.h" #include "UObject/SoftObjectPath.h" #include "IAssetViewport.h" #include "IPIEAuthorizer.h" #include "Features/IModularFeatures.h" DEFINE_LOG_CATEGORY(LogPlayLevel); #define LOCTEXT_NAMESPACE "PlayLevel" const static FName NAME_CategoryPIE("PIE"); // Forward declare local utility functions FText GeneratePIEViewportWindowTitle(const EPlayNetMode InNetMode, const ERHIFeatureLevel::Type InFeatureLevel, const FRequestPlaySessionParams& InSessionParams, const int32 ClientIndex, const float FixedTick); bool PromptMatineeClose(); // This class listens to output log messages, and forwards warnings and errors to the message log class FOutputLogErrorsToMessageLogProxy: public FOutputDevice { public: FOutputLogErrorsToMessageLogProxy() { GLog->AddOutputDevice(this); } ~FOutputLogErrorsToMessageLogProxy() { GLog->RemoveOutputDevice(this); } // FOutputDevice interface virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) override { //@TODO: Remove IsInGameThread() once the message log is thread safe if ((Verbosity <= ELogVerbosity::Warning) && IsInGameThread()) { const FText Message = FText::Format(LOCTEXT("OutputLogToMessageLog", "{0}: {1}"), FText::FromName(Category), FText::AsCultureInvariant(FString(V))); switch (Verbosity) { case ELogVerbosity::Warning: FMessageLog(NAME_CategoryPIE).SuppressLoggingToOutputLog(true).Warning(Message); break; case ELogVerbosity::Error: FMessageLog(NAME_CategoryPIE).SuppressLoggingToOutputLog(true).Error(Message); break; case ELogVerbosity::Fatal: FMessageLog(NAME_CategoryPIE).SuppressLoggingToOutputLog(true).CriticalError(Message); break; } } } // End of FOutputDevice interface }; void UEditorEngine::EndPlayMap() { if (bIsEndingPlay) { return; } TGuardValue GuardIsEndingPlay(bIsEndingPlay, true); FEditorDelegates::PrePIEEnded.Broadcast(bIsSimulatingInEditor); // Clean up Soft Object Path remaps FSoftObjectPath::ClearPIEPackageNames(); FlushAsyncLoading(); if (GEngine->XRSystem.IsValid() && !bIsSimulatingInEditor) { GEngine->XRSystem->OnEndPlay(*GEngine->GetWorldContextFromWorld(PlayWorld)); } // Matinee must be closed before PIE can stop - matinee during PIE will be editing a PIE-world actor if (GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit)) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "PIENeedsToCloseMatineeMessage", "Closing 'Play in Editor' must close UnrealMatinee.")); GLevelEditorModeTools().DeactivateMode(FBuiltinEditorModes::EM_InterpEdit); } EndPlayOnLocalPc(); const FScopedBusyCursor BusyCursor; check(PlayWorld); // Enable screensavers when ending PIE. EnableScreenSaver(true); // Make a list of all the actors that should be selected TArray SelectedActors; if (ActorsThatWereSelected.Num() > 0) { for (int32 ActorIndex = 0; ActorIndex < ActorsThatWereSelected.Num(); ++ActorIndex) { TWeakObjectPtr Actor = ActorsThatWereSelected[ActorIndex].Get(); if (Actor.IsValid()) { SelectedActors.Add(Actor.Get()); } } ActorsThatWereSelected.Empty(); } else { for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); if (Actor) { checkSlow(Actor->IsA(AActor::StaticClass())); AActor* EditorActor = EditorUtilities::GetEditorWorldCounterpartActor(Actor); if (EditorActor) { SelectedActors.Add(EditorActor); } } } } // Deselect all objects, to avoid problems caused by property windows still displaying // properties for an object that gets garbage collected during the PIE clean-up phase. GEditor->SelectNone(true, true, false); GetSelectedActors()->DeselectAll(); GetSelectedObjects()->DeselectAll(); GetSelectedComponents()->DeselectAll(); // For every actor that was selected previously, make sure it's editor equivalent is selected GEditor->GetSelectedActors()->BeginBatchSelectOperation(); for (int32 ActorIndex = 0; ActorIndex < SelectedActors.Num(); ++ActorIndex) { AActor* Actor = Cast(SelectedActors[ActorIndex]); if (Actor) { // We need to notify or else the manipulation transform widget won't appear, but only notify once at the end because OnEditorSelectionChanged is expensive for large groups. SelectActor(Actor, true, false); } } GEditor->GetSelectedActors()->EndBatchSelectOperation(true); // let the editor know FEditorDelegates::EndPIE.Broadcast(bIsSimulatingInEditor); // clean up any previous Play From Here sessions if (GameViewport != NULL && GameViewport->Viewport != NULL) { // Remove debugger commands handler binding GameViewport->OnGameViewportInputKey().Unbind(); // Remove close handler binding GameViewport->OnCloseRequested().Remove(ViewportCloseRequestedDelegateHandle); GameViewport->CloseRequested(GameViewport->Viewport); } CleanupGameViewport(); // Clean up each world individually TArray OnlineIdentifiers; TArray WorldsBeingCleanedUp; bool bSeamlessTravelActive = false; for (int32 WorldIdx = WorldList.Num() - 1; WorldIdx >= 0; --WorldIdx) { FWorldContext& ThisContext = WorldList[WorldIdx]; if (ThisContext.WorldType == EWorldType::PIE) { if (ThisContext.World()) { WorldsBeingCleanedUp.Add(ThisContext.World()); } if (ThisContext.SeamlessTravelHandler.IsInTransition()) { bSeamlessTravelActive = true; } if (ThisContext.World()) { TeardownPlaySession(ThisContext); ShutdownWorldNetDriver(ThisContext.World()); } // Cleanup online subsystems instantiated during PIE FName OnlineIdentifier = UOnlineEngineInterface::Get()->GetOnlineIdentifier(ThisContext); if (UOnlineEngineInterface::Get()->DoesInstanceExist(OnlineIdentifier)) { // Stop ticking and clean up, but do not destroy as we may be in a failed online delegate UOnlineEngineInterface::Get()->ShutdownOnlineSubsystem(OnlineIdentifier); OnlineIdentifiers.Add(OnlineIdentifier); } // Remove world list after online has shutdown in case any async actions require the world context WorldList.RemoveAt(WorldIdx); } } // If seamless travel is happening then there is likely additional PIE worlds that need tearing down so seek them out if (bSeamlessTravelActive) { for (TObjectIterator WorldIt; WorldIt; ++WorldIt) { if (WorldIt->IsPlayInEditor()) { WorldsBeingCleanedUp.AddUnique(*WorldIt); } } } if (OnlineIdentifiers.Num()) { UE_LOG(LogPlayLevel, Display, TEXT("Shutting down PIE online subsystems")); // Cleanup online subsystem shortly as we might be in a failed delegate // have to do this in batch because timer delegate doesn't recognize bound data // as a different delegate FTimerDelegate DestroyTimer; DestroyTimer.BindUObject(this, &UEditorEngine::CleanupPIEOnlineSessions, OnlineIdentifiers); GetTimerManager()->SetTimer(CleanupPIEOnlineSessionsTimerHandle, DestroyTimer, 0.1f, false); } { // We could have been toggling back and forth between simulate and pie before ending the play map // Make sure the property windows are cleared of any pie actors GUnrealEd->UpdateFloatingPropertyWindows(); // Clean up any pie actors being referenced GEngine->BroadcastLevelActorListChanged(); } // Lose the EditorWorld pointer (this is only maintained while PIEing) FNavigationSystem::OnPIEEnd(*EditorWorld); FGameDelegates::Get().GetEndPlayMapDelegate().Broadcast(); // find objects like Textures in the playworld levels that won't get garbage collected as they are marked RF_Standalone for (FThreadSafeObjectIterator It; It; ++It) { UObject* Object = *It; if (Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor)) { if (Object->HasAnyFlags(RF_Standalone)) { // Clear RF_Standalone flag from objects in the levels used for PIE so they get cleaned up. Object->ClearFlags(RF_Standalone); } // Close any asset editors that are currently editing this object GEditor->GetEditorSubsystem()->CloseAllEditorsForAsset(Object); } } EditorWorld->bAllowAudioPlayback = true; EditorWorld = nullptr; // mark everything contained in the PIE worlds to be deleted for (UWorld* World: WorldsBeingCleanedUp) { // Because of the seamless travel the world might still be in the root set, so clear that World->RemoveFromRoot(); // Occasionally during seamless travel the Levels array won't yet be populated so mark this world first // then pick up the sub-levels via the level iterator World->MarkObjectsPendingKill(); for (auto LevelIt(World->GetLevelIterator()); LevelIt; ++LevelIt) { if (const ULevel* Level = *LevelIt) { // We already picked up the persistent level with the top level mark objects if (Level->GetOuter() != World) { CastChecked(Level->GetOuter())->MarkObjectsPendingKill(); } } } for (ULevelStreaming* LevelStreaming: World->GetStreamingLevels()) { // If an unloaded levelstreaming still has a loaded level we need to mark its objects to be deleted as well if (LevelStreaming->GetLoadedLevel() && (!LevelStreaming->ShouldBeLoaded() || !LevelStreaming->ShouldBeVisible())) { CastChecked(LevelStreaming->GetLoadedLevel()->GetOuter())->MarkObjectsPendingKill(); } } } // mark all objects contained within the PIE game instances to be deleted for (TObjectIterator It; It; ++It) { auto MarkObjectPendingKill = [](UObject* Object) { Object->MarkPendingKill(); }; ForEachObjectWithOuter(*It, MarkObjectPendingKill, true, RF_NoFlags, EInternalObjectFlags::PendingKill); } // Flush any render commands and released accessed UTextures and materials to give them a chance to be collected. if (FSlateApplication::IsInitialized()) { FSlateApplication::Get().FlushRenderState(); } // Clean up any PIE world objects { // The trans buffer should never have a PIE object in it. If it does though, reset it, which may happen sometimes with selection objects if (GEditor->Trans->ContainsPieObjects()) { GEditor->ResetTransaction(NSLOCTEXT("UnrealEd", "TransactionContainedPIEObject", "A PIE object was in the transaction buffer and had to be destroyed")); } // Garbage Collect CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } // Make sure that all objects in the temp levels were entirely garbage collected. for (FThreadSafeObjectIterator ObjectIt; ObjectIt; ++ObjectIt) { UObject* Object = *ObjectIt; if (Object->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor)) { UWorld* TheWorld = UWorld::FindWorldInPackage(Object->GetOutermost()); if (TheWorld) { StaticExec(nullptr, *FString::Printf(TEXT("OBJ REFS CLASS=WORLD NAME=%s"), *TheWorld->GetPathName())); } else { UE_LOG(LogPlayLevel, Error, TEXT("No PIE world was found when attempting to gather references after GC.")); } FReferenceChainSearch RefChainSearch(Object, EReferenceChainSearchMode::Shortest); FFormatNamedArguments Arguments; Arguments.Add(TEXT("Path"), FText::FromString(RefChainSearch.GetRootPath())); // We cannot safely recover from this. FMessageLog(NAME_CategoryPIE).CriticalError()->AddToken(FUObjectToken::Create(Object, FText::FromString(Object->GetFullName())))->AddToken(FTextToken::Create(FText::Format(LOCTEXT("PIEObjectStillReferenced", "Object from PIE level still referenced. Shortest path from root: {Path}"), Arguments))); } } // Final cleanup/reseting FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext(); UPackage* Package = EditorWorldContext.World()->GetOutermost(); // Spawn note actors dropped in PIE. if (GEngine->PendingDroppedNotes.Num() > 0) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "CreatePIENoteActors", "Create PIE Notes")); for (int32 i = 0; i < GEngine->PendingDroppedNotes.Num(); i++) { FDropNoteInfo& NoteInfo = GEngine->PendingDroppedNotes[i]; ANote* NewNote = EditorWorldContext.World()->SpawnActor(NoteInfo.Location, NoteInfo.Rotation); if (NewNote) { NewNote->Text = NoteInfo.Comment; if (NewNote->GetRootComponent() != NULL) { NewNote->GetRootComponent()->SetRelativeScale3D(FVector(2.f)); } } } Package->MarkPackageDirty(); GEngine->PendingDroppedNotes.Empty(); } // ensure stereo rendering is disabled in case we need to re-enable next PIE run (except when the editor is running in VR) bool bInVRMode = IVREditorModule::Get().IsVREditorModeActive(); if (GEngine->StereoRenderingDevice && !bInVRMode) { GEngine->StereoRenderingDevice->EnableStereo(false); } // Restores realtime viewports that have been disabled for PIE. const FText SystemDisplayName = LOCTEXT("RealtimeOverrideMessage_PIE", "Play in Editor"); RemoveViewportsRealtimeOverride(SystemDisplayName); EnableWorldSwitchCallbacks(false); // Set the autosave timer to have at least 10 seconds remaining before autosave const static float SecondsWarningTillAutosave = 10.0f; GUnrealEd->GetPackageAutoSaver().ForceMinimumTimeTillAutoSave(SecondsWarningTillAutosave); for (TObjectIterator It; It; ++It) { UAudioComponent* AudioComp = *It; if (AudioComp->GetWorld() == EditorWorldContext.World()) { AudioComp->ReregisterComponent(); } } if (PlayInEditorSessionInfo.IsSet()) { // Save the window positions to the CDO object. ULevelEditorPlaySettings* PlaySettingsConfig = GetMutableDefault(); for (int32 WindowIndex = 0; WindowIndex < PlayInEditorSessionInfo->CachedWindowInfo.Num(); WindowIndex++) { if (WindowIndex < PlaySettingsConfig->MultipleInstancePositions.Num()) { PlaySettingsConfig->MultipleInstancePositions[WindowIndex] = PlayInEditorSessionInfo->CachedWindowInfo[WindowIndex].Position; } else { PlaySettingsConfig->MultipleInstancePositions.Add(PlayInEditorSessionInfo->CachedWindowInfo[WindowIndex].Position); } // Update the position where the first PIE window will be opened (this also updates its displayed value in "Editor Preferences" --> "Level Editor" --> "Play" --> "New Window Position") if (WindowIndex == 0) { // Remember last known size PlaySettingsConfig->LastSize = PlayInEditorSessionInfo->CachedWindowInfo[0].Size; // Only update it if "Always center window to screen" is disabled, and the size was not 0 (which means it is attached to the editor rather than being an standalone window) if (!PlaySettingsConfig->CenterNewWindow && PlaySettingsConfig->LastSize.X > 0 && PlaySettingsConfig->LastSize.Y > 0) { PlaySettingsConfig->NewWindowPosition = PlaySettingsConfig->MultipleInstancePositions[WindowIndex]; PlaySettingsConfig->NewWindowWidth = PlaySettingsConfig->LastSize.X; PlaySettingsConfig->NewWindowHeight = PlaySettingsConfig->LastSize.Y; } } } PlaySettingsConfig->PostEditChange(); PlaySettingsConfig->SaveConfig(); } PlayInEditorSessionInfo.Reset(); // no longer queued CancelRequestPlaySession(); bRequestEndPlayMapQueued = false; // Tear down the output log to message log thunker OutputLogErrorsToMessageLogProxyPtr.Reset(); // Remove undo barrier GUnrealEd->Trans->RemoveUndoBarrier(); // display any info if required. FMessageLog(NAME_CategoryPIE).Notify(LOCTEXT("PIEErrorsPresent", "Errors/warnings reported while playing in editor.")); FMessageLog(NAME_CategoryPIE).Open(EMessageSeverity::Warning); { // Temporary until the deprecated variable is removed PRAGMA_DISABLE_DEPRECATION_WARNINGS bIsSimulatingInEditor = false; PRAGMA_ENABLE_DEPRECATION_WARNINGS } } void UEditorEngine::CleanupPIEOnlineSessions(TArray OnlineIdentifiers) { for (FName& OnlineIdentifier: OnlineIdentifiers) { UE_LOG(LogPlayLevel, Display, TEXT("Destroying online subsystem %s"), *OnlineIdentifier.ToString()); UOnlineEngineInterface::Get()->DestroyOnlineSubsystem(OnlineIdentifier); } } void UEditorEngine::TeardownPlaySession(FWorldContext& PieWorldContext) { check(PieWorldContext.WorldType == EWorldType::PIE); PlayWorld = PieWorldContext.World(); PlayWorld->BeginTearingDown(); if (!PieWorldContext.RunAsDedicated) { // Slate data for this pie world FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle); // Destroy Viewport if (PieWorldContext.GameViewport != NULL && PieWorldContext.GameViewport->Viewport != NULL) { PieWorldContext.GameViewport->CloseRequested(PieWorldContext.GameViewport->Viewport); } CleanupGameViewport(); // Clean up the slate PIE viewport if we have one if (SlatePlayInEditorSession) { if (SlatePlayInEditorSession->DestinationSlateViewport.IsValid()) { TSharedPtr Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin(); if (PlayInEditorSessionInfo.IsSet() && PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::PlayInEditor) { // Set the editor viewport location to match that of Play in Viewport if we aren't simulating in the editor, we have a valid player to get the location from (unless we're going back to VR Editor, in which case we won't teleport the user.) if (bLastViewAndLocationValid == true && !GEngine->IsStereoscopic3D(Viewport->GetActiveViewport())) { bLastViewAndLocationValid = false; Viewport->GetAssetViewportClient().SetViewLocation(LastViewLocation); if (Viewport->GetAssetViewportClient().IsPerspective()) { // Rotation only matters for perspective viewports not orthographic Viewport->GetAssetViewportClient().SetViewRotation(LastViewRotation); } } } // No longer simulating in the viewport Viewport->GetAssetViewportClient().SetIsSimulateInEditorViewport(false); FEditorModeRegistry::Get().UnregisterMode(FBuiltinEditorModes::EM_Physics); // Clear out the hit proxies before GC'ing Viewport->GetAssetViewportClient().Viewport->InvalidateHitProxy(); } else if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid()) { // Unregister the game viewport from slate. This sends a final message to the viewport // so it can have a chance to release mouse capture, mouse lock, etc. FSlateApplication::Get().UnregisterGameViewport(); // Viewport client is cleaned up. Make sure its not being accessed SlatePlayInEditorSession->SlatePlayInEditorWindowViewport->SetViewportClient(NULL); // The window may have already been destroyed in the case that the PIE window close box was pressed if (SlatePlayInEditorSession->SlatePlayInEditorWindow.IsValid()) { // Destroy the SWindow FSlateApplication::Get().DestroyWindowImmediately(SlatePlayInEditorSession->SlatePlayInEditorWindow.Pin().ToSharedRef()); } } } // Disassociate the players from their PlayerControllers. // This is done in the GameEngine path in UEngine::LoadMap. // But since PIE is just shutting down, and not loading a // new map, we need to do it manually here for now. // for (auto It = GEngine->GetLocalPlayerIterator(PlayWorld); It; ++It) for (FLocalPlayerIterator It(GEngine, PlayWorld); It; ++It) { if (It->PlayerController) { if (It->PlayerController->GetPawn()) { PlayWorld->DestroyActor(It->PlayerController->GetPawn(), true); } PlayWorld->DestroyActor(It->PlayerController, true); It->PlayerController = NULL; } } } // Change GWorld to be the play in editor world during cleanup. ensureMsgf(EditorWorld == GWorld, TEXT("TearDownPlaySession current world: %s"), GWorld ? *GWorld->GetName() : TEXT("No World")); GWorld = PlayWorld; GIsPlayInEditorWorld = true; // Remember Simulating flag so that we know if OnSimulateSessionFinished is required after everything has been cleaned up. bool bWasSimulatingInEditor = PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor; // Stop all audio and remove references to temp level. if (FAudioDevice* AudioDevice = PlayWorld->GetAudioDeviceRaw()) { AudioDevice->Flush(PlayWorld); AudioDevice->ResetInterpolation(); AudioDevice->OnEndPIE(false); // TODO: Should this have been bWasSimulatingInEditor? AudioDevice->SetTransientMasterVolume(1.0f); } // Clean up all streaming levels PlayWorld->bIsLevelStreamingFrozen = false; PlayWorld->SetShouldForceUnloadStreamingLevels(true); PlayWorld->FlushLevelStreaming(); // cleanup refs to any duplicated streaming levels for (int32 LevelIndex = 0; LevelIndex < PlayWorld->GetStreamingLevels().Num(); LevelIndex++) { ULevelStreaming* StreamingLevel = PlayWorld->GetStreamingLevels()[LevelIndex]; if (StreamingLevel != NULL) { const ULevel* PlayWorldLevel = StreamingLevel->GetLoadedLevel(); if (PlayWorldLevel != NULL) { UWorld* World = Cast(PlayWorldLevel->GetOuter()); if (World != NULL) { // Attempt to move blueprint debugging references back to the editor world if (EditorWorld != NULL && EditorWorld->GetStreamingLevels().IsValidIndex(LevelIndex)) { const ULevel* EditorWorldLevel = EditorWorld->GetStreamingLevels()[LevelIndex]->GetLoadedLevel(); if (EditorWorldLevel != NULL) { UWorld* SublevelEditorWorld = Cast(EditorWorldLevel->GetOuter()); if (SublevelEditorWorld != NULL) { World->TransferBlueprintDebugReferences(SublevelEditorWorld); } } } } } } } // Construct a list of editors that are active for objects being debugged. We will refresh these when we have cleaned up to ensure no invalid objects exist in them TArray Editors; UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); const UWorld::FBlueprintToDebuggedObjectMap& EditDebugObjectsPre = PlayWorld->GetBlueprintObjectsBeingDebugged(); for (UWorld::FBlueprintToDebuggedObjectMap::TConstIterator EditIt(EditDebugObjectsPre); EditIt; ++EditIt) { if (UBlueprint* TargetBP = EditIt.Key().Get()) { if (IBlueprintEditor* EachEditor = static_cast(AssetEditorSubsystem->FindEditorForAsset(TargetBP, false))) { Editors.AddUnique(EachEditor); } } } // Go through and let all the PlayWorld Actor's know they are being destroyed for (FActorIterator ActorIt(PlayWorld); ActorIt; ++ActorIt) { ActorIt->RouteEndPlay(EEndPlayReason::EndPlayInEditor); } PieWorldContext.OwningGameInstance->Shutdown(); // Move blueprint debugging pointers back to the objects in the editor world PlayWorld->TransferBlueprintDebugReferences(EditorWorld); FPhysScene* PhysScene = PlayWorld->GetPhysicsScene(); if (PhysScene) { PhysScene->WaitPhysScenes(); PhysScene->KillVisualDebugger(); } // Clean up the temporary play level. PlayWorld->CleanupWorld(); // Remove from root (Seamless travel may have done this) PlayWorld->RemoveFromRoot(); PlayWorld = NULL; // Refresh any editors we had open in case they referenced objects that no longer exist. for (int32 iEditors = 0; iEditors < Editors.Num(); iEditors++) { Editors[iEditors]->RefreshEditors(); } // Restore GWorld. GWorld = EditorWorld; GIsPlayInEditorWorld = false; FWorldContext& EditorWorldContext = GEditor->GetEditorWorldContext(); // Let the viewport know about leaving PIE/Simulate session. Do it after everything's been cleaned up // as the viewport will play exit sound here and this has to be done after GetAudioDevice()->Flush // otherwise all sounds will be immediately stopped. if (!PieWorldContext.RunAsDedicated) { // Slate data for this pie world FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(PieWorldContext.ContextHandle); if (SlatePlayInEditorSession && SlatePlayInEditorSession->DestinationSlateViewport.IsValid()) { TSharedPtr Viewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin(); if (Viewport->HasPlayInEditorViewport()) { Viewport->EndPlayInEditorSession(); } // Let the Slate viewport know that we're leaving Simulate mode if (bWasSimulatingInEditor) { Viewport->OnSimulateSessionFinished(); } StaticCast(Viewport->GetAssetViewportClient()).SetReferenceToWorldContext(EditorWorldContext); } // Remove the slate info from the map (note that the UWorld* is long gone at this point, but the WorldContext still exists. It will be removed outside of this function) SlatePlayInEditorMap.Remove(PieWorldContext.ContextHandle); } } // Deprecated, just format to match our new style. void UEditorEngine::PlayMap(const FVector* StartLocation, const FRotator* StartRotation, int32 Destination, int32 InPlayInViewportIndex, bool bUseMobilePreview) { // Deprecated, replaced with RequestPlaySession. FRequestPlaySessionParams Params; if (bUseMobilePreview) { Params.SessionDestination = EPlaySessionDestinationType::NewProcess; Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::MobilePreview; } if (StartLocation) { Params.StartLocation = *StartLocation; Params.StartRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator; } RequestPlaySession(Params); } void UEditorEngine::RequestPlaySession(const FRequestPlaySessionParams& InParams) { // Store our Request to be operated on next Tick. PlaySessionRequest = InParams; // If they don't want to use a specific set of Editor Play Settings, fall back to the CDO. if (!PlaySessionRequest->EditorPlaySettings) { PlaySessionRequest->EditorPlaySettings = GetMutableDefault(); } // Now we duplicate their Editor Play Settings so that we can mutate it as part of startup // to help rule out invalid configuration combinations. FObjectDuplicationParameters DuplicationParams(PlaySessionRequest->EditorPlaySettings, GetTransientPackage()); // Kept alive by AddReferencedObjects PlaySessionRequest->EditorPlaySettings = CastChecked(StaticDuplicateObjectEx(DuplicationParams)); // ToDo: Allow the CDO for the Game Instance to modify the settings after we copy them // so that they can validate user settings before attempting a launch. // Play Sessions can use the Game Mode to determine the default Player Start position // or the start position can be overridden by the incoming launch arguments PRAGMA_DISABLE_DEPRECATION_WARNINGS GIsPIEUsingPlayerStart = !InParams.StartLocation.IsSet(); PRAGMA_ENABLE_DEPRECATION_WARNINGS if (InParams.SessionDestination == EPlaySessionDestinationType::Launcher) { check(InParams.LauncherTargetDevice.IsSet()); } } // Deprecated, just format to match our new style. void UEditorEngine::RequestPlaySession(bool bAtPlayerStart, TSharedPtr DestinationViewport, bool bInSimulateInEditor, const FVector* StartLocation, const FRotator* StartRotation, int32 DestinationConsole, bool bUseMobilePreview, bool bUseVRPreview, bool bUseVulkanPreview) { FRequestPlaySessionParams Params; if (StartLocation) { Params.StartLocation = *StartLocation; Params.StartRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator; } if (DestinationViewport != nullptr) { Params.DestinationSlateViewport = DestinationViewport; } if (bInSimulateInEditor) { Params.WorldType = EPlaySessionWorldType::SimulateInEditor; } if (bUseVRPreview) { check(!bUseMobilePreview && !bUseVulkanPreview); Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::VRPreview; } if (bUseVulkanPreview) { check(!bUseMobilePreview && !bUseVRPreview); Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::VulkanPreview; Params.SessionDestination = EPlaySessionDestinationType::NewProcess; } if (bUseMobilePreview) { check(!bUseVRPreview && !bUseVulkanPreview); Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::MobilePreview; Params.SessionDestination = EPlaySessionDestinationType::NewProcess; } RequestPlaySession(Params); } // Deprecated, forwards request onto the FRequestPlaySessionParams version. void UEditorEngine::RequestPlaySession(const FVector* StartLocation, const FRotator* StartRotation, bool MobilePreview, bool VulkanPreview, const FString& MobilePreviewTargetDevice, FString AdditionalLaunchParameters) { FRequestPlaySessionParams Params; if (MobilePreview) { check(!VulkanPreview); Params.SessionDestination = EPlaySessionDestinationType::NewProcess; Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::MobilePreview; Params.MobilePreviewTargetDevice = MobilePreviewTargetDevice; Params.AdditionalStandaloneCommandLineParameters = AdditionalLaunchParameters; } if (VulkanPreview) { check(!MobilePreview); Params.SessionDestination = EPlaySessionDestinationType::NewProcess; Params.SessionPreviewTypeOverride = EPlaySessionPreviewType::VulkanPreview; Params.AdditionalStandaloneCommandLineParameters = AdditionalLaunchParameters; } if (StartLocation) { Params.StartLocation = *StartLocation; Params.StartRotation = StartRotation ? *StartRotation : FRotator::ZeroRotator; } RequestPlaySession(Params); } // Deprecated, forwards request onto the FRequestPlaySessionParams version. void UEditorEngine::RequestPlaySession(const FString& DeviceId, const FString& DeviceName) { FRequestPlaySessionParams::FLauncherDeviceInfo DeviceInfo; DeviceInfo.DeviceId = DeviceId; DeviceInfo.DeviceName = DeviceName; FRequestPlaySessionParams Params; Params.LauncherTargetDevice = DeviceInfo; Params.SessionDestination = EPlaySessionDestinationType::Launcher; RequestPlaySession(Params); } void UEditorEngine::CancelRequestPlaySession() { PlaySessionRequest.Reset(); PlayInEditorSessionInfo.Reset(); } bool UEditorEngine::SaveMapsForPlaySession() { // Prompt the user to save the level if it has not been saved before. // An unmodified but unsaved blank template level does not appear in the dirty packages check below. if (FEditorFileUtils::GetFilename(GWorld).Len() == 0) { if (!FEditorFileUtils::SaveCurrentLevel()) { return false; } } // Also save dirty packages, this is required because we're going to be launching a session outside of our normal process const bool bPromptUserToSave = true; const bool bSaveMapPackages = true; const bool bSaveContentPackages = true; if (!FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages)) { return false; } return true; } void UEditorEngine::PlaySessionPaused() { FEditorDelegates::PausePIE.Broadcast(PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); } void UEditorEngine::PlaySessionResumed() { FEditorDelegates::ResumePIE.Broadcast(PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); } void UEditorEngine::PlaySessionSingleStepped() { FEditorDelegates::SingleStepPIE.Broadcast(PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); } bool UEditorEngine::ProcessDebuggerCommands(const FKey InKey, const FModifierKeysState ModifierKeyState, EInputEvent EventType) { if (EventType == IE_Pressed) { return FPlayWorldCommands::GlobalPlayWorldActions->ProcessCommandBindings(InKey, ModifierKeyState, false); } return false; } // This function is deprecated, just call the non-deprecated version. void UEditorEngine::StartQueuedPlayMapRequest() { StartQueuedPlaySessionRequest(); } void UEditorEngine::StartQueuedPlaySessionRequest() { if (!PlaySessionRequest.IsSet()) { UE_LOG(LogPlayLevel, Warning, TEXT("StartQueuedPlaySessionRequest() called whith no request queued. Ignoring...")); return; } StartQueuedPlaySessionRequestImpl(); // Ensure the request is always reset after an attempt (which may fail) // so that we don't get stuck in an infinite loop of start attempts. PlaySessionRequest.Reset(); } void UEditorEngine::StartQueuedPlaySessionRequestImpl() { if (!ensureAlwaysMsgf(PlaySessionRequest.IsSet(), TEXT("StartQueuedPlaySessionRequest should not be called without a request set!"))) { return; } // End any previous sessions running in separate processes. EndPlayOnLocalPc(); // If there's level already being played, close it. (This may change GWorld). if (PlayWorld && PlaySessionRequest->SessionDestination == EPlaySessionDestinationType::InProcess) { // Cache our Play Session Request, as EndPlayMap will clear it. When this function exits the request will be reset anyways. FRequestPlaySessionParams OriginalRequest = PlaySessionRequest.GetValue(); // Immediately end the current play world. EndPlayMap(); // Restore the request as we're now processing it. PlaySessionRequest = OriginalRequest; } // We want to use the ULevelEditorPlaySettings that come from the Play Session Request. // By the time this function gets called, these settings are a copy of either the CDO, // or a user provided instance. The settings may have been modified by the game instance // after the request was made, to allow game instances to pre-validate settings. const ULevelEditorPlaySettings* EditorPlaySettings = PlaySessionRequest->EditorPlaySettings; check(EditorPlaySettings); PlayInEditorSessionInfo = FPlayInEditorSessionInfo(); PlayInEditorSessionInfo->PlayRequestStartTime = FPlatformTime::Seconds(); PlayInEditorSessionInfo->PlayRequestStartTime_StudioAnalytics = FStudioAnalytics::GetAnalyticSeconds(); // Keep a copy of their original request settings for any late // joiners or async processes that need access to the settings after launch. PlayInEditorSessionInfo->OriginalRequestParams = PlaySessionRequest.GetValue(); // Load the saved window positions from the EditorPlaySettings object. for (const FIntPoint& Position: EditorPlaySettings->MultipleInstancePositions) { FPlayInEditorSessionInfo::FWindowSizeAndPos& NewPos = PlayInEditorSessionInfo->CachedWindowInfo.Add_GetRef(FPlayInEditorSessionInfo::FWindowSizeAndPos()); NewPos.Position = Position; } // If our settings require us to launch a separate process in any form, we require the user to save // their content so that when the new process reads the data from disk it will match what we have in-editor. bool bUserWantsInProcess; EditorPlaySettings->GetRunUnderOneProcess(bUserWantsInProcess); bool bIsSeparateProcess = PlaySessionRequest->SessionDestination != EPlaySessionDestinationType::InProcess; if (!bUserWantsInProcess) { int32 NumClients; EditorPlaySettings->GetPlayNumberOfClients(NumClients); EPlayNetMode NetMode; EditorPlaySettings->GetPlayNetMode(NetMode); // More than one client will spawn a second process. bIsSeparateProcess |= NumClients > 1; // If they want to run anyone as a client, a dedicated server is started in a separate process. bIsSeparateProcess |= NetMode == EPlayNetMode::PIE_Client; } if (bIsSeparateProcess && !SaveMapsForPlaySession()) { // Maps did not save, print a warning FText ErrorMsg = LOCTEXT("PIEWorldSaveFail", "PIE failed because map save was canceled"); UE_LOG(LogPlayLevel, Warning, TEXT("%s"), *ErrorMsg.ToString()); FMessageLog(NAME_CategoryPIE).Warning(ErrorMsg); FMessageLog(NAME_CategoryPIE).Open(); CancelRequestPlaySession(); return; } // We'll branch primarily based on the Session Destination, because it affects which settings we apply and how. switch (PlaySessionRequest->SessionDestination) { case EPlaySessionDestinationType::InProcess: // Create one-or-more PIE/SIE sessions inside of the current process. StartPlayInEditorSession(PlaySessionRequest.GetValue()); break; case EPlaySessionDestinationType::NewProcess: // Create one-or-more PIE session by launching a new process on the local machine. StartPlayInNewProcessSession(PlaySessionRequest.GetValue()); break; case EPlaySessionDestinationType::Launcher: // Create a Play Session via the Launcher which may be on a local or remote device. StartPlayUsingLauncherSession(PlaySessionRequest.GetValue()); break; default: check(false); } } void UEditorEngine::EndPlayOnLocalPc() { for (int32 i = 0; i < PlayOnLocalPCSessions.Num(); ++i) { if (PlayOnLocalPCSessions[i].ProcessHandle.IsValid()) { if (FPlatformProcess::IsProcRunning(PlayOnLocalPCSessions[i].ProcessHandle)) { FPlatformProcess::TerminateProc(PlayOnLocalPCSessions[i].ProcessHandle); } PlayOnLocalPCSessions[i].ProcessHandle.Reset(); } } PlayOnLocalPCSessions.Empty(); } int32 FInternalPlayLevelUtils::ResolveDirtyBlueprints(const bool bPromptForCompile, TArray& ErroredBlueprints, const bool bForceLevelScriptRecompile) { struct FLocal { static void OnMessageLogLinkActivated(const class TSharedRef& Token) { if (Token->GetType() == EMessageToken::Object) { const TSharedRef UObjectToken = StaticCastSharedRef(Token); if (UObjectToken->GetObject().IsValid()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(UObjectToken->GetObject().Get()); } } } static void AddCompileErrorToLog(UBlueprint* ErroredBlueprint, FMessageLog& BlueprintLog) { FFormatNamedArguments Arguments; Arguments.Add(TEXT("Name"), FText::FromString(ErroredBlueprint->GetName())); TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Warning); Message->AddToken(FTextToken::Create(LOCTEXT("BlueprintCompileFailed", "Blueprint failed to compile: "))); Message->AddToken(FUObjectToken::Create(ErroredBlueprint, FText::FromString(ErroredBlueprint->GetName())) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&FLocal::OnMessageLogLinkActivated))); BlueprintLog.AddMessage(Message); } }; const bool bAutoCompile = !bPromptForCompile; FString PromptDirtyList; TArray InNeedOfRecompile; ErroredBlueprints.Empty(); FMessageLog BlueprintLog("BlueprintLog"); double BPRegenStartTime = FPlatformTime::Seconds(); for (TObjectIterator BlueprintIt; BlueprintIt; ++BlueprintIt) { UBlueprint* Blueprint = *BlueprintIt; // ignore up-to-date BPs if (Blueprint->IsUpToDate()) { continue; } // do not try to recompile BPs that have not changed since they last failed to compile, so don't check Blueprint->IsUpToDate() const bool bIsDirtyAndShouldBeRecompiled = Blueprint->IsPossiblyDirty(); if (!FBlueprintEditorUtils::IsDataOnlyBlueprint(Blueprint) && (bIsDirtyAndShouldBeRecompiled || (FBlueprintEditorUtils::IsLevelScriptBlueprint(Blueprint) && bForceLevelScriptRecompile)) && (Blueprint->Status != BS_Unknown) && !Blueprint->IsPendingKill()) { InNeedOfRecompile.Add(Blueprint); if (bPromptForCompile) { PromptDirtyList += FString::Printf(TEXT("\n %s"), *Blueprint->GetName()); } } else if (BS_Error == Blueprint->Status && Blueprint->bDisplayCompilePIEWarning) { ErroredBlueprints.Add(Blueprint); FLocal::AddCompileErrorToLog(Blueprint, BlueprintLog); } } bool bRunCompilation = bAutoCompile; if (bPromptForCompile) { FFormatNamedArguments Args; Args.Add(TEXT("DirtyBlueprints"), FText::FromString(PromptDirtyList)); const FText PromptMsg = FText::Format(NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintsDirty", "One or more blueprints have been modified without being recompiled. Do you want to compile them now? \n{DirtyBlueprints}"), Args); EAppReturnType::Type PromptResponse = FMessageDialog::Open(EAppMsgType::YesNo, PromptMsg); bRunCompilation = (PromptResponse == EAppReturnType::Yes); } int32 RecompiledCount = 0; if (bRunCompilation && (InNeedOfRecompile.Num() > 0)) { const FText LogPageLabel = (bAutoCompile) ? LOCTEXT("BlueprintAutoCompilationPageLabel", "Pre-Play auto-recompile") : LOCTEXT("BlueprintCompilationPageLabel", "Pre-Play recompile"); BlueprintLog.NewPage(LogPageLabel); TArray CompiledBlueprints; auto OnBlueprintPreCompileLambda = [&CompiledBlueprints](UBlueprint* InBlueprint) { check(InBlueprint != nullptr); if (CompiledBlueprints.Num() == 0) { UE_LOG(LogPlayLevel, Log, TEXT("[PlayLevel] Compiling %s before play..."), *InBlueprint->GetName()); } else { UE_LOG(LogPlayLevel, Log, TEXT("[PlayLevel] Compiling %s as a dependent..."), *InBlueprint->GetName()); } CompiledBlueprints.Add(InBlueprint); }; // Register compile callback FDelegateHandle PreCompileDelegateHandle = GEditor->OnBlueprintPreCompile().AddLambda(OnBlueprintPreCompileLambda); // Recompile all necessary blueprints in a single loop, saving GC until the end for (auto BlueprintIt = InNeedOfRecompile.CreateIterator(); BlueprintIt; ++BlueprintIt) { UBlueprint* Blueprint = *BlueprintIt; int32 CurrItIndex = BlueprintIt.GetIndex(); // Compile the Blueprint (note: re-instancing may trigger additional compiles for child/dependent Blueprints; see callback above) FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipGarbageCollection); // Check for errors after compiling for (UBlueprint* CompiledBlueprint: CompiledBlueprints) { if (CompiledBlueprint != Blueprint) { int32 ExistingIndex = InNeedOfRecompile.Find(CompiledBlueprint); // if this dependent blueprint is already set up to compile // later in this loop, then there is no need to add it to be recompiled again if (ExistingIndex > CurrItIndex) { InNeedOfRecompile.RemoveAt(ExistingIndex); } } const bool bHadError = (!CompiledBlueprint->IsUpToDate() && CompiledBlueprint->Status != BS_Unknown); // Check if the Blueprint has already been added to the error list to prevent it from being added again if (bHadError && ErroredBlueprints.Find(CompiledBlueprint) == INDEX_NONE) { ErroredBlueprints.Add(CompiledBlueprint); FLocal::AddCompileErrorToLog(CompiledBlueprint, BlueprintLog); } ++RecompiledCount; } // Reset for next pass CompiledBlueprints.Empty(); } // Now that all Blueprints have been compiled, run a single GC pass to clean up artifacts CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); // Unregister compile callback GEditor->OnBlueprintPreCompile().Remove(PreCompileDelegateHandle); UE_LOG(LogPlayLevel, Log, TEXT("PlayLevel: Blueprint regeneration took %d ms (%i blueprints)"), (int32)((FPlatformTime::Seconds() - BPRegenStartTime) * 1000), RecompiledCount); } else if (bAutoCompile) { UE_LOG(LogPlayLevel, Log, TEXT("PlayLevel: No blueprints needed recompiling")); } return RecompiledCount; } void UEditorEngine::RequestEndPlayMap() { if (PlayWorld) { bRequestEndPlayMapQueued = true; // Cache the position and rotation of the camera (the controller may be destroyed before we end the pie session and we need them to preserve the camera position) if (bLastViewAndLocationValid == false) { for (int32 WorldIdx = WorldList.Num() - 1; WorldIdx >= 0; --WorldIdx) { FWorldContext& ThisContext = WorldList[WorldIdx]; if (ThisContext.WorldType == EWorldType::PIE) { FSlatePlayInEditorInfo* const SlatePlayInEditorSession = SlatePlayInEditorMap.Find(ThisContext.ContextHandle); if ((SlatePlayInEditorSession != nullptr) && (SlatePlayInEditorSession->EditorPlayer.IsValid() == true)) { if (SlatePlayInEditorSession->EditorPlayer.Get()->PlayerController != nullptr) { SlatePlayInEditorSession->EditorPlayer.Get()->PlayerController->GetPlayerViewPoint(LastViewLocation, LastViewRotation); bLastViewAndLocationValid = true; break; } } } } } } } FString UEditorEngine::BuildPlayWorldURL(const TCHAR* MapName, bool bSpectatorMode, FString AdditionalURLOptions) { // the URL we are building up FString URL(MapName); // If we hold down control, start in spectating mode if (bSpectatorMode) { // Start in spectator mode URL += TEXT("?SpectatorOnly=1"); } // Add any game-specific options set in the INI file URL += InEditorGameURLOptions; // Add any additional options that were specified for this call URL += AdditionalURLOptions; return URL; } bool UEditorEngine::SpawnPlayFromHereStart(UWorld* World, AActor*& PlayerStart) { if (PlayInEditorSessionInfo.IsSet() && PlayInEditorSessionInfo->OriginalRequestParams.HasPlayWorldPlacement()) { // Rotation may be optional in original request. return SpawnPlayFromHereStart(World, PlayerStart, PlayInEditorSessionInfo->OriginalRequestParams.StartLocation.GetValue(), PlayInEditorSessionInfo->OriginalRequestParams.StartRotation.Get(FRotator::ZeroRotator)); } // Not having a location set is still considered a success. return true; } bool UEditorEngine::SpawnPlayFromHereStart(UWorld* World, AActor*& PlayerStart, const FVector& StartLocation, const FRotator& StartRotation) { // null it out in case we don't need to spawn one, and the caller relies on us setting it PlayerStart = NULL; // spawn the PlayerStartPIE in the given world FActorSpawnParameters SpawnParameters; SpawnParameters.OverrideLevel = World->PersistentLevel; PlayerStart = World->SpawnActor(PlayFromHerePlayerStartClass, StartLocation, StartRotation, SpawnParameters); // make sure we were able to spawn the PlayerStartPIE there if (!PlayerStart) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Prompt_22", "Failed to create entry point. Try another location, or you may have to rebuild your level.")); return false; } // tag the start ANavigationObjectBase* NavPlayerStart = Cast(PlayerStart); if (NavPlayerStart) { NavPlayerStart->bIsPIEPlayerStart = true; } return true; } static bool ShowBlueprintErrorDialog(TArray ErroredBlueprints) { if (FApp::IsUnattended() || GIsRunningUnattendedScript) { // App is running in unattended mode, so we should avoid modal dialogs and proceed return true; } struct Local { static void OnHyperlinkClicked(TWeakObjectPtr InBlueprint, TSharedPtr InDialog) { if (UBlueprint* BlueprintToEdit = InBlueprint.Get()) { // Open the blueprint GEditor->EditObject(BlueprintToEdit); } if (InDialog.IsValid()) { // Opening the blueprint editor above may end up creating an invisible new window on top of the dialog, // thus making it not interactable, so we have to force the dialog back to the front InDialog->BringToFront(true); } } }; TSharedRef DialogContents = SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(0, 0, 0, 16) [SNew(STextBlock) .Text(NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintErrors", "One or more blueprints has an unresolved compiler error, are you sure you want to Play in Editor?"))]; TSharedPtr CustomDialog; for (UBlueprint* Blueprint: ErroredBlueprints) { TWeakObjectPtr BlueprintPtr = Blueprint; DialogContents->AddSlot() .AutoHeight() .HAlign(HAlign_Left) [SNew(SHyperlink) .Style(FEditorStyle::Get(), "Common.GotoBlueprintHyperlink") .OnNavigate(FSimpleDelegate::CreateLambda([BlueprintPtr, &CustomDialog]() { Local::OnHyperlinkClicked(BlueprintPtr, CustomDialog); })) .Text(FText::FromString(Blueprint->GetName())) .ToolTipText(NSLOCTEXT("SourceHyperlink", "EditBlueprint_ToolTip", "Click to edit the blueprint"))]; } DialogContents->AddSlot() .Padding(0, 16, 0, 0) [SNew(STextBlock) .Text(NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintErrorsDelayedOpen", "Clicked blueprints will open once this dialog is closed."))]; FText DialogTitle = NSLOCTEXT("PlayInEditor", "PrePIE_BlueprintErrorsTitle", "Blueprint Compilation Errors"); FText OKText = NSLOCTEXT("PlayInEditor", "PrePIE_OkText", "Play in Editor"); FText CancelText = NSLOCTEXT("Dialogs", "EAppReturnTypeCancel", "Cancel"); CustomDialog = SNew(SCustomDialog) .Title(DialogTitle) .IconBrush("NotificationList.DefaultMessage") .DialogContent(DialogContents) .Buttons({SCustomDialog::FButton(OKText), SCustomDialog::FButton(CancelText)}); int32 ButtonPressed = CustomDialog->ShowModal(); return ButtonPressed == 0; } FGameInstancePIEResult UEditorEngine::PreCreatePIEServerInstance(const bool bAnyBlueprintErrors, const bool bStartInSpectatorMode, const float PIEStartTime, const bool bSupportsOnlinePIE, int32& InNumOnlinePIEInstances) { return FGameInstancePIEResult::Success(); } bool UEditorEngine::SupportsOnlinePIE() const { return UOnlineEngineInterface::Get()->SupportsOnlinePIE(); } void UEditorEngine::OnLoginPIEComplete(int32 LocalUserNum, bool bWasSuccessful, const FString& ErrorString, FPieLoginStruct DataStruct) { // This is needed because pie login may change the state of the online objects that called this function. This also enqueues the // callback back onto the main thread at an appropriate time instead of mid-networking callback. GetTimerManager()->SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UEditorEngine::OnLoginPIEComplete_Deferred, LocalUserNum, bWasSuccessful, ErrorString, DataStruct)); } void UEditorEngine::OnLoginPIEComplete_Deferred(int32 LocalUserNum, bool bWasSuccessful, FString ErrorString, FPieLoginStruct DataStruct) { // This function will get called for both Async Online Service Log-in and for local non-async client creation. // This is called once per client that will be created, and may be called many frames after the initial PIE request // was started. UE_LOG(LogPlayLevel, Verbose, TEXT("OnLoginPIEComplete LocalUserNum: %d bSuccess: %d %s"), LocalUserNum, bWasSuccessful, *ErrorString); FWorldContext* PieWorldContext = GetWorldContextFromHandle(DataStruct.WorldContextHandle); if (!PieWorldContext) { // This will fail if PIE was ended before this callback happened, silently return return; } // Detect success based on override bool bPIELoginSucceeded = IsLoginPIESuccessful(LocalUserNum, bWasSuccessful, ErrorString, DataStruct); // Create a new Game Instance for this. UGameInstance* GameInstance = CreateInnerProcessPIEGameInstance(PlayInEditorSessionInfo->OriginalRequestParams, DataStruct.GameInstancePIEParameters, DataStruct.PIEInstanceIndex); if (GameInstance) { GameInstance->GetWorldContext()->bWaitingOnOnlineSubsystem = false; // Logging after the create so a new MessageLog Page is created if (bPIELoginSucceeded) { if (DataStruct.GameInstancePIEParameters.NetMode != EPlayNetMode::PIE_Client) { FMessageLog(NAME_CategoryPIE).Info(LOCTEXT("LoggedInServer", "Server logged in")); } else { FMessageLog(NAME_CategoryPIE).Info(LOCTEXT("LoggedInClient", "Client logged in")); } } else { if (DataStruct.GameInstancePIEParameters.NetMode != EPlayNetMode::PIE_Client) { FMessageLog(NAME_CategoryPIE).Error(FText::Format(LOCTEXT("LoggedInServerFailure", "Server failed to login. {0}"), FText::FromString(ErrorString))); } else { FMessageLog(NAME_CategoryPIE).Error(FText::Format(LOCTEXT("LoggedInClientFailure", "Client failed to login. {0}"), FText::FromString(ErrorString))); } } } // If there was a startup failure EndPlayMap might have already been called and unsetting this. if (PlayInEditorSessionInfo.IsSet()) { PlayInEditorSessionInfo->NumOutstandingPIELogins--; if (!ensureAlwaysMsgf(PlayInEditorSessionInfo->NumOutstandingPIELogins >= 0, TEXT("PIEInstancesToLogInCount was not properly reset at some point."))) { PlayInEditorSessionInfo->NumOutstandingPIELogins = 0; } // If there are no more instances waiting to log-in then we can do post-launch notifications. if (PlayInEditorSessionInfo->NumOutstandingPIELogins == 0) { if (!bPIELoginSucceeded) { UE_LOG(LogPlayLevel, Warning, TEXT("At least one of the requested PIE instances failed to start, ending PIE session.")); EndPlayMap(); } else { OnAllPIEInstancesStarted(); } } } } void UEditorEngine::OnAllPIEInstancesStarted() { GiveFocusToLastClientPIEViewport(); // Print out a log message stating the overall startup time. { FFormatNamedArguments Arguments; Arguments.Add(TEXT("StartTime"), FPlatformTime::Seconds() - PlayInEditorSessionInfo->PlayRequestStartTime); FMessageLog(NAME_CategoryPIE).Info(FText::Format(LOCTEXT("PIETotalStartTime", "Play in editor total start time {StartTime} seconds."), Arguments)); } } void UEditorEngine::GiveFocusToLastClientPIEViewport() { // Find the non-dedicated server or last client window to give focus to. We choose the last one // because when launching additional instances, by applying focus to the first one it immediately // pushes the PINW instances behind the editor and you can't tell that they were launched. int32 HighestPIEInstance = TNumericLimits::Min(); UGameViewportClient* ViewportClient = nullptr; for (const FWorldContext& WorldContext: WorldList) { if (WorldContext.WorldType == EWorldType::PIE && !WorldContext.RunAsDedicated) { if (WorldContext.PIEInstance > HighestPIEInstance) { HighestPIEInstance = WorldContext.PIEInstance; ViewportClient = WorldContext.GameViewport; } } } if (ViewportClient && ViewportClient->GetGameViewportWidget().IsValid()) { FSlateApplication::Get().RegisterGameViewport(ViewportClient->GetGameViewportWidget().ToSharedRef()); } // Make sure to focus the game viewport. { const bool bPreviewTypeIsVR = PlayInEditorSessionInfo->OriginalRequestParams.SessionPreviewTypeOverride.Get(EPlaySessionPreviewType::NoPreview) == EPlaySessionPreviewType::VRPreview; const bool bIsVR = IVREditorModule::Get().IsVREditorEnabled() || (bPreviewTypeIsVR && GEngine->XRSystem.IsValid()); const bool bGameGetsMouseControl = PlayInEditorSessionInfo->OriginalRequestParams.EditorPlaySettings->GameGetsMouseControl; if (PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::PlayInEditor && (bGameGetsMouseControl || bIsVR)) { FSlateApplication::Get().SetAllUserFocusToGameViewport(); } } } void UEditorEngine::RequestLateJoin() { if (!ensureMsgf(PlayInEditorSessionInfo.IsSet(), TEXT("RequestLateJoin shouldn't be called if no session is in progress!"))) { return; } if (!ensureMsgf(PlayInEditorSessionInfo->OriginalRequestParams.WorldType != EPlaySessionWorldType::SimulateInEditor, TEXT("RequestLateJoin shouldn't be called for SIE!"))) { return; } bool bUsePIEOnlineAuthentication = false; if (PlayInEditorSessionInfo->bUsingOnlinePlatform) { int32 TotalNumDesiredClients = PlayInEditorSessionInfo->NumClientInstancesCreated + 1; bool bHasRequiredLogins = TotalNumDesiredClients <= UOnlineEngineInterface::Get()->GetNumPIELogins(); if (bHasRequiredLogins) { bUsePIEOnlineAuthentication = true; } else { FText ErrorMsg = LOCTEXT("PIELateJoinLoginFailure", "Not enough login credentials to add additional PIE instance, change editor settings."); UE_LOG(LogPlayLevel, Warning, TEXT("%s"), *ErrorMsg.ToString()); FMessageLog(NAME_CategoryPIE).Warning(ErrorMsg); return; } } // By the time of Late Join, a server should already be running if needed. So instead we only need // to launch a new instance following the same flow as all of the clients that already joined. EPlayNetMode NetMode; PlayInEditorSessionInfo->OriginalRequestParams.EditorPlaySettings->GetPlayNetMode(NetMode); // Adding another instance to a Listen Server will create clients, not more servers. if (NetMode == EPlayNetMode::PIE_ListenServer) { NetMode = EPlayNetMode::PIE_Client; } CreateNewPlayInEditorInstance(PlayInEditorSessionInfo->OriginalRequestParams, false, NetMode); } void UEditorEngine::CreateNewPlayInEditorInstance(FRequestPlaySessionParams& InRequestParams, const bool bInDedicatedInstance, const EPlayNetMode InNetMode) { bool bUserWantsSingleProcess; InRequestParams.EditorPlaySettings->GetRunUnderOneProcess(bUserWantsSingleProcess); // If they don't want to use a single process, we can only launch one client inside the editor. if (!bUserWantsSingleProcess && PlayInEditorSessionInfo->NumClientInstancesCreated > 0) { if (bInDedicatedInstance) { // If they needed a server and they need multiple processes, it should // have already been launched by this time (by the block above). This means // this code should only ever be launching clients (but the code below can // launch either clients, listen servers, or dedicated servers.) check(PlayInEditorSessionInfo->bServerWasLaunched); } const bool bIsDedicatedServer = false; LaunchNewProcess(InRequestParams, PlayInEditorSessionInfo->NumClientInstancesCreated, InNetMode, bIsDedicatedServer); PlayInEditorSessionInfo->NumClientInstancesCreated++; } else { FPieLoginStruct PIELoginInfo; FGameInstancePIEParameters GameInstancePIEParameters; GameInstancePIEParameters.PIEStartTime = PlayInEditorSessionInfo->PlayRequestStartTime_StudioAnalytics; GameInstancePIEParameters.bAnyBlueprintErrors = PlayInEditorSessionInfo->bAnyBlueprintErrors; // If they require a server and one hasn't been launched then it is dedicated. If they're a client or listen server // then it doesn't count as a dedicated server so this can be false (NetMode will handle ListenServer). GameInstancePIEParameters.bRunAsDedicated = bInDedicatedInstance; GameInstancePIEParameters.bSimulateInEditor = InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor; GameInstancePIEParameters.bStartInSpectatorMode = PlayInEditorSessionInfo->bStartedInSpectatorMode; GameInstancePIEParameters.EditorPlaySettings = PlayInEditorSessionInfo->OriginalRequestParams.EditorPlaySettings; GameInstancePIEParameters.WorldFeatureLevel = PreviewPlatform.GetEffectivePreviewFeatureLevel(); GameInstancePIEParameters.NetMode = InNetMode; GameInstancePIEParameters.OverrideMapURL = InRequestParams.GlobalMapOverride; PIELoginInfo.GameInstancePIEParameters = GameInstancePIEParameters; // Create a World Context for our client. FWorldContext& PieWorldContext = CreateNewWorldContext(EWorldType::PIE); PieWorldContext.PIEInstance = PlayInEditorSessionInfo->PIEInstanceCount++; PieWorldContext.bWaitingOnOnlineSubsystem = true; PIELoginInfo.WorldContextHandle = PieWorldContext.ContextHandle; PIELoginInfo.PIEInstanceIndex = PieWorldContext.PIEInstance; // Fixed tick setting if (InRequestParams.EditorPlaySettings->ServerFixedFPS > 0 && bInDedicatedInstance) { PieWorldContext.PIEFixedTickSeconds = 1.f / (float)InRequestParams.EditorPlaySettings->ServerFixedFPS; } if (InRequestParams.EditorPlaySettings->ClientFixedFPS.Num() > 0 && !bInDedicatedInstance) { const int32 ClientFixedIdx = PlayInEditorSessionInfo->NumClientInstancesCreated % InRequestParams.EditorPlaySettings->ClientFixedFPS.Num(); const int32 DesiredFPS = InRequestParams.EditorPlaySettings->ClientFixedFPS[ClientFixedIdx]; if (DesiredFPS > 0) { PieWorldContext.PIEFixedTickSeconds = 1.f / (float)DesiredFPS; } } // We increment how many PIE instances we think are going to boot up as it is decremented // in OnLoginPIEComplete_Deferred and used to check if all instances have booted up. PlayInEditorSessionInfo->NumOutstandingPIELogins++; // If they are using an Online Subsystem that requires log-in then they require async // log-in so we use a deferred log-in approach. if (PlayInEditorSessionInfo->bUsingOnlinePlatform) { FName OnlineIdentifier = UOnlineEngineInterface::Get()->GetOnlineIdentifier(PieWorldContext); UE_LOG(LogPlayLevel, Display, TEXT("Creating online subsystem for client %s"), *OnlineIdentifier.ToString()); if (GameInstancePIEParameters.bRunAsDedicated) { // Dedicated servers don't use a login UOnlineEngineInterface::Get()->SetForceDedicated(OnlineIdentifier, true); OnLoginPIEComplete_Deferred(0, true, FString(), PIELoginInfo); } else { // Login to Online platform before creating world FOnPIELoginComplete OnPIELoginCompleteDelegate; OnPIELoginCompleteDelegate.BindUObject(this, &UEditorEngine::OnLoginPIEComplete, PIELoginInfo); // Kick off an async request to the Online Interface to log-in and, on completion, finish client creation. UOnlineEngineInterface::Get()->LoginPIEInstance(OnlineIdentifier, 0, PlayInEditorSessionInfo->NumClientInstancesCreated, OnPIELoginCompleteDelegate); } } else { // Otherwise, we can create the client immediately. We'll emulate the PIE login structure so we can just // use the same flow as the deferred log-in, but skipping the deferred part. OnLoginPIEComplete_Deferred(0, true, FString(), PIELoginInfo); } // Only count non-dedicated instances as clients so that our indexes line up for log-ins. if (!GameInstancePIEParameters.bRunAsDedicated && PlayInEditorSessionInfo.IsSet()) { PlayInEditorSessionInfo->NumClientInstancesCreated++; } } } class SPIEViewport: public SViewport { SLATE_BEGIN_ARGS(SPIEViewport) : _Content(), _RenderDirectlyToWindow(false), _EnableStereoRendering(false), _IgnoreTextureAlpha(true) { _Clipping = EWidgetClipping::ClipToBoundsAlways; } SLATE_DEFAULT_SLOT(FArguments, Content) /** * Whether or not to render directly to the window's backbuffer or an offscreen render target that is applied to the window later * Rendering to an offscreen target is the most common option in the editor where there may be many frames which this viewport's interface may wish to not re-render but use a cached buffer instead * Rendering directly to the backbuffer is the most common option in the game where you want to update each frame without the cost of writing to an intermediate target first. */ SLATE_ARGUMENT(bool, RenderDirectlyToWindow) /** Whether or not to enable stereo rendering. */ SLATE_ARGUMENT(bool, EnableStereoRendering) /** * If true, the viewport's texture alpha is ignored when performing blending. In this case only the viewport tint opacity is used * If false, the texture alpha is used during blending */ SLATE_ARGUMENT(bool, IgnoreTextureAlpha) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { SViewport::Construct( SViewport::FArguments() .EnableGammaCorrection(false) // Gamma correction in the game is handled in post processing in the scene renderer .RenderDirectlyToWindow(InArgs._RenderDirectlyToWindow) .EnableStereoRendering(InArgs._EnableStereoRendering) .IgnoreTextureAlpha(InArgs._IgnoreTextureAlpha) [InArgs._Content.Widget]); } virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override { SViewport::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); // Rather than binding the attribute we're going to poll it in tick, otherwise we will make this widget volatile, and it therefore // wont be possible to cache it or its children in GSlateEnableGlobalInvalidation mode. SetEnabled(FSlateApplication::Get().GetNormalExecutionAttribute().Get()); } }; void UEditorEngine::OnViewportCloseRequested(FViewport* InViewport) { RequestEndPlayMap(); } FSceneViewport* UEditorEngine::GetGameSceneViewport(UGameViewportClient* ViewportClient) const { return ViewportClient->GetGameViewport(); } FViewport* UEditorEngine::GetActiveViewport() { // Get the Level editor module and request the Active Viewport. FLevelEditorModule& LevelEditorModule = FModuleManager::Get().GetModuleChecked(TEXT("LevelEditor")); TSharedPtr ActiveLevelViewport = LevelEditorModule.GetFirstActiveViewport(); if (ActiveLevelViewport.IsValid()) { return ActiveLevelViewport->GetActiveViewport(); } return nullptr; } FViewport* UEditorEngine::GetPIEViewport() { // Check both cases where the PIE viewport may be, otherwise return NULL if none are found. if (GameViewport) { return GameViewport->Viewport; } else { for (const FWorldContext& WorldContext: WorldList) { if (WorldContext.WorldType == EWorldType::PIE) { // We can't use FindChecked here because when using the dedicated server option we don't initialize this map // (we don't use a viewport for the PIE context in this case) FSlatePlayInEditorInfo* SlatePlayInEditorSessionPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle); if (SlatePlayInEditorSessionPtr != nullptr && SlatePlayInEditorSessionPtr->SlatePlayInEditorWindowViewport.IsValid()) { return SlatePlayInEditorSessionPtr->SlatePlayInEditorWindowViewport.Get(); } } } } return nullptr; } bool UEditorEngine::GetSimulateInEditorViewTransform(FTransform& OutViewTransform) const { if (PlayInEditorSessionInfo->OriginalRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor) { // The first PIE world context is the one that can toggle between PIE and SIE for (const FWorldContext& WorldContext: WorldList) { if (WorldContext.WorldType == EWorldType::PIE && !WorldContext.RunAsDedicated) { const FSlatePlayInEditorInfo* SlateInfoPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle); if (SlateInfoPtr) { // This is only supported inside SLevelEditor viewports currently TSharedPtr LevelViewport = SlateInfoPtr->DestinationSlateViewport.Pin(); if (LevelViewport.IsValid()) { FEditorViewportClient& EditorViewportClient = LevelViewport->GetAssetViewportClient(); OutViewTransform = FTransform(EditorViewportClient.GetViewRotation(), EditorViewportClient.GetViewLocation()); return true; } } break; } } } return false; } void UEditorEngine::ToggleBetweenPIEandSIE(bool bNewSession) { bIsToggleBetweenPIEandSIEQueued = false; FEditorDelegates::OnPreSwitchBeginPIEAndSIE.Broadcast(bIsSimulatingInEditor); // The first PIE world context is the one that can toggle between PIE and SIE // Network PIE/SIE toggling is not really meant to be supported. FSlatePlayInEditorInfo* SlateInfoPtr = nullptr; for (const FWorldContext& WorldContext: WorldList) { if (WorldContext.WorldType == EWorldType::PIE && !WorldContext.RunAsDedicated) { SlateInfoPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle); break; } } if (!SlateInfoPtr) { return; } if (FEngineAnalytics::IsAvailable() && !bNewSession) { FString ToggleType = bIsSimulatingInEditor ? TEXT("SIEtoPIE") : TEXT("PIEtoSIE"); FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.PIE"), TEXT("ToggleBetweenPIEandSIE"), ToggleType); } FSlatePlayInEditorInfo& SlatePlayInEditorSession = *SlateInfoPtr; // This is only supported inside SLevelEditor viewports currently TSharedPtr LevelViewport = SlatePlayInEditorSession.DestinationSlateViewport.Pin(); if (ensure(LevelViewport.IsValid())) { FEditorViewportClient& EditorViewportClient = LevelViewport->GetAssetViewportClient(); // Toggle to pie if currently simulating if (bIsSimulatingInEditor) { // The undo system may have a reference to a SIE object that is about to be destroyed, so clear the transactions ResetTransaction(NSLOCTEXT("UnrealEd", "ToggleBetweenPIEandSIE", "Toggle Between PIE and SIE")); // The Game's viewport needs to know about the change away from simluate before the PC is (potentially) created GameViewport->GetGameViewport()->SetPlayInEditorIsSimulate(false); // The editor viewport client wont be visible so temporarily disable it being realtime const bool bShouldBeRealtime = false; const FText SystemDisplayName = LOCTEXT("RealtimeOverrideMessage_PIE", "Play in Editor"); // Remove any previous override since we already applied a override when entering PIE EditorViewportClient.RemoveRealtimeOverride(SystemDisplayName); EditorViewportClient.AddRealtimeOverride(bShouldBeRealtime, SystemDisplayName); if (!SlatePlayInEditorSession.EditorPlayer.IsValid()) { OnSwitchWorldsForPIE(true); UWorld* World = GameViewport->GetWorld(); AGameModeBase* AuthGameMode = World->GetAuthGameMode(); if (AuthGameMode && GameViewport->GetGameInstance()) // If there is no GameMode, we are probably the client and cannot RestartPlayer. { AuthGameMode->SpawnPlayerFromSimulate(EditorViewportClient.GetViewLocation(), EditorViewportClient.GetViewRotation()); } OnSwitchWorldsForPIE(false); } // A game viewport already exists, tell the level viewport its in to swap to it LevelViewport->SwapViewportsForPlayInEditor(); // No longer simulating GameViewport->SetIsSimulateInEditorViewport(false); EditorViewportClient.SetIsSimulateInEditorViewport(false); FEditorModeRegistry::Get().UnregisterMode(FBuiltinEditorModes::EM_Physics); bIsSimulatingInEditor = false; } else { // Swap to simulate from PIE LevelViewport->SwapViewportsForSimulateInEditor(); GameViewport->SetIsSimulateInEditorViewport(true); GameViewport->GetGameViewport()->SetPlayInEditorIsSimulate(true); EditorViewportClient.SetIsSimulateInEditorViewport(true); TSharedRef Factory = MakeShareable(new FPhysicsManipulationEdModeFactory); FEditorModeRegistry::Get().RegisterMode(FBuiltinEditorModes::EM_Physics, Factory); bIsSimulatingInEditor = true; // Make sure the viewport is in real-time mode const bool bShouldBeRealtime = true; const FText SystemDisplayName = LOCTEXT("RealtimeOverrideMessage_PIE", "Play in Editor"); // Remove any previous override since we already applied a override when entering PIE EditorViewportClient.RemoveRealtimeOverride(SystemDisplayName); EditorViewportClient.AddRealtimeOverride(bShouldBeRealtime, SystemDisplayName); // The Simulate window should show stats EditorViewportClient.SetShowStats(true); if (SlatePlayInEditorSession.EditorPlayer.IsValid() && SlatePlayInEditorSession.EditorPlayer.Get()->PlayerController) { // Move the editor camera to where the player was. FVector ViewLocation; FRotator ViewRotation; SlatePlayInEditorSession.EditorPlayer.Get()->PlayerController->GetPlayerViewPoint(ViewLocation, ViewRotation); EditorViewportClient.SetViewLocation(ViewLocation); if (EditorViewportClient.IsPerspective()) { // Rotation only matters for perspective viewports not orthographic EditorViewportClient.SetViewRotation(ViewRotation); } } } } // Backup ActorsThatWereSelected as this will be cleared whilst deselecting TArray> BackupOfActorsThatWereSelected(ActorsThatWereSelected); // Unselect everything GEditor->SelectNone(true, true, false); GetSelectedActors()->DeselectAll(); GetSelectedObjects()->DeselectAll(); // restore the backup ActorsThatWereSelected = BackupOfActorsThatWereSelected; // make sure each selected actors sim equivalent is selected if we're Simulating but not if we're Playing for (int32 ActorIndex = 0; ActorIndex < ActorsThatWereSelected.Num(); ++ActorIndex) { TWeakObjectPtr Actor = ActorsThatWereSelected[ActorIndex].Get(); if (Actor.IsValid()) { AActor* SimActor = EditorUtilities::GetSimWorldCounterpartActor(Actor.Get()); if (SimActor && !SimActor->IsHidden()) { SelectActor(SimActor, bIsSimulatingInEditor, false); } } } FEditorDelegates::OnSwitchBeginPIEAndSIE.Broadcast(bIsSimulatingInEditor); } int32 UEditorEngine::OnSwitchWorldForSlatePieWindow(int32 WorldID, int32 WorldPIEInstance) { static const int32 EditorWorldID = 0; static const int32 PieWorldID = 1; // PlayWorld cannot be depended on as it only points to the first instance int32 RestoreID = -1; if (WorldID == -1 && !GIsPlayInEditorWorld) { // When we have an invalid world id we always switch to the pie world in the PIE window OnSwitchWorldsForPIEInstance(WorldPIEInstance); // The editor world was active restore it later RestoreID = EditorWorldID; } else if (WorldID == PieWorldID && !GIsPlayInEditorWorld) { // Want to restore the PIE world and the current world is not already the pie world OnSwitchWorldsForPIEInstance(WorldPIEInstance); } else if (WorldID == EditorWorldID && GWorld != EditorWorld) { // Want to restore the editor world and the current world is not already the editor world OnSwitchWorldsForPIEInstance(-1); } else { // Current world is already the same as the world being switched to (nested calls to this for example) } return RestoreID; } void UEditorEngine::OnSwitchWorldsForPIE(bool bSwitchToPieWorld, UWorld* OverrideWorld) { if (bSwitchToPieWorld) { SetPlayInEditorWorld(OverrideWorld ? OverrideWorld : PlayWorld); } else { RestoreEditorWorld(OverrideWorld ? OverrideWorld : EditorWorld); } } void UEditorEngine::OnSwitchWorldsForPIEInstance(int32 WorldPIEInstance) { if (WorldPIEInstance < 0) { RestoreEditorWorld(EditorWorld); } else { FWorldContext* PIEContext = GetPIEWorldContext(WorldPIEInstance); if (PIEContext && PIEContext->World()) { SetPlayInEditorWorld(PIEContext->World()); } } } void UEditorEngine::EnableWorldSwitchCallbacks(bool bEnable) { if (bEnable) { // Set up a delegate to be called in Slate when GWorld needs to change. Slate does not have direct access to the playworld to switch itself FScopedConditionalWorldSwitcher::SwitchWorldForPIEDelegate = FOnSwitchWorldForPIE::CreateUObject(this, &UEditorEngine::OnSwitchWorldsForPIE); if (!ScriptExecutionStartHandle.IsValid()) { // This function can get called multiple times in multiplayer PIE ScriptExecutionStartHandle = FBlueprintContextTracker::OnEnterScriptContext.AddUObject(this, &UEditorEngine::OnScriptExecutionStart); ScriptExecutionEndHandle = FBlueprintContextTracker::OnExitScriptContext.AddUObject(this, &UEditorEngine::OnScriptExecutionEnd); } } else { // Don't actually need to reset this delegate but doing so allows is to check invalid attempts to execute the delegate FScopedConditionalWorldSwitcher::SwitchWorldForPIEDelegate = FOnSwitchWorldForPIE(); // There should never be an active function context when pie is ending! check(!FunctionStackWorldSwitcher); if (ScriptExecutionStartHandle.IsValid()) { FBlueprintContextTracker::OnEnterScriptContext.Remove(ScriptExecutionStartHandle); ScriptExecutionStartHandle.Reset(); FBlueprintContextTracker::OnExitScriptContext.Remove(ScriptExecutionEndHandle); ScriptExecutionEndHandle.Reset(); } } } void UEditorEngine::OnScriptExecutionStart(const FBlueprintContextTracker& ContextTracker, const UObject* ContextObject, const UFunction* ContextFunction) { // Only do world switching for game thread callbacks when current world is set, this is only bound at all in PIE so no need to check GIsEditor if (IsInGameThread() && GWorld) { // See if we should create a world switcher, which is true if we don't have one and our PIE info is missing if (!FunctionStackWorldSwitcher && (!GIsPlayInEditorWorld || GPlayInEditorID == -1)) { check(FunctionStackWorldSwitcherTag == -1); UWorld* ContextWorld = GetWorldFromContextObject(ContextObject, EGetWorldErrorMode::ReturnNull); if (ContextWorld && ContextWorld->WorldType == EWorldType::PIE) { FunctionStackWorldSwitcher = new FScopedConditionalWorldSwitcher(ContextWorld); FunctionStackWorldSwitcherTag = ContextTracker.GetScriptEntryTag(); } } } } void UEditorEngine::OnScriptExecutionEnd(const struct FBlueprintContextTracker& ContextTracker) { if (IsInGameThread()) { if (FunctionStackWorldSwitcher) { int32 CurrentScriptEntryTag = ContextTracker.GetScriptEntryTag(); // Tag starts at 1 for first function on stack check(CurrentScriptEntryTag >= 1 && FunctionStackWorldSwitcherTag >= 1); if (CurrentScriptEntryTag == FunctionStackWorldSwitcherTag) { FunctionStackWorldSwitcherTag = -1; delete FunctionStackWorldSwitcher; FunctionStackWorldSwitcher = nullptr; } } } } UWorld* UEditorEngine::CreatePIEWorldByDuplication(FWorldContext& WorldContext, UWorld* InWorld, FString& PlayWorldMapName) { double StartTime = FPlatformTime::Seconds(); UPackage* InPackage = InWorld->GetOutermost(); UWorld* NewPIEWorld = NULL; const FString WorldPackageName = InPackage->GetName(); // Preserve the old path keeping EditorWorld name the same PlayWorldMapName = UWorld::ConvertToPIEPackageName(WorldPackageName, WorldContext.PIEInstance); // Display a busy cursor while we prepare the PIE world const FScopedBusyCursor BusyCursor; // Before loading the map, we need to set these flags to true so that postload will work properly TGuardValue OverrideIsPlayWorld(GIsPlayInEditorWorld, true); const FName PlayWorldMapFName = FName(*PlayWorldMapName); UWorld::WorldTypePreLoadMap.FindOrAdd(PlayWorldMapFName) = EWorldType::PIE; // Create a package for the PIE world UE_LOG(LogPlayLevel, Log, TEXT("Creating play world package: %s"), *PlayWorldMapName); UPackage* PlayWorldPackage = CreatePackage(*PlayWorldMapName); PlayWorldPackage->SetPackageFlags(PKG_PlayInEditor); PlayWorldPackage->PIEInstanceID = WorldContext.PIEInstance; PlayWorldPackage->FileName = InPackage->FileName; PRAGMA_DISABLE_DEPRECATION_WARNINGS PlayWorldPackage->SetGuid(InPackage->GetGuid()); PRAGMA_ENABLE_DEPRECATION_WARNINGS PlayWorldPackage->MarkAsFullyLoaded(); // check(GPlayInEditorID == -1 || GPlayInEditorID == WorldContext.PIEInstance); // Currently GPlayInEditorID is not correctly reset after map loading, so it's not safe to assert here FTemporaryPlayInEditorIDOverride IDHelper(WorldContext.PIEInstance); { double SDOStart = FPlatformTime::Seconds(); // Reset any GUID fixups with lazy pointers FLazyObjectPtr::ResetPIEFixups(); // Prepare soft object paths for fixup FSoftObjectPath::AddPIEPackageName(FName(*PlayWorldMapName)); for (ULevelStreaming* StreamingLevel: InWorld->GetStreamingLevels()) { if (StreamingLevel && !StreamingLevel->HasAllFlags(RF_DuplicateTransient)) { FString StreamingLevelPIEName = UWorld::ConvertToPIEPackageName(StreamingLevel->GetWorldAssetPackageName(), WorldContext.PIEInstance); FSoftObjectPath::AddPIEPackageName(FName(*StreamingLevelPIEName)); } } // NULL GWorld before various PostLoad functions are called, this makes it easier to debug invalid GWorld accesses GWorld = NULL; // Duplicate the editor world to create the PIE world NewPIEWorld = UWorld::GetDuplicatedWorldForPIE(InWorld, PlayWorldPackage, WorldContext.PIEInstance); // Fixup model components. The index buffers have been created for the components in the source world and the order // in which components were post-loaded matters. So don't try to guarantee a particular order here, just copy the // elements over. if (NewPIEWorld->PersistentLevel->Model != NULL && NewPIEWorld->PersistentLevel->Model == InWorld->PersistentLevel->Model && NewPIEWorld->PersistentLevel->ModelComponents.Num() == InWorld->PersistentLevel->ModelComponents.Num()) { NewPIEWorld->PersistentLevel->Model->ClearLocalMaterialIndexBuffersData(); for (int32 ComponentIndex = 0; ComponentIndex < NewPIEWorld->PersistentLevel->ModelComponents.Num(); ++ComponentIndex) { UModelComponent* SrcComponent = InWorld->PersistentLevel->ModelComponents[ComponentIndex]; UModelComponent* DestComponent = NewPIEWorld->PersistentLevel->ModelComponents[ComponentIndex]; DestComponent->CopyElementsFrom(SrcComponent); } } UE_LOG(LogPlayLevel, Log, TEXT("PIE: StaticDuplicateObject took: (%fs)"), float(FPlatformTime::Seconds() - SDOStart)); } // Clean up the world type list now that PostLoad has occurred UWorld::WorldTypePreLoadMap.Remove(PlayWorldMapFName); check(NewPIEWorld); NewPIEWorld->FeatureLevel = InWorld->FeatureLevel; PostCreatePIEWorld(NewPIEWorld); UE_LOG(LogPlayLevel, Log, TEXT("PIE: Created PIE world by copying editor world from %s to %s (%fs)"), *InWorld->GetPathName(), *NewPIEWorld->GetPathName(), float(FPlatformTime::Seconds() - StartTime)); return NewPIEWorld; } void UEditorEngine::PostCreatePIEWorld(UWorld* NewPIEWorld) { double WorldInitStart = FPlatformTime::Seconds(); // Init the PIE world NewPIEWorld->WorldType = EWorldType::PIE; NewPIEWorld->InitWorld(); UE_LOG(LogPlayLevel, Log, TEXT("PIE: World Init took: (%fs)"), float(FPlatformTime::Seconds() - WorldInitStart)); // Tag PlayWorld Actors that also exist in EditorWorld. At this point, no temporary/run-time actors exist in PlayWorld for (FActorIterator PlayActorIt(NewPIEWorld); PlayActorIt; ++PlayActorIt) { GEditor->ObjectsThatExistInEditorWorld.Set(*PlayActorIt); } } UWorld* UEditorEngine::CreatePIEWorldFromEntry(FWorldContext& WorldContext, UWorld* InWorld, FString& PlayWorldMapName) { double StartTime = FPlatformTime::Seconds(); // Create the world UWorld* LoadedWorld = UWorld::CreateWorld(EWorldType::PIE, false); check(LoadedWorld); if (LoadedWorld->GetOutermost() != GetTransientPackage()) { LoadedWorld->GetOutermost()->PIEInstanceID = WorldContext.PIEInstance; } // Force default GameMode class so project specific code doesn't fire off. // We want this world to truly remain empty while we wait for connect! check(LoadedWorld->GetWorldSettings()); LoadedWorld->GetWorldSettings()->DefaultGameMode = AGameModeBase::StaticClass(); PlayWorldMapName = UGameMapsSettings::GetGameDefaultMap(); return LoadedWorld; } bool UEditorEngine::WorldIsPIEInNewViewport(UWorld* InWorld) { FWorldContext& WorldContext = GetWorldContextFromWorldChecked(InWorld); if (WorldContext.WorldType == EWorldType::PIE) { FSlatePlayInEditorInfo* SlateInfoPtr = SlatePlayInEditorMap.Find(WorldContext.ContextHandle); if (SlateInfoPtr) { return SlateInfoPtr->SlatePlayInEditorWindow.IsValid(); } } return false; } void UEditorEngine::SetPIEInstanceWindowSwitchDelegate(FPIEInstanceWindowSwitch InSwitchDelegate) { PIEInstanceWindowSwitchDelegate = InSwitchDelegate; } void UEditorEngine::FocusNextPIEWorld(UWorld* CurrentPieWorld, bool previous) { // Get the current world's idx int32 CurrentIdx = 0; for (CurrentIdx = 0; CurrentPieWorld && CurrentIdx < WorldList.Num(); ++CurrentIdx) { if (WorldList[CurrentIdx].World() == CurrentPieWorld) { break; } } // Step through the list to find the next or previous int32 step = previous ? -1 : 1; CurrentIdx += (WorldList.Num() + step); while (CurrentPieWorld && WorldList[CurrentIdx % WorldList.Num()].World() != CurrentPieWorld) { FWorldContext& Context = WorldList[CurrentIdx % WorldList.Num()]; if (Context.World() && Context.WorldType == EWorldType::PIE && Context.GameViewport != NULL) { break; } CurrentIdx += step; } if (WorldList[CurrentIdx % WorldList.Num()].World()) { // Bring new window to front and activate new viewport FSlatePlayInEditorInfo* SlateInfoPtr = SlatePlayInEditorMap.Find(WorldList[CurrentIdx % WorldList.Num()].ContextHandle); if (SlateInfoPtr && SlateInfoPtr->SlatePlayInEditorWindowViewport.IsValid()) { FSceneViewport* SceneViewport = SlateInfoPtr->SlatePlayInEditorWindowViewport.Get(); FSlateApplication& SlateApp = FSlateApplication::Get(); TSharedRef ViewportWidget = SceneViewport->GetViewportWidget().Pin().ToSharedRef(); TSharedPtr ViewportWindow = SlateApp.FindWidgetWindow(ViewportWidget); check(ViewportWindow.IsValid()); // Force window to front ViewportWindow->BringToFront(); // Execute notification delegate in case game code has to do anything else PIEInstanceWindowSwitchDelegate.ExecuteIfBound(); } } } void UEditorEngine::ResetPIEAudioSetting(UWorld* CurrentPieWorld) { ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); if (!PlayInSettings->EnableGameSound) { if (FAudioDevice* AudioDevice = CurrentPieWorld->GetAudioDeviceRaw()) { AudioDevice->SetTransientMasterVolume(0.0f); } } } UGameViewportClient* UEditorEngine::GetNextPIEViewport(UGameViewportClient* CurrentViewport) { // Get the current world's idx int32 CurrentIdx = 0; for (CurrentIdx = 0; CurrentViewport && CurrentIdx < WorldList.Num(); ++CurrentIdx) { if (WorldList[CurrentIdx].GameViewport == CurrentViewport) { break; } } // Step through the list to find the next or previous int32 step = 1; CurrentIdx += (WorldList.Num() + step); while (CurrentViewport && WorldList[CurrentIdx % WorldList.Num()].GameViewport != CurrentViewport) { FWorldContext& Context = WorldList[CurrentIdx % WorldList.Num()]; if (Context.GameViewport && Context.WorldType == EWorldType::PIE) { return Context.GameViewport; } CurrentIdx += step; } return NULL; } void UEditorEngine::RemapGamepadControllerIdForPIE(class UGameViewportClient* InGameViewport, int32& ControllerId) { // Increment the controller id if we are the focused window, and RouteGamepadToSecondWindow is true (and we are running multiple clients). // This cause the focused window to NOT handle the input, decrement controllerID, and pass it to the next window. const ULevelEditorPlaySettings* PlayInSettings = GetDefault(); const bool CanRouteGamepadToSecondWindow = [&PlayInSettings] { bool RouteGamepadToSecondWindow(false); return (PlayInSettings->GetRouteGamepadToSecondWindow(RouteGamepadToSecondWindow) && RouteGamepadToSecondWindow); }(); const bool CanRunUnderOneProcess = [&PlayInSettings] { bool RunUnderOneProcess(false); return (PlayInSettings->GetRunUnderOneProcess(RunUnderOneProcess) && RunUnderOneProcess); }(); if (CanRouteGamepadToSecondWindow && CanRunUnderOneProcess && InGameViewport->GetWindow().IsValid() && InGameViewport->GetWindow()->HasFocusedDescendants()) { ControllerId++; } } void UEditorEngine::StartPlayInEditorSession(FRequestPlaySessionParams& InRequestParams) { // This reflects that the user has tried to launch a PIE session, but it may still // create one-or-more new processes depending on multiplayer settings. check(InRequestParams.SessionDestination == EPlaySessionDestinationType::InProcess); // Broadcast PreBeginPIE before checks that might block PIE below (BeginPIE is broadcast below after the checks) FEditorDelegates::PreBeginPIE.Broadcast(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); const double PIEStartTime = FStudioAnalytics::GetAnalyticSeconds(); const FScopedBusyCursor BusyCursor; // Cancel the transaction if one is opened when PIE is requested. This is generally avoided // because we buffer the request for the PIE session until the start of the next frame, but sometimes // transactions can get stuck open due to implementation errors so it's important that we check. if (GEditor->IsTransactionActive()) { FFormatNamedArguments Args; FText TransactionName = GEditor->GetTransactionName(); Args.Add(TEXT("TransactionName"), TransactionName); if (InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor) { Args.Add(TEXT("PlaySession"), NSLOCTEXT("UnrealEd", "SimulatePlaySession", "Simulate")); } else { Args.Add(TEXT("PlaySession"), NSLOCTEXT("UnrealEd", "PIEPlaySession", "Play In Editor")); } FText NotificationText; NotificationText = FText::Format(NSLOCTEXT("UnrealEd", "CancellingTransactionForPIE", "Cancelling open '{TransactionName}' operation to start {PlaySession}"), Args); FNotificationInfo Info(NotificationText); Info.ExpireDuration = 5.0f; Info.bUseLargeFont = true; FSlateNotificationManager::Get().AddNotification(Info); GEditor->CancelTransaction(0); UE_LOG(LogPlayLevel, Warning, TEXT("Cancelling Open Transaction '%s' to start PIE session."), *TransactionName.ToString()); } // Prompt the user that Matinee must be closed before PIE can occur. If they don't want // to close Matinee, we can't PIE. if (!PromptMatineeClose()) { CancelRequestPlaySession(); return; } TArray PlayAuthorizers = IModularFeatures::Get().GetModularFeatureImplementations(IPIEAuthorizer::GetModularFeatureName()); for (const IPIEAuthorizer* Authority: PlayAuthorizers) { FString DeniedReason; if (!Authority->RequestPIEPermission(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor, DeniedReason)) { // In case the authorizer didn't notify the user as to why this was blocked. UE_LOG(LogPlayLevel, Warning, TEXT("Play-In-Editor canceled by plugin: %s"), *DeniedReason); CancelRequestPlaySession(); return; } } // Make sure there's no outstanding load requests FlushAsyncLoading(); // Update the Blueprint Debugger FBlueprintEditorUtils::FindAndSetDebuggableBlueprintInstances(); // Broadcast BeginPIE after checks that might block PIE above (PreBeginPIE is broadcast above before the checks) // ToDo: Shouldn't this move below the early-out for Error'd Blueprints? FEditorDelegates::BeginPIE.Broadcast(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); UWorld* InWorld = GetEditorWorldContext().World(); // Let navigation know PIE is starting so it can avoid any blueprint creation/deletion/instantiation affect editor map's navmesh changes FNavigationSystem::OnPIEStart(*InWorld); ULevelEditorPlaySettings* EditorPlaySettings = InRequestParams.EditorPlaySettings; check(EditorPlaySettings); // Auto-compile dirty blueprints (if needed) and let the user cancel PIE if there are TArray ErroredBlueprints; FInternalPlayLevelUtils::ResolveDirtyBlueprints(!EditorPlaySettings->AutoRecompileBlueprints, ErroredBlueprints); // Don't show the dialog if we're in the middle of a demo, just assume they'll work. if (ErroredBlueprints.Num() && !GIsDemoMode) { // There was at least one blueprint with an error, make sure the user is OK with that. bool bContinuePIE = ShowBlueprintErrorDialog(ErroredBlueprints); if (!bContinuePIE) { FMessageLog("BlueprintLog").Open(EMessageSeverity::Warning); FEditorDelegates::EndPIE.Broadcast(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); FNavigationSystem::OnPIEEnd(*InWorld); CancelRequestPlaySession(); return; } else { // The user wants to ignore the compiler errors, mark the Blueprints and do not warn them again unless the Blueprint attempts to compile for (UBlueprint* Blueprint: ErroredBlueprints) { Blueprint->bDisplayCompilePIEWarning = false; } } } // Register for log processing so we can promote errors/warnings to the message log if (GetDefault()->bPromoteOutputLogWarningsDuringPIE) { OutputLogErrorsToMessageLogProxyPtr = MakeShareable(new FOutputLogErrorsToMessageLogProxy()); } // Notify the XRSystem that it needs to BeginPlay. if (GEngine->XRSystem.IsValid() && InRequestParams.WorldType == EPlaySessionWorldType::PlayInEditor) { GEngine->XRSystem->OnBeginPlay(*GEngine->GetWorldContextFromWorld(InWorld)); } // Remember old GWorld EditorWorld = InWorld; // Clear any messages from last time GEngine->ClearOnScreenDebugMessages(); // Start a new PIE log page { const FString WorldPackageName = EditorWorld->GetOutermost()->GetName(); FFormatNamedArguments Arguments; Arguments.Add(TEXT("Package"), FText::FromString(FPackageName::GetLongPackageAssetName(WorldPackageName))); Arguments.Add(TEXT("TimeStamp"), FText::AsDateTime(FDateTime::Now())); FText PIESessionLabel = InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor ? FText::Format(LOCTEXT("SIESessionLabel", "SIE session: {Package} ({TimeStamp})"), Arguments) : FText::Format(LOCTEXT("PIESessionLabel", "PIE session: {Package} ({TimeStamp})"), Arguments); FMessageLog(NAME_CategoryPIE).NewPage(PIESessionLabel); } // Flush all audio sources from the editor world if (FAudioDeviceHandle AudioDevice = EditorWorld->GetAudioDevice()) { AudioDevice->Flush(EditorWorld); AudioDevice->ResetInterpolation(); AudioDevice->OnBeginPIE(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); } // Mute the editor world so no sounds come from it. EditorWorld->bAllowAudioPlayback = false; // Can't allow realtime viewports whilst in PIE so disable it for ALL viewports here. const bool bShouldBeRealtime = false; const FText SystemDisplayName = LOCTEXT("RealtimeOverrideMessage_PIE", "Play in Editor"); SetViewportsRealtimeOverride(bShouldBeRealtime, SystemDisplayName); // Allow the global config to override our ability to create multiple PIE worlds. if (!GEditor->bAllowMultiplePIEWorlds) { EditorPlaySettings->SetRunUnderOneProcess(false); } // If they're Simulating in Editor, we just override them to only one instance (as it'll use the editor world and not duplicate) // We also override them to Offline play mode as networking isn't supported with SIE anyways. if (InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor) { EditorPlaySettings->SetPlayNetMode(EPlayNetMode::PIE_Standalone); EditorPlaySettings->SetPlayNumberOfClients(1); } // If they need to use the online services, validate that they have provided // enough pie credentials to launch the desired number of clients. bool bUseOnlineSubsystemForLogin = false; { int32 DesiredNumberOfClients; EditorPlaySettings->GetPlayNumberOfClients(DesiredNumberOfClients); if (SupportsOnlinePIE() && InRequestParams.WorldType != EPlaySessionWorldType::SimulateInEditor) { bool bHasRequiredLogins = DesiredNumberOfClients <= UOnlineEngineInterface::Get()->GetNumPIELogins(); if (bHasRequiredLogins) { // If we support online PIE use it even if we're standalone bUseOnlineSubsystemForLogin = true; } else { FText ErrorMsg = LOCTEXT("PIELoginFailure", "Not enough login credentials to launch all PIE instances, change editor settings"); UE_LOG(LogPlayLevel, Verbose, TEXT("%s"), *ErrorMsg.ToString()); FMessageLog(NAME_CategoryPIE).Warning(ErrorMsg); } } UOnlineEngineInterface::Get()->SetShouldTryOnlinePIE(bUseOnlineSubsystemForLogin); } PlayInEditorSessionInfo->bUsingOnlinePlatform = bUseOnlineSubsystemForLogin; PlayInEditorSessionInfo->bAnyBlueprintErrors = ErroredBlueprints.Num() > 0; // Now that we've gotten all of the editor house-keeping out of the way we can finally // start creating world instances and multi player clients! { // First, we handle starting a dedicated server. This can exist as either a separate // process, or as an internal world. bool bUserWantsSingleProcess; InRequestParams.EditorPlaySettings->GetRunUnderOneProcess(bUserWantsSingleProcess); EPlayNetMode NetMode; InRequestParams.EditorPlaySettings->GetPlayNetMode(NetMode); // Standalone requires no server, and ListenServer doesn't require a separate server. const bool bNetModeRequiresSeparateServer = NetMode == EPlayNetMode::PIE_Client; const bool bLaunchExtraServerAnyways = InRequestParams.EditorPlaySettings->bLaunchSeparateServer; const bool bNeedsServer = bNetModeRequiresSeparateServer || bLaunchExtraServerAnyways; // If they require a separate server we'll give the EditorEngine a chance to handle any additional prep-work. if (bNeedsServer) { // Allow the engine to cancel the server request if needed. FGameInstancePIEResult PreCreateResult = PreCreatePIEServerInstance( ErroredBlueprints.Num() > 0, false /*bStartInSpectorMode*/, PIEStartTime, true, PlayInEditorSessionInfo->NumOutstandingPIELogins); if (!PreCreateResult.IsSuccess()) { // ToDo: This will skip client creation as well right now. Probably OK though. UE_LOG(LogPlayLevel, Warning, TEXT("PlayInEditor Session Server failed Pre-Create and will not be started.")); return; } // If they don't want single process we launch the server as a separate process. If they do // want single process, it will get handled below as part of client startup. if (!bUserWantsSingleProcess) { const bool bIsDedicatedServer = true; const bool bIsHost = true; const int32 InstanceIndex = 0; LaunchNewProcess(InRequestParams, InstanceIndex, EPlayNetMode::PIE_ListenServer, bIsDedicatedServer); PlayInEditorSessionInfo->bServerWasLaunched = true; } } // If control is pressed, start in spectator mode FModifierKeysState KeysState = FSlateApplication::Get().GetModifierKeys(); PlayInEditorSessionInfo->bStartedInSpectatorMode = InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor || KeysState.IsControlDown(); // Now that the dedicated server was (optionally) started, we'll start as many requested clients as we can. // Because the user indicated they wanted PIE/PINW we'll put the first client in the editor respecting that // setting. Any additional clients will either be in-process new windows, or separate processes based on settings. int32 NumClients; InRequestParams.EditorPlaySettings->GetPlayNumberOfClients(NumClients); // If the have a net mode that requires a server but they didn't create (or couldn't create due to single-process // limitations) a dedicated one, then we launch an extra world context acting as a server in-process. const bool bRequiresExtraListenServer = bNeedsServer && !PlayInEditorSessionInfo->bServerWasLaunched; int32 NumRequestedInstances = FMath::Max(NumClients, 1); if (bRequiresExtraListenServer) { NumRequestedInstances++; } for (int32 InstanceIndex = 0; InstanceIndex < NumRequestedInstances; InstanceIndex++) { // If they are running single-process and they need a server, the first instance will be the server. const bool bClientIsServer = (InstanceIndex == 0) && (NetMode == EPlayNetMode::PIE_ListenServer || bRequiresExtraListenServer); EPlayNetMode LocalNetMode = NetMode; // If they're the server, we want to override them to be a ListenServer. This will get ignored if they're secretly a dedicated // server so it's okay. if (bClientIsServer) { LocalNetMode = EPlayNetMode::PIE_ListenServer; } // If they want to launch a Listen Server and have multiple clients, the subsequent clients need to be // treated as Clients so they connect to the listen server instead of launching multiple Listen Servers. if (NetMode == EPlayNetMode::PIE_ListenServer && InstanceIndex > 0) { LocalNetMode = EPlayNetMode::PIE_Client; } bool bRunAsDedicated = bClientIsServer && bRequiresExtraListenServer; // Create the instance. This can end up creating separate processes if needed based on settings. // This code is separated out of here so it can be re-used by the Late Join flow. CreateNewPlayInEditorInstance(InRequestParams, bRunAsDedicated, LocalNetMode); // If there was an error creating an instance it will call EndPlay which invalidates the session info. // This also broadcasts the EndPIE event so no need to do that here (to make a matching call to our BeginPIE) if (!PlayInEditorSessionInfo.IsSet()) { return; } } // This needs to be run before ToggleBetweenPIEandSIE as it creates the list of selected actors. // It only re-selects the actors if you are entering SIE. TransferEditorSelectionToPlayInstances(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); // If they requested a SIE, we immediately convert the PIE into a SIE. This finds and executes on the first // world context, and doesn't support networking. if (!bUseOnlineSubsystemForLogin && InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor) { const bool bNewSession = true; ToggleBetweenPIEandSIE(bNewSession); } } // Disable the screen saver when PIE is running EnableScreenSaver(false); // Update the details window with the actors we have just selected GUnrealEd->UpdateFloatingPropertyWindows(); // Clean up any editor actors being referenced GEngine->BroadcastLevelActorListChanged(); // Set an undo barrier so that transactions prior to PIE can't be undone GUnrealEd->Trans->SetUndoBarrier(); // Notify that we've changed safe zone ratios for visualization. { FMargin SafeZoneRatio = EditorPlaySettings->PIESafeZoneOverride; SafeZoneRatio.Left /= (EditorPlaySettings->NewWindowWidth / 2.0f); SafeZoneRatio.Right /= (EditorPlaySettings->NewWindowWidth / 2.0f); SafeZoneRatio.Bottom /= (EditorPlaySettings->NewWindowHeight / 2.0f); SafeZoneRatio.Top /= (EditorPlaySettings->NewWindowHeight / 2.0f); FSlateApplication::Get().OnDebugSafeZoneChanged.Broadcast(SafeZoneRatio, false); } { // Temporarily set this information until the deprecated variables are removed. PRAGMA_DISABLE_DEPRECATION_WARNINGS bIsSimulatingInEditor = InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor; PRAGMA_ENABLE_DEPRECATION_WARNINGS } FEditorDelegates::PostPIEStarted.Broadcast(InRequestParams.WorldType == EPlaySessionWorldType::SimulateInEditor); } /** Creates an GameInstance with the given settings. A window is created if this isn't a server. */ UGameInstance* UEditorEngine::CreateInnerProcessPIEGameInstance(FRequestPlaySessionParams& InParams, const FGameInstancePIEParameters& InPIEParameters, int32 InPIEInstanceIndex) { // Create a GameInstance for this new instance. FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; UClass* GameInstanceClass = GameInstanceClassName.TryLoadClass(); // If an invalid class type was specified we fall back to the default. if (!GameInstanceClass) { GameInstanceClass = UGameInstance::StaticClass(); } UGameInstance* GameInstance = NewObject(this, GameInstanceClass); // We need to temporarily add the GameInstance to the root because the InitializeForPlayInEditor // call can do garbage collection wiping out the GameInstance GameInstance->AddToRoot(); // Attempt to initialize the GameInstance. This will construct the world. const bool bFirstWorld = !PlayWorld; const FGameInstancePIEResult InitializeResult = GameInstance->InitializeForPlayInEditor(InPIEInstanceIndex, InPIEParameters); if (!InitializeResult.IsSuccess()) { FMessageDialog::Open(EAppMsgType::Ok, InitializeResult.FailureReason); FEditorDelegates::EndPIE.Broadcast(InPIEParameters.bSimulateInEditor); FNavigationSystem::OnPIEEnd(*EditorWorld); GameInstance->RemoveFromRoot(); return nullptr; } // Our game instance was successfully created FWorldContext* const PieWorldContext = GameInstance->GetWorldContext(); check(PieWorldContext); PlayWorld = PieWorldContext->World(); // Temporarily set GWorld to our newly created world. This utility function // also sets GIsPlayInEditorWorld so that users can know if GWorld is actually // a PIE world or not. SetPlayInEditorWorld(PlayWorld); // Initialize a local player and viewport client for non-dedicated server instances. UGameViewportClient* ViewportClient = nullptr; ULocalPlayer* NewLocalPlayer = nullptr; TSharedPtr PIEViewport = nullptr; if (!InPIEParameters.bRunAsDedicated) { bool bCreateNewAudioDevice = InParams.EditorPlaySettings->IsCreateAudioDeviceForEveryPlayer(); // Create an instance of the Game Viewport Client, with the class specified by the Engine. ViewportClient = NewObject(this, GameViewportClientClass); ViewportClient->Init(*PieWorldContext, GameInstance, bCreateNewAudioDevice); ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault(); ViewportClient->EngineShowFlags.SetServerDrawDebug(PlayInSettings->ShowServerDebugDrawingByDefault()); if (!InParams.EditorPlaySettings->EnableGameSound) { if (FAudioDeviceHandle GameInstanceAudioDevice = GameInstance->GetWorld()->GetAudioDevice()) { GameInstanceAudioDevice->SetTransientMasterVolume(0.0f); } } GameViewport = ViewportClient; GameViewport->bIsPlayInEditorViewport = true; // Update our World Context to know which Viewport Client is associated. PieWorldContext->GameViewport = ViewportClient; // Add a callback for Game Input that isn't absorbed by the Game Viewport. This allows us to // make editor commands work (such as Shift F1, etc.) from within PIE. ViewportClient->OnGameViewportInputKey().BindUObject(this, &UEditorEngine::ProcessDebuggerCommands); // Listen for when the viewport is closed, so we can see about shutting down PIE. ViewportCloseRequestedDelegateHandle = ViewportClient->OnCloseRequested().AddUObject(this, &UEditorEngine::OnViewportCloseRequested); FSlatePlayInEditorInfo& SlatePlayInEditorSession = SlatePlayInEditorMap.Add(PieWorldContext->ContextHandle, FSlatePlayInEditorInfo()); // Might be invalid depending how pie was launched. Code below handles this if (InParams.DestinationSlateViewport.Get(nullptr).IsValid()) { SlatePlayInEditorSession.DestinationSlateViewport = InParams.DestinationSlateViewport.GetValue(); // Only one PIE Instance can live in a given viewport, so we'll null it out so that we create // windows instead for the remaining clients. InParams.DestinationSlateViewport = nullptr; } // Attempt to initialize a Local Player. FString Error; NewLocalPlayer = ViewportClient->SetupInitialLocalPlayer(Error); if (!NewLocalPlayer) { FMessageDialog::Open(EAppMsgType::Ok, FText::Format(NSLOCTEXT("UnrealEd", "Error_CouldntSpawnPlayer", "Couldn't spawn player: {0}"), FText::FromString(Error))); // go back to using the real world as GWorld RestoreEditorWorld(EditorWorld); EndPlayMap(); GameInstance->RemoveFromRoot(); return nullptr; } // A Local Player gets created even in SIE (which is different than a Player Controller), but we only // store a reference if we're PIE for the UI to know where to restore our viewport location after PIE closes. if (!InPIEParameters.bSimulateInEditor) { SlatePlayInEditorSession.EditorPlayer = NewLocalPlayer; } // Note: For K2 debugging purposes this MUST be created before beginplay is called because beginplay can trigger breakpoints // and we need to be able to refocus the pie viewport afterwards so it must be created first in order for us to find it { // If the original request provided a Slate Viewport, we'll use that for our output. if (SlatePlayInEditorSession.DestinationSlateViewport.IsValid()) { TSharedPtr LevelViewportRef = SlatePlayInEditorSession.DestinationSlateViewport.Pin(); LevelViewportRef->StartPlayInEditorSession(ViewportClient, InParams.WorldType == EPlaySessionWorldType::SimulateInEditor); // We count this as a viewport being created so that subsequent clients won't think they're the 'first' and use the wrong setting. PlayInEditorSessionInfo->NumViewportInstancesCreated++; } else { // Generate a new Window to put this instance in. PIEViewport = GeneratePIEViewportWindow(InParams, PlayInEditorSessionInfo->NumViewportInstancesCreated, *PieWorldContext, InPIEParameters.NetMode, ViewportClient, SlatePlayInEditorSession); // Increment for each viewport so that the window titles get correct numbers and it uses the right save/load setting. Non-visible // servers won't be bumping this number as it's used for saving/restoring window positions. PlayInEditorSessionInfo->NumViewportInstancesCreated++; } // Broadcast that the Viewport has been successfully created. UGameViewportClient::OnViewportCreated().Broadcast(); } // Mark the Viewport as a PIE Viewport if (GameViewport && GameViewport->Viewport) { GameViewport->Viewport->SetPlayInEditorViewport(true); } if (InParams.EditorPlaySettings->bUseNonRealtimeAudioDevice && AudioDeviceManager) { UE_LOG(LogPlayLevel, Log, TEXT("Creating new non-realtime audio mixer")); FAudioDeviceParams DeviceParams = AudioDeviceManager->GetDefaultParamsForNewWorld(); DeviceParams.Scope = EAudioDeviceScope::Unique; DeviceParams.AssociatedWorld = PlayWorld; DeviceParams.bIsNonRealtime = true; FAudioDeviceHandle AudioDevice = AudioDeviceManager->RequestAudioDevice(DeviceParams); check(AudioDevice.IsValid()); if (PlayWorld) { PlayWorld->SetAudioDevice(AudioDevice); } } } // By this point it is safe to remove the GameInstance from the root and allow it to garbage collected as per usual GameInstance->RemoveFromRoot(); // If the request wanted to override the game mode we have to do that here while we still have specifics about // the request. This will allow if (InParams.GameModeOverride) { GameInstance->GetWorld()->GetWorldSettings()->DefaultGameMode = InParams.GameModeOverride; } // Transfer the Blueprint Debug references to the first client world that is created. This needs to be called before // GameInstance->StartPlayInEditorGameInstance so that references are transfered by the time BeginPlay is called. if (bFirstWorld && PlayWorld) { EditorWorld->TransferBlueprintDebugReferences(PlayWorld); } FGameInstancePIEResult StartResult = FGameInstancePIEResult::Success(); { FTemporaryPlayInEditorIDOverride OverrideIDHelper(InPIEInstanceIndex); StartResult = GameInstance->StartPlayInEditorGameInstance(NewLocalPlayer, InPIEParameters); } if (!StartResult.IsSuccess()) { FMessageDialog::Open(EAppMsgType::Ok, StartResult.FailureReason); RestoreEditorWorld(EditorWorld); EndPlayMap(); return nullptr; } EnableWorldSwitchCallbacks(true); if (PIEViewport.IsValid()) { // Register the new viewport widget with Slate for viewport specific message routing. FSlateApplication::Get().RegisterGameViewport(PIEViewport.ToSharedRef()); } // Go back to using the editor world as GWorld. RestoreEditorWorld(EditorWorld); return GameInstance; } FText GeneratePIEViewportWindowTitle(const EPlayNetMode InNetMode, const ERHIFeatureLevel::Type InFeatureLevel, const FRequestPlaySessionParams& InSessionParams, const int32 ClientIndex, const float FixedTick) { #if PLATFORM_64BITS const FString PlatformBitsString(TEXT("64")); #else const FString PlatformBitsString(TEXT("32")); #endif const FText WindowTitleOverride = GetDefault()->ProjectDisplayedTitle; FFormatNamedArguments Args; Args.Add(TEXT("GameName"), FText::FromString(FString(WindowTitleOverride.IsEmpty() ? FApp::GetProjectName() : WindowTitleOverride.ToString()))); Args.Add(TEXT("PlatformBits"), FText::FromString(PlatformBitsString)); Args.Add(TEXT("RHIName"), FText::FromName(ShaderPlatformToPlatformName(GetFeatureLevelShaderPlatform(InFeatureLevel)))); if (InNetMode == PIE_Client) { Args.Add(TEXT("NetMode"), FText::FromString(FString::Printf(TEXT("Client %d"), ClientIndex))); } else if (InNetMode == PIE_ListenServer) { Args.Add(TEXT("NetMode"), FText::FromString(TEXT("Server"))); } else { Args.Add(TEXT("NetMode"), FText::FromString(TEXT("Standalone"))); } if (GEngine->StereoRenderingDevice && GEngine->StereoRenderingDevice.IsValid() && InSessionParams.SessionPreviewTypeOverride == EPlaySessionPreviewType::VRPreview && GEngine->XRSystem && GEngine->XRSystem.IsValid()) { Args.Add(TEXT("XRSystemName"), FText::FromName(GEngine->XRSystem->GetSystemName())); } else { Args.Add(TEXT("XRSystemName"), FText::GetEmpty()); } if (FixedTick > 0.f) { int32 FixedFPS = (int32)(1.f / FixedTick); Args.Add(TEXT("FixedFPS"), FText::FromString(FString::Printf(TEXT("Fixed %dfps"), FixedFPS))); } else { Args.Add(TEXT("FixedFPS"), FText::GetEmpty()); } return FText::Format(NSLOCTEXT("UnrealEd", "PlayInEditor_WindowTitleFormat", "{GameName} Preview [NetMode: {NetMode}] {FixedFPS} ({PlatformBits}-bit/{RHIName}) {XRSystemName}"), Args); } void UEditorEngine::TransferEditorSelectionToPlayInstances(const bool bInSelectInstances) { // Make a list of all the selected actors TArray SelectedActors; TArray SelectedComponents; for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); if (Actor) { checkSlow(Actor->IsA(AActor::StaticClass())); SelectedActors.Add(Actor); } } // Unselect everything GEditor->SelectNone(true, true, false); GetSelectedActors()->DeselectAll(); GetSelectedObjects()->DeselectAll(); GetSelectedComponents()->DeselectAll(); ActorsThatWereSelected.Empty(); // For every actor that was selected previously, make sure it's sim equivalent is selected for (int32 ActorIndex = 0; ActorIndex < SelectedActors.Num(); ++ActorIndex) { AActor* Actor = Cast(SelectedActors[ActorIndex]); if (Actor) { ActorsThatWereSelected.Add(Actor); AActor* SimActor = EditorUtilities::GetSimWorldCounterpartActor(Actor); if (SimActor && !SimActor->IsHidden()) { SelectActor(SimActor, bInSelectInstances, false); } } } } TSharedRef UEditorEngine::GeneratePIEViewportWindow(const FRequestPlaySessionParams& InSessionParams, int32 InViewportIndex, const FWorldContext& InWorldContext, EPlayNetMode InNetMode, UGameViewportClient* InViewportClient, FSlatePlayInEditorInfo& InSlateInfo) { FIntPoint WindowSize, WindowPosition; GetWindowSizeAndPositionForInstanceIndex(*InSessionParams.EditorPlaySettings, InViewportIndex, WindowSize, WindowPosition); bool bCenterNewWindowOverride = false; // VR Preview overrides window location. const bool bVRPreview = InSessionParams.SessionPreviewTypeOverride.Get(EPlaySessionPreviewType::NoPreview) == EPlaySessionPreviewType::VRPreview; bool bUseOSWndBorder = bVRPreview; if (bVRPreview) { bCenterNewWindowOverride = true; } // Check to see if they've provided a custom SWindow for us to place our Session in. TSharedPtr PieWindow = InSessionParams.CustomPIEWindow.Pin(); const bool bHasCustomWindow = PieWindow.IsValid(); // If they haven't provided a Slate Window (common), we will create one. if (!bHasCustomWindow) { FText ViewportName = GeneratePIEViewportWindowTitle(InNetMode, PreviewPlatform.GetEffectivePreviewFeatureLevel(), InSessionParams, InWorldContext.PIEInstance, InWorldContext.PIEFixedTickSeconds); PieWindow = SNew(SWindow) .Title(ViewportName) .ScreenPosition(FVector2D(WindowPosition.X, WindowPosition.Y)) .ClientSize(FVector2D(WindowSize.X, WindowSize.Y)) .AutoCenter(bCenterNewWindowOverride ? EAutoCenter::PreferredWorkArea : EAutoCenter::None) .UseOSWindowBorder(bUseOSWndBorder) .SaneWindowPlacement(!bCenterNewWindowOverride) .SizingRule(ESizingRule::UserSized) .AdjustInitialSizeAndPositionForDPIScale(false); PieWindow->SetAllowFastUpdate(true); } // Setup a delegate for switching to the play world on slate input events, drawing and ticking FOnSwitchWorldHack OnWorldSwitch = FOnSwitchWorldHack::CreateUObject(this, &UEditorEngine::OnSwitchWorldForSlatePieWindow, InWorldContext.PIEInstance); PieWindow->SetOnWorldSwitchHack(OnWorldSwitch); if (!bHasCustomWindow) { // Mac does not support parenting, do not keep on top #if PLATFORM_MAC FSlateApplication::Get().AddWindow(PieWindow.ToSharedRef()); #else TSharedRef MainWindow = FModuleManager::LoadModuleChecked(TEXT("MainFrame")).GetParentWindow().ToSharedRef(); if (InSessionParams.EditorPlaySettings->PIEAlwaysOnTop) { FSlateApplication::Get().AddWindowAsNativeChild(PieWindow.ToSharedRef(), MainWindow, true); } else { FSlateApplication::Get().AddWindow(PieWindow.ToSharedRef()); } #endif } TSharedRef ViewportOverlayWidgetRef = SNew(SOverlay); TSharedRef GameLayerManagerRef = SNew(SGameLayerManager) .SceneViewport_UObject(this, &UEditorEngine::GetGameSceneViewport, InViewportClient) [ViewportOverlayWidgetRef]; bool bRenderDirectlyToWindow = bVRPreview; bool bEnableStereoRendering = bVRPreview && (/* only first PIE instance can be VR */ InViewportIndex == 0); static const auto CVarPropagateAlpha = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.PostProcessing.PropagateAlpha")); const EAlphaChannelMode::Type PropagateAlpha = EAlphaChannelMode::FromInt(CVarPropagateAlpha->GetValueOnGameThread()); const bool bIgnoreTextureAlpha = (PropagateAlpha != EAlphaChannelMode::AllowThroughTonemapper); TSharedRef PieViewportWidget = SNew(SPIEViewport) .RenderDirectlyToWindow(bRenderDirectlyToWindow) .EnableStereoRendering(bEnableStereoRendering) .IgnoreTextureAlpha(bIgnoreTextureAlpha) [GameLayerManagerRef]; // Create a wrapper widget for PIE viewport to process play world actions TSharedRef GlobalPlayWorldActionsWidgetRef = SNew(SGlobalPlayWorldActions) [PieViewportWidget]; PieWindow->SetContent(GlobalPlayWorldActionsWidgetRef); if (!bHasCustomWindow) { // Ensure the PIE window appears does not appear behind other windows. PieWindow->BringToFront(); } InViewportClient->SetViewportOverlayWidget(PieWindow, ViewportOverlayWidgetRef); InViewportClient->SetGameLayerManager(GameLayerManagerRef); bool bShouldMinimizeRootWindow = bVRPreview && GEngine->XRSystem.IsValid() && InSessionParams.EditorPlaySettings->ShouldMinimizeEditorOnVRPIE; // Set up a notification when the window is closed so we can clean up PIE { struct FLocal { static void OnPIEWindowClosed(const TSharedRef& WindowBeingClosed, TWeakPtr PIEViewportWidget, TWeakObjectPtr OwningEditorEngine, int32 ViewportIndex, bool bRestoreRootWindow) { // Save off the window position const FVector2D PIEWindowPos = WindowBeingClosed->GetLocalToScreenTransform().GetTranslation(); FIntPoint WindowSize = FIntPoint(WindowBeingClosed->GetClientSizeInScreen().X, WindowBeingClosed->GetClientSizeInScreen().Y); FIntPoint WindowPosition = FIntPoint(PIEWindowPos.X, PIEWindowPos.Y); if (OwningEditorEngine.IsValid()) { OwningEditorEngine->StoreWindowSizeAndPositionForInstanceIndex(ViewportIndex, WindowSize, WindowPosition); } // Route the callback PIEViewportWidget.Pin()->OnWindowClosed(WindowBeingClosed); if (bRestoreRootWindow) { // restore previously minimized root window. TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); if (RootWindow.IsValid() && RootWindow->IsWindowMinimized()) { RootWindow->Restore(); } } } }; PieWindow->SetOnWindowClosed(FOnWindowClosed::CreateStatic(&FLocal::OnPIEWindowClosed, TWeakPtr(PieViewportWidget), TWeakObjectPtr(this), InViewportIndex, bShouldMinimizeRootWindow)); } // Create a new viewport that the viewport widget will use to render the game InSlateInfo.SlatePlayInEditorWindowViewport = MakeShared(InViewportClient, PieViewportWidget); GameLayerManagerRef->SetSceneViewport(InSlateInfo.SlatePlayInEditorWindowViewport.Get()); const bool bShouldGameGetMouseControl = InSessionParams.EditorPlaySettings->GameGetsMouseControl || (bEnableStereoRendering && GEngine && GEngine->XRSystem.IsValid()); InSlateInfo.SlatePlayInEditorWindowViewport->SetPlayInEditorGetsMouseControl(bShouldGameGetMouseControl); PieViewportWidget->SetViewportInterface(InSlateInfo.SlatePlayInEditorWindowViewport.ToSharedRef()); FSlateApplication::Get().RegisterViewport(PieViewportWidget); InSlateInfo.SlatePlayInEditorWindow = PieWindow; // Let the viewport client know what viewport is using it. We need to set the Viewport Frame as // well (which in turn sets the viewport) so that SetRes command will work. InViewportClient->SetViewportFrame(InSlateInfo.SlatePlayInEditorWindowViewport.Get()); // Mark the viewport as PIE viewport InViewportClient->Viewport->SetPlayInEditorViewport(InViewportClient->bIsPlayInEditorViewport); // Change the system resolution to match our window, to make sure game and slate window are kept synchronized FSystemResolution::RequestResolutionChange(WindowSize.X, WindowSize.Y, EWindowMode::Windowed); const bool bHMDIsReady = (GEngine && GEngine->XRSystem.IsValid() && GEngine->XRSystem->GetHMDDevice() && GEngine->XRSystem->GetHMDDevice()->IsHMDConnected()); if (bVRPreview && bHMDIsReady) { GEngine->StereoRenderingDevice->EnableStereo(true); // minimize the root window to provide max performance for the preview. TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); if (RootWindow.IsValid() && bShouldMinimizeRootWindow) { RootWindow->Minimize(); } } return PieViewportWidget; } /* fits the window position to make sure it falls within the confines of the desktop */ void FitWindowPositionToWorkArea(FIntPoint& WinPos, FIntPoint& WinSize, const FMargin& WinPadding) { const int32 HorzPad = WinPadding.GetTotalSpaceAlong(); const int32 VertPad = WinPadding.GetTotalSpaceAlong(); FIntPoint TotalSize(WinSize.X + HorzPad, WinSize.Y + VertPad); FDisplayMetrics DisplayMetrics; FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); // Limit the size, to make sure it fits within the desktop area { FIntPoint NewWinSize; NewWinSize.X = FMath::Min(TotalSize.X, DisplayMetrics.VirtualDisplayRect.Right - DisplayMetrics.VirtualDisplayRect.Left); NewWinSize.Y = FMath::Min(TotalSize.Y, DisplayMetrics.VirtualDisplayRect.Bottom - DisplayMetrics.VirtualDisplayRect.Top); if (NewWinSize != TotalSize) { TotalSize = NewWinSize; WinSize.X = NewWinSize.X - HorzPad; WinSize.Y = NewWinSize.Y - VertPad; } } const FSlateRect PreferredWorkArea(DisplayMetrics.VirtualDisplayRect.Left, DisplayMetrics.VirtualDisplayRect.Top, DisplayMetrics.VirtualDisplayRect.Right - TotalSize.X, DisplayMetrics.VirtualDisplayRect.Bottom - TotalSize.Y); // if no more windows fit horizontally, place them in a new row if (WinPos.X > PreferredWorkArea.Right) { WinPos.X = PreferredWorkArea.Left; WinPos.Y += TotalSize.Y; if (WinPos.Y > PreferredWorkArea.Bottom) { WinPos.Y = PreferredWorkArea.Top; } } // if no more rows fit vertically, stack windows on top of each other else if (WinPos.Y > PreferredWorkArea.Bottom) { WinPos.Y = PreferredWorkArea.Top; WinPos.X += TotalSize.X; if (WinPos.X > PreferredWorkArea.Right) { WinPos.X = PreferredWorkArea.Left; } } // Clamp values to make sure they fall within the desktop area WinPos.X = FMath::Clamp(WinPos.X, (int32)PreferredWorkArea.Left, (int32)PreferredWorkArea.Right); WinPos.Y = FMath::Clamp(WinPos.Y, (int32)PreferredWorkArea.Top, (int32)PreferredWorkArea.Bottom); } void UEditorEngine::GetWindowSizeAndPositionForInstanceIndex(ULevelEditorPlaySettings& InEditorPlaySettings, const int32 InInstanceIndex, FIntPoint& OutSize, FIntPoint& OutPosition) { if (!ensureMsgf(PlayInEditorSessionInfo.IsSet(), TEXT("Cannot get saved Window Size/Position if a session has not been started."))) { OutSize = FIntPoint(1280, 720); OutPosition = FIntPoint(0, 0); return; } FMargin WindowBorderSize(8.0f, 30.0f, 8.0f, 8.0f); TSharedPtr TopLevelWindow = FSlateApplication::Get().GetActiveTopLevelWindow(); if (TopLevelWindow.IsValid()) { WindowBorderSize = TopLevelWindow->GetWindowBorderSize(true); } // Alright, they don't have a saved position or don't want to load from the saved position. First, figure out // how big the window should be. If it is the first client, it uses a different resolution source than additional. if (InInstanceIndex == 0) { OutSize = FIntPoint(InEditorPlaySettings.NewWindowWidth, InEditorPlaySettings.NewWindowHeight); } else { // Use the size for additional client windows. InEditorPlaySettings.GetClientWindowSize(OutSize); } // Figure out how big the users resolution is FDisplayMetrics DisplayMetrics; FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); const FVector2D DisplaySize = FVector2D( DisplayMetrics.PrimaryDisplayWorkAreaRect.Right - DisplayMetrics.PrimaryDisplayWorkAreaRect.Left, DisplayMetrics.PrimaryDisplayWorkAreaRect.Bottom - DisplayMetrics.PrimaryDisplayWorkAreaRect.Top); // If the size is zero then they want us to auto-detect the resolution. if (OutSize.X <= 0 || OutSize.Y <= 0) { OutSize.X = FMath::RoundToInt(0.75f * DisplaySize.X); OutSize.Y = FMath::RoundToInt(0.75f * DisplaySize.Y); } // Now we can position the window. If it is the first window, we can respect the center window flag. if (InInstanceIndex == 0) { // Center window if CenterNewWindow checked or if NewWindowPosition is FIntPoint::NoneValue (-1,-1) if (InEditorPlaySettings.CenterNewWindow || InEditorPlaySettings.NewWindowPosition == FIntPoint::NoneValue) { // We don't store the last window position in this case, because we want additional windows // to open starting at the top left of the monitor. OutPosition.X = FMath::RoundToInt((DisplaySize.X / 2.f) - (OutSize.X / 2)); OutPosition.Y = FMath::RoundToInt((DisplaySize.Y / 2.f) - (OutSize.Y / 2)); } else { OutPosition = InEditorPlaySettings.NewWindowPosition; } } else { if (InInstanceIndex < PlayInEditorSessionInfo->CachedWindowInfo.Num()) { OutPosition = PlayInEditorSessionInfo->CachedWindowInfo[InInstanceIndex].Position; FitWindowPositionToWorkArea(OutPosition, OutSize, WindowBorderSize); } // Add a new entry. else { // We bump the position to go to the right, and the clamp will auto-wrap it for us if it falls off screen. OutPosition = PlayInEditorSessionInfo->LastOpenedWindowInfo.Position + FIntPoint(PlayInEditorSessionInfo->LastOpenedWindowInfo.Size.X, 0); // We're opening multiple windows. We're going to calculate a new position (opening them FitWindowPositionToWorkArea(OutPosition, OutSize, WindowBorderSize); // Store this position as the 'last opened' position. This means additional windows will start to // the right of this, unless they run out of room at which point they'll start over on the next row PlayInEditorSessionInfo->LastOpenedWindowInfo.Size = OutSize; PlayInEditorSessionInfo->LastOpenedWindowInfo.Position = OutPosition; } } // Store this Size/Position for this duration StoreWindowSizeAndPositionForInstanceIndex(InInstanceIndex, OutSize, OutPosition); } void UEditorEngine::StoreWindowSizeAndPositionForInstanceIndex(const int32 InInstanceIndex, const FIntPoint& InSize, const FIntPoint& InPosition) { // Overwrite an existing one if we have it if (InInstanceIndex < PlayInEditorSessionInfo->CachedWindowInfo.Num()) { PlayInEditorSessionInfo->CachedWindowInfo[InInstanceIndex].Size = InSize; PlayInEditorSessionInfo->CachedWindowInfo[InInstanceIndex].Position = InPosition; } else { FPlayInEditorSessionInfo::FWindowSizeAndPos& NewInfo = PlayInEditorSessionInfo->CachedWindowInfo.Add_GetRef(FPlayInEditorSessionInfo::FWindowSizeAndPos()); NewInfo.Size = InSize; NewInfo.Position = InPosition; } } bool PromptMatineeClose() { if (GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit)) { const bool bContinuePIE = EAppReturnType::Yes == FMessageDialog::Open(EAppMsgType::YesNo, NSLOCTEXT("UnrealEd", "PIENeedsToCloseMatineeQ", "'Play in Editor' must close UnrealMatinee. Continue?")); if (!bContinuePIE) { return false; } GLevelEditorModeTools().DeactivateMode(FBuiltinEditorModes::EM_InterpEdit); } return true; } // Deprecated Stubs PRAGMA_DISABLE_DEPRECATION_WARNINGS UGameInstance* UEditorEngine::CreatePIEGameInstance(int32 InPIEInstance, bool bInSimulateInEditor, bool bAnyBlueprintErrors, bool bStartInSpectatorMode, bool bPlayNetDedicated, bool bPlayStereoscopic, float PIEStartTime) { return nullptr; } void UEditorEngine::LoginPIEInstances(bool bAnyBlueprintErrors, bool bStartInSpectatorMode, double PIEStartTime) {} void UEditorEngine::OnLoginPIEAllComplete() {} void UEditorEngine::PlayInEditor(UWorld* InWorld, bool bInSimulateInEditor, FPlayInEditorOverrides Overrides /* = FPlayInEditorOverrides() */) {} PRAGMA_ENABLE_DEPRECATION_WARNINGS #undef LOCTEXT_NAMESPACE