There is a bug that occurs in UE5 when seamless traveling that occurs if a client beats the server in loading the next level. There’s a race condition that causes the server to never see that client as having loaded the new level, therefore never transitions it’s player controller over to the new level, and the server and all the clients hang.
The following is for Epic. If you are interested in the workaround, it’s at the bottom
Cause:
This seems due to an interaction with some state variables on PlayerController, and the methods:
void APlayerController::ServerNotifyLoadedWorld_Implementation(FName WorldPackageName)
and
void AGameModeBase::PostSeamlessTravel()
ServerNotifyLoadedWorld is an RPC that gets called from the client when they finish transitioning to a new level (either the transition map or the destination map)
If the client calls this AFTER the server has loaded the map, all the checks in the code pass and GameMode::HandleSeamlessTravelPlayer is called and everything works fine.
If the Server is STILL transitioning, this method is not called
So the clients who beat the server to the new level are in a sort of limbo state because the server is not ready to transition them. The server eventually loads the next level and calls GameMode::PostSeamlessTravel, which has the responsibility of transitioning all players who beat the server to the next level.
However, the way the server is detecting that the client beat the server is not correct. It does so by calling PlayerController::HasClientLoadedCurrentWorld, and here is where the issue lies. This method checks a number of things, but eventually this code executes:
if (SeamlessTravelCount > 0)
{
// In the case where seamless travel has occurred, make sure the client has actually completed the travel
return bInCorrectWorld && (LastCompletedSeamlessTravelCount == SeamlessTravelCount);
}
This check fails:
(LastCompletedSeamlessTravelCount == SeamlessTravelCount). In our testing, SeamlessTravelCount is incremented at the start of the seamless travel, however LastCompletedSeamlessTravelCount is not incremented until GameMode::HandleSeamlessTravelPlayer is called (which eventually calls APlayerController::PostSeamlessTravel(), which does the increment). So PlayerController::HasClientLoadedCurrentWorld never returns true because HandleSeamlessTravelPlayer is never called, and HandleSeamlessTravelPlayer is never called because HasClientLoadedCurrentWorld never returns true
Workaround:
Subclass PlayerController if you have not already done so. add the following code to the header file:
virtual void NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest) override;
UFUNCTION(Reliable, Server, WithValidation, SealedEvent)
void ServerNotifyLoadedWorldWorkaround(FName WorldPackageName);
And in the .cpp file add the following code:
void AYourPlayerControllerName::NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest)
{
Super::NotifyLoadedWorld(WorldPackageName, bFinalDest);
// Call custom ServerNotifyLoadedWorld
ServerNotifyLoadedWorldWorkaround(WorldPackageName);
}
bool AYourPlayerControllerName::ServerNotifyLoadedWorld_Validate(FName WorldPackageName)
{
RPC_VALIDATE( WorldPackageName.IsValid() );
return true;
}
void AYourPlayerControllerName::ServerNotifyLoadedWorldWorkaround_Implementation(FName WorldPackageName)
{
UE_LOG(LogPlayerController, Verbose, TEXT("AYourPlayerControllerName::ServerNotifyLoadedWorldWorkaround_Implementation: Client loaded %s"), *WorldPackageName.ToString());
UWorld *CurWorld = GetWorld();
// Only valid for calling, for PC's in the process of seamless traveling
// NOTE: SeamlessTravelCount tracks client seamless travel, through the serverside gameplay code; this should not be replaced.
if (CurWorld != NULL && !CurWorld->IsNetMode(NM_Client) && SeamlessTravelCount > 0 && LastCompletedSeamlessTravelCount < SeamlessTravelCount)
{
// Update our info on what world the client is in
UNetConnection *const Connection = Cast<UNetConnection>(Player);
if (Connection != NULL)
{
Connection->SetClientWorldPackageName(WorldPackageName);
}
// if both the server and this client have completed the transition, handle it
FSeamlessTravelHandler &SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld(CurWorld);
AGameModeBase *CurGameMode = CurWorld->GetAuthGameMode();
if (!SeamlessTravelHandler.IsInTransition() && WorldPackageName == CurWorld->GetOutermost()->GetFName() && CurGameMode != NULL)
{
AController *TravelPlayer = this;
CurGameMode->HandleSeamlessTravelPlayer(TravelPlayer);
}
else
{
// This is the seamless travel fix for client loading before server and softlocking
// Skip this for TransitionMapName
FString TransitionMapString = GetDefault<UGameMapsSettings>()->TransitionMap.GetLongPackageName();
FName TransitionMapName(*TransitionMapString);
if (TransitionMapName != WorldPackageName)
{
LastCompletedSeamlessTravelCount = SeamlessTravelCount;
}
}
}
}
You will incur an extra RPC during level transitions, but the impact of this should be minimal.