#Seamless Travel + PC BeginPlay

1 messages · Page 1 of 1 (latest)

gentle shoal
#

Hello there. Welcome to the painful world of multiplayer.

The reason your UI doesn't show is that PlayerControllers specifically are persisted when seamless traveling and the old and new PlayerController (based on the old and new GameMode) have the same class.

#

Given they are persisted, they never reset the bHasBegunPlay flag, which in return means they never call BeginPlay again.

knotty slate
#

i also have readded them in PostSeamlessTravel but it still doesnt show on my client.
I am using steam multiplayer to test

gentle shoal
#

What is "them"?

knotty slate
#

on my host pc it is always there and it just doesnt show on my client pc

#

them is the UI or the UMG widget

#

so this is my current code which i add in BeginPlay and PostSeamlessTravel as well

void AMP_PlayerController::AddUIToScreen()
{
    if (IsLocalController())
    {
        // UMG widget
        PickupCountWidget = CreateWidget<UMP_PickupCountWidget>(this, PickupCountWidgetClass);
        if (IsValid(PickupCountWidget))
        {
            PickupCountWidget->AddToViewport();
            GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, FString::Printf(TEXT("Pickup Count Widget added to screen")));
        }
    }
}
#

i just added the on screen debug message to see if the function is actually being called

#

i was gonna test that right now

gentle shoal
#

The easist you could do is move that to the AHUD class tbh.

#

Unlike APlayerController, AHUD is recreated.

#

AGameModeBase runs GenericPlayerInitialization for both Login and SeamlessTravel.

#

Which in return calls InitializeHUDForPlayer, which RPCs the HUD class to the Client, which then (re-)creates the HUD Actor.

#

If you put your UI spawning into your own AHUD BeginPlay, it should work.

knotty slate
#

what if i am not using the AGameModeBase ?

gentle shoal
#

Then you would probably not be using Unreal Engine :P

knotty slate
#

i am using its child class i think

#

the AGameMode class

gentle shoal
#

That means you still us AGameModeBase + whatever the child class does.

knotty slate
#

oh okay fair enough

gentle shoal
#

You are using AGameMode and AGameState and not their Base classes, which is also the correct thing to do with Multiplayer.

#

AGameMode does not override GenericPlayerInitialization.

knotty slate
#

so there is no such "rule" to say that its better to create widgets in player controller ?

gentle shoal
#

Nope.

#

There is also a way to get this working in the PlayerController, but the easiest option is to just use the HUD class for this.

#

The more intrusive option is to change the persisting of the PC.

knotty slate
#

Just for learning sake, whats the way to get it working on the PlayerController class? I just want to know my options so i can know all the caveats

knotty slate
gentle shoal
#
void AGameMode::HandleSeamlessTravelPlayer(AController*& C)
{
    UE_LOG(LogGameMode, Log, TEXT(">> GameMode::HandleSeamlessTravelPlayer: %s "), *C->GetName());

    APlayerController* PC = Cast<APlayerController>(C);

    UClass* PCClassToSpawn = GetPlayerControllerClassToSpawnForSeamlessTravel(PC);

    if (PC && PC->GetClass() != PCClassToSpawn)
    {
        if (PC->Player != nullptr)
        {
            // We need to spawn a new PlayerController to replace the old one
            APlayerController* const NewPC = SpawnPlayerControllerCommon(PC->IsLocalPlayerController() ? ROLE_SimulatedProxy : ROLE_AutonomousProxy, PC->GetFocalLocation(), PC->GetControlRotation(), PCClassToSpawn);
            if (NewPC == nullptr)
            {
                UE_LOG(LogGameMode, Warning, TEXT("Failed to spawn new PlayerController for %s (old class %s)"), *PC->GetHumanReadableName(), *PC->GetClass()->GetName());
                PC->Destroy();
                return;
            }
            else
            {
                PC->SeamlessTravelTo(NewPC);
                NewPC->SeamlessTravelFrom(PC);
                SwapPlayerControllers(PC, NewPC);
                PC = NewPC;
                C = NewPC;
            }
        }
        else
        {
            PC->Destroy();
        }
    }

    // [..]

}
#

This is what persist your PC if the classes are the same. There is, afaik, no reason why that has to be done, as it clearly creates a new one if your travel to a map with a new PC class.

knotty slate
#

That does seem hectic

gentle shoal
#

That function is virtual, so you could just override it and copy that code, and then just always do the PC->SeamlessTravelTo part with the spawning of a new PC.

knotty slate
#

Thanks for the info and the help!

gentle shoal
#

In terms of not doing that or the HUD stuff, I would need to check the PC class once more for some events.

#

If the PC classes are the same, it does this here btw

#
    else
    {
        // clear out data that was only for the previous game
        C->PlayerState->Reset();
        // create a new PlayerState and copy over info; this is necessary because the old GameMode may have used a different PlayerState class
        APlayerState* OldPlayerState = C->PlayerState;
        C->InitPlayerState();
        OldPlayerState->SeamlessTravelTo(C->PlayerState);
        // we don"t need the old PlayerState anymore
        //@fixme: need a way to replace PlayerStates that doesn't cause incorrect "player left the game"/"player entered the game" messages
        OldPlayerState->Destroy();
    }

    InitSeamlessTravelPlayer(C);

    // Initialize hud and other player details, shared with PostLogin
    GenericPlayerInitialization(C);

    if (PC)
    {
        // This may spawn the player pawn if the game is in progress
        HandleStartingNewPlayer(PC);
    }
knotty slate
#

Interesting

gentle shoal
#

InitSeamlessTravelPlayer does call APlayerController::PostSeamlessTravel, but that's Server-side at that point. Can override that and RPC to init the UMG stuff, but that's basically the same as just moving it to the AHUD class tbh.

#

If you want to look at what is actually going on during SeamlessTravel, you can check FSeamlessTravelHandler btw.

knotty slate
#

PostSeamlessTravel is not client sided ?

gentle shoal
#

Well it's called by the GameMode and not an RPC.

#

GameMode is Server only, so there is no way it can be client sided.

#

Unless it's called from elsewhere, but I didn't see any other call to it.

knotty slate
#

So is that why the UMG widgets dont spawn in server for other local controllers?

#

or they are not created for other local controllers

gentle shoal
#

If you put your UI spawning into BeginPlay without guarding it with IsLocalController, you get duplicated UI on the ListenServer

#

At least for that initial BeginPlay call

knotty slate
#

Yes I did experience that

gentle shoal
#

Again, your problem right now is that the Controllers won't call BeginPlay if they are persisted.

#

I never got to asking Epic why they didn't reset the flag.

knotty slate
#

Is this flag different in different engine versions? or its been the same since 5.0 or maybe even earlier?

gentle shoal
#

That behavior is the same for ages.

#

Basically, inside FSeamlessTravelHandler::Tick, it eventually calls this: LoadedWorld->BeginPlay();
That isn't necessarily the final map, as SeamlessTravel has a transition map, but it does call BeginPlay on it.

That in return does this:

    AGameModeBase* const GameMode = GetAuthGameMode();
    if (GameMode)
    {
        GameMode->StartPlay();
void AGameModeBase::StartPlay()
{
    GameState->HandleBeginPlay();
}
void AGameStateBase::HandleBeginPlay()
{
    bReplicatedHasBegunPlay = true;

    GetWorldSettings()->NotifyBeginPlay();
    GetWorldSettings()->NotifyMatchStarted();
}

And finally this:

void AWorldSettings::NotifyBeginPlay()
{
    UWorld* World = GetWorld();
    if (!World->GetBegunPlay())
    {
        World->OnWorldPreBeginPlay.Broadcast();

        for (FActorIterator It(World); It; ++It)
        {
            SCOPE_CYCLE_COUNTER(STAT_ActorBeginPlay);
            const bool bFromLevelLoad = true;
            It->DispatchBeginPlay(bFromLevelLoad);
        }

        World->SetBegunPlay(true);
    }
}
knotty slate
#

Okay last question, it is a bit unrelated. I have heard some people say its better to build UE from source while doing multiplayer. Does it actually make a difference? If so why?

gentle shoal
#

The bReplicatedHasBegunPlay will do the same through the OnRep.

#

This will call AActor::DispatchBeginPlay. The actuall call to BeginPlay that you don't get is guarded by this:

UWorld* World = (!HasActorBegunPlay() && IsValidChecked(this) ? GetWorld() : nullptr);

if (World)
{
#

Aka, if the Actor HAS already "BegunPlay", it sets the world to nullptr and the if fails.

#

Aka your PlayerController won't get that sweet BeginPlay call.

gentle shoal
knotty slate
#

This is really deep. Thanks for all the information I never even looked at the behind the scenes of all this.

gentle shoal
#

I'm bombarding you with info cause for one you seem to use C++, which is a huge plus for multiplayer, and second, you really want to learn this stuff sooner or later, cause multiplayer is painful enough already.

#

You don't need to know everything instantly, (I#m doing this for over 10 years already..), but you wanna understand why stuff doesn't work as best as possible.

knotty slate
#

Yes no worries. It is really enlightening. I never bothered to look behind the scenes, I am glad you gave me that push

gentle shoal
#

Sadly, I can't see a nice solution for the PlayerController to handle this.

#

There probably is, but I haven't dealt with this problem for a while and I would probably fall back to moving the stuff to the HUD class to just not having to fight it.

knotty slate
#

Fair enough
Thanks for all this info once again

gentle shoal
#

HUD instance is local only already, so you also don't need to bother with IsLocalXYZ. RPCs won't work in there but that's what you have the PC for after all.

#

I'll see if UDN/Epic Pro Support, has a post about this, cause it annoys me that I still don't know why they keep the PC around and don't properly reset it.

knotty slate
#

Okay thanks once again I will get to it

gentle shoal
#

Hm, can't find much. If you really want to keep it in the PC, override PostSeamlessTravel in your PlayerController and send a ClientRPC in which you spawn the UI.

#

Although, one sec.

#

Ah I have a better idea.

#

I forgot that RPCs are usually virtual. You can override APlayerController::ClientSetHUD_Implementation (specifically the _Implementation version!).

#

That is the RPC that is called for normal Login flow and Seamless Travel.

#

If you move your UI create into that function, away from BeginPlay, it should be covered by both cases.

#

Just make sure you still call Super::ClientSetHUD_Implementation :D

#
void AMP_PlayerController::ClientSetHUD_Implementation(TSubclassOf<AHUD> NewHUDClass)
{
  Super::ClientSetHUD_Implementation(NewHUDClass);

  // Ensure your Widget reference isn't already valid.
  // After all, it could have survived the travel too.
  if (IsValid(PickupCountWidget))
  {
    PickupCountWidget->RemoveFromParent();
    PickupCountWidget = nullptr;
  }

  AddUIToScreen();
}
#

I might also advice to not spawn too many individual widgets to your Viewport. Becomes a nightmare to manage.
I think the CommonUI plugin (comes with UE) has some setup for managing UI with layers, where you set up your layers upfront and then add UI elements to those layers. And then you would want to have a HUD layer and potentially just spawn a single HUD widget into that which in return houses your PickupCountWidget and what not.

#

Doesn't need the CommonUI plugin though. The single HUD widget is generally not a bad idea.

#

Anyway, gotta get to work now :D

#

--
Found a Japanese post asking for something like this and Epic answered that there is no specific callback for this on Clients. So overriding ClientSetHUD seems okay to me.

knotty slate
#

Yes this worked
Thanks a lot!

charred nimbus
#

AI suggested overriding this function if you have more stuff to be reinitalized: ```C++
void APlayerController::NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest)
{
// place the camera at the first playerstart we can find
SetViewTarget(this);

if (TActorIterator<APlayerStart> It(GetWorld()); It)
{
    APlayerStart* P = *It;

    FRotator SpawnRotation(ForceInit);
    SpawnRotation.Yaw = P->GetActorRotation().Yaw;
    SetInitialLocationAndRotation(P->GetActorLocation(), SpawnRotation);
}

}```

#

you just need to check if bFinalDest is true for your code to execute to avoid that it runs on the transition map too

#

@gentle shoal ^