GIsPlayInEditorWorld is false during PostLoad of an actor within the duplicated PIE world

We have found that in UE5.8 the behavior or FApp::IsGame while running a PIE world is inconsistent.

We have an automated test that captures some primitive data at an initial player location, then teleports to a new location to capture other primitive data. This is accomplished with a level Blueprint that runs in PIE. The problem occurs after the teleport, and stems from UPrimitiveComponent::OnRegister where we have added our own custom code to the end of the function which at one point branches when calling FApp::IsGame. Note, the reproduction steps show how to see the problem using unmodified UE5.8 while debugging as well.

At Position 1, FApp::IsGame returns True with this callstack on the GameThread:

EnlightenUtil::GetPrimitiveEnlightenId(const UPrimitiveComponent *)
FSystemAssignmentManager::OnPrimitiveRegister(UPrimitiveComponent *)
FEnlightenEngineSupport::OnPrimitiveRegister(UPrimitiveComponent *)
UPrimitiveComponent::OnRegister()
UStaticMeshComponent::OnRegister()
UInstancedStaticMeshComponent::OnRegister()
UActorComponent::ExecuteRegisterEvents(FRegisterComponentContext *)
UActorComponent::RegisterComponentWithWorld(UWorld *, FRegisterComponentContext *)
AActor::IncrementalRegisterComponents(int, FRegisterComponentContext *)
ULevel::IncrementalRegisterComponents(FRegisterComponentContext &)
ULevel::IncrementalUpdateComponents(int, bool, FRegisterComponentContext *)
UWorld::AddToWorld(ULevel *, const UE::Math::TTransform<double> &, bool, const TOptional<UE::FTimeout const> &, FNetLevelVisibilityTransactionId, ULevelStreaming *)
ULevelStreaming::UpdateStreamingState(bool &, bool &, const TOptional<UE::FTimeout const> &)
UWorld::UpdateLevelStreaming(const TOptional<UE::FTimeout const> &)
UWorld::FlushLevelStreaming::__l2::<lambda_1>::operator()()
UWorld::FlushLevelStreaming(EFlushLevelStreamingType)
UWorld::BlockTillLevelStreamingCompleted()
UEngine::BlockTillLevelStreamingCompleted(UWorld *)
UGameInstance::StartPlayInEditorGameInstance(ULocalPlayer *, const FGameInstancePIEParameters &)
UEditorEngine::CreateInnerProcessPIEGameInstance(FRequestPlaySessionParams &, const FGameInstancePIEParameters &, int)
UEditorEngine::OnLoginPIEComplete_Deferred(int, bool, FString, FPieLoginStruct)
UEditorEngine::CreateNewPlayInEditorInstance(FRequestPlaySessionParams &, const bool, const EPlayNetMode)
UEditorEngine::StartPlayInEditorSession(FRequestPlaySessionParams &)

Then after the teleport to Position 2 here is the callstack when FApp::IsGame returns False, also on the GameThread:

EnlightenUtil::GetPrimitiveEnlightenId(const UPrimitiveComponent *)
FSystemAssignmentManager::OnPrimitiveRegister(UPrimitiveComponent *)
FEnlightenEngineSupport::OnPrimitiveRegister(UPrimitiveComponent *)
UPrimitiveComponent::OnRegister()
UStaticMeshComponent::OnRegister()
UInstancedStaticMeshComponent::OnRegister()
UActorComponent::ExecuteRegisterEvents(FRegisterComponentContext *)
UActorComponent::RegisterComponentWithWorld(UWorld *, FRegisterComponentContext *)
AActor::IncrementalRegisterComponents(int, FRegisterComponentContext *)
ULevel::IncrementalRegisterComponents(FRegisterComponentContext &)
ULevel::IncrementalUpdateComponents(int, bool, FRegisterComponentContext *)
UWorld::AddToWorld(ULevel *, const UE::Math::TTransform<double> &, bool, const TOptional<UE::FTimeout const> &, FNetLevelVisibilityTransactionId, ULevelStreaming *)
ULevelStreaming::UpdateStreamingState(bool &, bool &, const TOptional<UE::FTimeout const> &)
UWorld::UpdateLevelStreaming(const TOptional<UE::FTimeout const> &)

According to the comments accompanying FApp::IsGame, it should return true on the GameThread (non-worker thread) while a normal PIE game is active.

We do have a temporary fix in place:

const UWorld* World = Component->GetWorld();
bool bIsRunningPlayInEditor = (World && World->IsPlayInEditor());

This returns True in the same code block where FApp::IsGame would return False.

However, it seems that there is a problem with FApp::IsGame, which we used in many places.

Thanks for your time and advice on the issue,

-Nate

[Attachment Removed]

Steps to Reproduce

The problem affects our custom code, but can reproduced while debugging the Unreal Editor (unmodified) while setting some specific breakpoints.

Please use the attached project in zip file IsGameTest.zip, which contains a simple map named Test.

  • Run the project while debugging the Unreal Editor and open the map Test
  • Set a breakpoint in UWorld::GetDuplicatedWorldForPIE
  • Launch PIE in the Editor, and after the breakpoint is hit set a new inside UModel::Initialize(ABrush* Owner, bool InRootOutside)
  • Click Continue in the debugger and the new breakpoint will be reached
  • Step through UModel::Initialize, then note that the condition ( GIsEditor && !FApp::IsGame() ) is false and UpdateVertices() is not called
    • Note: GIsEditor will be True here, and FApp::IsGame returns True correctly
  • Set a new breakpoint at AActor::TeleportTo, then remove the current one at UModel::Initialize, then click Continue
  • The new breakpoint at TeleportTo should be hit, re-add the breakpoint at UModel::Initialize(ABrush* Owner, bool InRootOutside) and click Continue
  • This time when stepping through UModel::Initialize FApp::IsGame will return False, and then UpdateVertices() is called
    • FApp::IsGame has in the comment, “Returns true if a normal or PIE game is active”. At this point in debugging the PIE has been created and the current thread is GameThread so it should return True
      [Attachment Removed]

Hi,

Your temporary fix is the correct approach, any time you have a world context available you’ll want to use that as the source of truth. There’s been a fair amount of debate around FApp::IsGame (and some talk of deprecating it since it’s pretty ambiguous), but FApp::IsGame and GIsPlayInEditorWorld are tough to remove outright since there are still some cases where we have no world context available and need to rely on those globals. Sorry about that, hopefully it’s not too painful to update the callsites on your end.

Best,

Cody

[Attachment Removed]

Cody,

Thanks for the response! This does help a lot, and confirms why we have in the past seen similar confusion on the subject.

I will look into our own code and consider updating.

Thanks,

Nate

[Attachment Removed]