Crash in the editor when using GEngine->Browse() or GEngine->LoadMap()

Hi,

We want to use the GEngine->LoadMap() function in order to immediately change the world, however it has issues in the editor (works fine otherwise)

Seems to be failing check()

check( CurrentGWorld != PlayWorld || bIsSimulatingInEditor );

Steps to Reproduce

  1. Open the uproject
  2. Play in the editor
  3. Crash

Hi Ivaylo,

Sorry about the delay. Functions UEngine::Browse() and UEngine::LoadMap() are quite low level and called as part of higher-level operations. I wouldn’t recommend calling them directly on their own. Instead, I suggest you call UGameplayStatics::OpenLevel() to switch levels, it should work both in the editor and in a packaged game.

Please give it a try and let me know if it works for you.

Best regards,

Vitor

Hi

UGameplayStatics::OpenLevel() uses SetClientTravel() which doesn’t change the world immediately, but works asynchronously. Is it possible after calling SetClientTravel() to somehow wait manually to trigger its functionality?

Thanks,

Ivo

Hi Ivaylo,

I am looking into how SetClientTravel(), Browse() and LoadMap() work to see if I can find a way to achieve an immediate world change in the Editor. Meanwhile, could you please provide some more information about your use case and requirements? For example:

  • Do you need this to happen on PostEditorTick as in the repro project? Is there otherwise another restriction on when and where the level load needs to happen?
  • Why do you need the world to be changed immediately? What do you need to keep from happening between the level load request and the moment when it is loaded and shown?

Best regards,

Vitor

Hi Ivaylo,

I dug into the source code of several level loading functions, including SetClientTravel(), TickWorldTravel(), Browse() and LoadMap(), and I believe I figured out what’s happening.

When running in the Editor, starting a PIE session will set UEditorEngine’s “EditorWorld” to the original Level Editor world, and “PlayWorld” to the newly created PIE world. However, the global “GWorld” variable is kind of troublesome, because some parts of the engine need it to point to “EditorWorld”, while others assume that it points to “PlayWorld”. In particular, when the high-level UEditorEngine::Tick() function is called during PIE, GWorld needs to point to EditorWorld, and the check() in its beginning tries to make sure that is the case. Inside it, some code paths need GWorld to point to PlayWorld, and those are usually placed inside a pair of calls to SetPlayInEditorWorld() and RestoreEditorWorld().

The indirect level loading mechanism offered by UEngine::SetClientTravel() schedules the level load to start on the next tick. At that time, function UEngine::TickWorldTravel() is called by UEditorEngine::Tick() like this:

UWorld* OldGWorld = SetPlayInEditorWorld(PlayWorld);
(...)
TickWorldTravel(PieContext, TickDeltaSeconds);
(...)
RestoreEditorWorld(OldGWorld);

Inside, UEngine::TickWorldTravel() calls UEngine::Browse(), which in turn calls UEngine::LoadMap(), which among several other duties, updates PlayWorld and GWorld. But the change to GWorld is reverted by the time UEditorEngine::Tick() returns, which keeps things consistent.

Based on the above, I successfully tested the following method to load a level at a spot similar to your test project (but inside an Editor module to have access to the UnrealEd module dependency):

UWorld* BackupGWorld = SetPlayInEditorWorld(OldPlayWorld);
GEngine->Browse(OldWorldContext, NewURL, Error);
RestoreEditorWorld(BackupGWorld);

Alternative approach

I also noticed that the new PIE world is created inside UGameInstance::InitializeForPlayInEditor() by using UEditorEngine::CreatePIEWorldByDuplication(). At that point, the world that gets duplicated can actually be overriden by setting FGameInstancePIEParameters::OverrideMapURL, which ultimately comes from FRequestPlaySessionParams::GlobalMapOverride. Structure FRequestPlaySessionParams is created by FInternalPlayWorldCommandCallbacks::PlayInViewport_Clicked() when the user clicks the PIE button. From there, either FRequestPlaySessionParams or FGameInstancePIEParameters gets passed around through several UEditorEngine functions. Some of those are virtual, so they offer an opportunity for a subclass to override their behavior and make changes to GlobalMapOverride or OverrideMapURL.

The following virtual functions are interesting candidates, and I was able to make “Map2” load when starting PIE on “Map1” by overriding any one of them:

virtual UEditorEngine::StartQueuedPlaySessionRequestImpl()
virtual UEditorEngine::StartPlayInEditorSession(FRequestPlaySessionParams&)
virtual UEditorEngine::CreateNewPlayInEditorInstance(FRequestPlaySessionParams&)
virtual UEditorEngine::OnLoginPIEComplete_Deferred(FPieLoginStruct)
virtual UEditorEngine::CreateInnerProcessPIEGameInstance(const FGameInstancePIEParameters&)

To create your own Editor Engine class and configure your project to use it, you can do as follows:

/// MyEditorEngine.h ///
 
#include "UnrealEd.h"
#include "MyEditorEngine.generated.h"
 
UCLASS()
class MYEDITORMODULE_API UMyEditorEngine : public UUnrealEdEngine
{
	GENERATED_BODY()
public:
	//virtual void StartQueuedPlaySessionRequestImpl() override;
	virtual void StartPlayInEditorSession(FRequestPlaySessionParams& InRequestParams) override;
	//virtual void CreateNewPlayInEditorInstance(FRequestPlaySessionParams& InRequestParams, const bool bInDedicatedInstance, const EPlayNetMode InNetMode) override;
	//virtual void OnLoginPIEComplete_Deferred(int32 LocalUserNum, bool bWasSuccessful, FString ErrorString, FPieLoginStruct DataStruct) override;
	//virtual UGameInstance* CreateInnerProcessPIEGameInstance(FRequestPlaySessionParams& InParams, const FGameInstancePIEParameters& InPIEParameters, int32 InPIEInstanceIndex) override;
};
/// MyEditorEngine.cpp ///
 
#include "MyEditorEngine.h"
 
// This is one example of override, others would be similar
void UMyEditorEngine::StartPlayInEditorSession(FRequestPlaySessionParams& InRequestParams)
{
	InRequestParams.GlobalMapOverride = TEXT("/Game/Map2");
	Super::StartPlayInEditorSession(InRequestParams);
}
/// DefaultEngine.ini ///
 
[/Script/Engine.Engine]
UnrealEdEngine=/Script/MyEditorModule.MyEditorEngine

I hope this is helpful. Please let me know if one of these solutions works for you.

All the best,

Vitor