EM_Task/UnrealEd/Private/PlayLevel.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

3395 lines
146 KiB
C++

// 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<bool> 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<UObject*> SelectedActors;
if (ActorsThatWereSelected.Num() > 0)
{
for (int32 ActorIndex = 0; ActorIndex < ActorsThatWereSelected.Num(); ++ActorIndex)
{
TWeakObjectPtr<AActor> Actor = ActorsThatWereSelected[ActorIndex].Get();
if (Actor.IsValid())
{
SelectedActors.Add(Actor.Get());
}
}
ActorsThatWereSelected.Empty();
}
else
{
for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It)
{
AActor* Actor = static_cast<AActor*>(*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<AActor>(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<FName> OnlineIdentifiers;
TArray<UWorld*> 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<UWorld> 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<UAssetEditorSubsystem>()->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<UWorld>(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<UWorld>(LevelStreaming->GetLoadedLevel()->GetOuter())->MarkObjectsPendingKill();
}
}
}
// mark all objects contained within the PIE game instances to be deleted
for (TObjectIterator<UGameInstance> 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<ANote>(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<UAudioComponent> 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<ULevelEditorPlaySettings>();
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<FName> 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<IAssetViewport> 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<UWorld>(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<UWorld>(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<IBlueprintEditor*> Editors;
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
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<IBlueprintEditor*>(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<IAssetViewport> 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<FLevelEditorViewportClient&>(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<ULevelEditorPlaySettings>();
}
// 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<ULevelEditorPlaySettings>(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<class IAssetViewport> 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<UBlueprint*>& ErroredBlueprints, const bool bForceLevelScriptRecompile)
{
struct FLocal
{
static void OnMessageLogLinkActivated(const class TSharedRef<IMessageToken>& Token)
{
if (Token->GetType() == EMessageToken::Object)
{
const TSharedRef<FUObjectToken> UObjectToken = StaticCastSharedRef<FUObjectToken>(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<FTokenizedMessage> 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<UBlueprint*> InNeedOfRecompile;
ErroredBlueprints.Empty();
FMessageLog BlueprintLog("BlueprintLog");
double BPRegenStartTime = FPlatformTime::Seconds();
for (TObjectIterator<UBlueprint> 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<UBlueprint*> 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<AActor>(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<ANavigationObjectBase>(PlayerStart);
if (NavPlayerStart)
{
NavPlayerStart->bIsPIEPlayerStart = true;
}
return true;
}
static bool ShowBlueprintErrorDialog(TArray<UBlueprint*> 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<UBlueprint> InBlueprint, TSharedPtr<SCustomDialog> 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<SVerticalBox> 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<SCustomDialog> CustomDialog;
for (UBlueprint* Blueprint: ErroredBlueprints)
{
TWeakObjectPtr<UBlueprint> 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<int32>::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<FLevelEditorModule>(TEXT("LevelEditor"));
TSharedPtr<IAssetViewport> 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<IAssetViewport> 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<IAssetViewport> 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<FPhysicsManipulationEdModeFactory> 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<TWeakObjectPtr<class AActor>> 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<AActor> 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<bool> 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<SViewport> ViewportWidget = SceneViewport->GetViewportWidget().Pin().ToSharedRef();
TSharedPtr<SWindow> 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<ULevelEditorPlaySettings>();
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<ULevelEditorPlaySettings>();
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<IPIEAuthorizer*> PlayAuthorizers = IModularFeatures::Get().GetModularFeatureImplementations<IPIEAuthorizer>(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<UBlueprint*> 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<UEditorStyleSettings>()->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<UGameMapsSettings>()->GameInstanceClass;
UClass* GameInstanceClass = GameInstanceClassName.TryLoadClass<UGameInstance>();
// If an invalid class type was specified we fall back to the default.
if (!GameInstanceClass)
{
GameInstanceClass = UGameInstance::StaticClass();
}
UGameInstance* GameInstance = NewObject<UGameInstance>(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<SPIEViewport> 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<UGameViewportClient>(this, GameViewportClientClass);
ViewportClient->Init(*PieWorldContext, GameInstance, bCreateNewAudioDevice);
ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault<ULevelEditorPlaySettings>();
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<IAssetViewport> 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<UGeneralProjectSettings>()->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<UObject*> SelectedActors;
TArray<UObject*> SelectedComponents;
for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It)
{
AActor* Actor = static_cast<AActor*>(*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<AActor>(SelectedActors[ActorIndex]);
if (Actor)
{
ActorsThatWereSelected.Add(Actor);
AActor* SimActor = EditorUtilities::GetSimWorldCounterpartActor(Actor);
if (SimActor && !SimActor->IsHidden())
{
SelectActor(SimActor, bInSelectInstances, false);
}
}
}
}
TSharedRef<SPIEViewport> 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<SWindow> 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<SWindow, ESPMode::Fast> MainWindow = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame")).GetParentWindow().ToSharedRef();
if (InSessionParams.EditorPlaySettings->PIEAlwaysOnTop)
{
FSlateApplication::Get().AddWindowAsNativeChild(PieWindow.ToSharedRef(), MainWindow, true);
}
else
{
FSlateApplication::Get().AddWindow(PieWindow.ToSharedRef());
}
#endif
}
TSharedRef<SOverlay> ViewportOverlayWidgetRef = SNew(SOverlay);
TSharedRef<SGameLayerManager> 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<SPIEViewport> PieViewportWidget =
SNew(SPIEViewport)
.RenderDirectlyToWindow(bRenderDirectlyToWindow)
.EnableStereoRendering(bEnableStereoRendering)
.IgnoreTextureAlpha(bIgnoreTextureAlpha)
[GameLayerManagerRef];
// Create a wrapper widget for PIE viewport to process play world actions
TSharedRef<SGlobalPlayWorldActions> 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<SWindow>& WindowBeingClosed, TWeakPtr<SViewport> PIEViewportWidget, TWeakObjectPtr<UEditorEngine> 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<SWindow> RootWindow = FGlobalTabmanager::Get()->GetRootWindow();
if (RootWindow.IsValid() && RootWindow->IsWindowMinimized())
{
RootWindow->Restore();
}
}
}
};
PieWindow->SetOnWindowClosed(FOnWindowClosed::CreateStatic(&FLocal::OnPIEWindowClosed, TWeakPtr<SViewport>(PieViewportWidget), TWeakObjectPtr<UEditorEngine>(this),
InViewportIndex, bShouldMinimizeRootWindow));
}
// Create a new viewport that the viewport widget will use to render the game
InSlateInfo.SlatePlayInEditorWindowViewport = MakeShared<FSceneViewport>(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<SWindow> 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<Orient_Horizontal>();
const int32 VertPad = WinPadding.GetTotalSpaceAlong<Orient_Vertical>();
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<SWindow> 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