Player State Copy Properties Event is called when window is shutting down

I knew that Copy Properties Event of Player State called when players move to another map by seamless travel.

However, it is also called when window is closing. I have not checked Use Seamless Travel in Game Mode and Net Mode is Standalone. I’m not setting net.AllowPIESeamlessTravel to true.

Additionally, this problem occurs when using Game Mode instead of Game Mode Base.

So, is it intended function of Unreal Engine?

If not, should I send bug report to Epic?

I check this problem more.

When I open level by Open Level blueprint node in PIE (Use Seamless Travel = false, Not Mode = standalone, net.AllowPIESeamlessTravel = false), flow is Browse → LoadMap → World BeginTearingDown → Player State End Play → Player State Destroyed → Cleanup World. It’s OK.

image

When I close PIE window by window close button or Quit Game kismet node, flow is Player State 1 Begin Play → Player State 0 Copy Properties → Player State 0 End Play → Player State 0 Destroyed → World BeginTearingDown → Player State 1 End Play → Game Instance Shutdown → Cleanup World.

I feel so weird.

I looked over the Unreal Engine source code.

The APlayerState::CopyProperties function is also called when APlayerState::Duplicate is called, even if seamless travel is not being performed.

And the APlayerState::Duplicate function is called in the AGameMode::AddInactivePlayer function and is called by the AGameMode::Logout function when PIE window is closing. AddInactivePlayer function is only implemented in AGameMode, not inherited by AGameModeBase. So this is why the problem occurs using the AGameMode class.

AddInactivePlayer calls APlayerState::Duplicate if the Player State is not from previous level, Game Mode is not must spectate and World is not tearing down.

// Unreal Engine Player State Source Code
void AGameMode::AddInactivePlayer(APlayerState* PlayerState, APlayerController* PC)
{
	check(PlayerState)
	UWorld* LocalWorld = GetWorld();
	// don't store if it's an old PlayerState from the previous level or if it's a spectator... or if we are shutting down
	if (!PlayerState->IsFromPreviousLevel() && !MustSpectate(PC) && !LocalWorld->bIsTearingDown)
	{
		APlayerState* const NewPlayerState = PlayerState->Duplicate();

When I tested it, bIsTearingDown was false. So I think the problem is that order is twisted. The BeginTearingDown function of UWorld function must be called before calling game mode Logout function, but currently, it is the opposite.

When we are closing PIE window, UEditorEngine::EndPlayMap function is called. This function calls UEditorEngine::TeardownPlaySession, which in turn calls UWorld::BeginTearingDown. However UEditorEngine::EndPlayMap also calls UEngine::CleanupGameViewport before calling UEditorEngine::TeardownPlaySession. UEngine::CleanupGameViewport calls UGameInstance::CleanupGameViewport, which calls UGameInstance::RemoveLocalPlayer, which calls APlayerController::Destory. Finally AController::Destroyed is called, and AGameMode::Logout is called.

// UEditorEngine::EndPlayMap function (Unreal Engine Source Code)

// 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());

Let’s look at the UGameEngine source code instead of UEditorEngine for PIE. The UGameEngine::PreExit function, called when we close game, calls BeginTearingDown function first and then Player Controller is destroyed with other actors and shuts down game instance.

// UGameEngine::PreExit function (Unreal Engine Source Code)

// Clean up all worlds
for (int32 WorldIndex = 0; WorldIndex < WorldList.Num(); ++WorldIndex)
{
	UWorld* const World = WorldList[WorldIndex].World();
	if ( World != NULL )
	{
		World->BeginTearingDown();

		// Cancel any pending connection to a server
		CancelPending(World);

		// Shut down any existing game connections
		ShutdownWorldNetDriver(World);

		for (FActorIterator ActorIt(World); ActorIt; ++ActorIt)
		{
			ActorIt->RouteEndPlay(EEndPlayReason::Quit);
		}

		if (World->GetGameInstance() != nullptr)
		{
			World->GetGameInstance()->Shutdown();
		}

		World->FlushLevelStreaming(EFlushLevelStreamingType::Visibility);
		World->CleanupWorld();
	}
}

So maybe, if it is not PIE, it doesn’t work like that.

Who do you help to make sure it is bug?
If it is bug, how do I send a bug report to the Unreal Engine developer?