How to change mapping context of a client in multiplayer?

Hi. I have a problem with disabling movement of the clients at the start of the game. I want pawns to spawn when a player is loaded and while the match state is “WaitingToStart” they shouldn’t be able to move. I use PostLogin function in my gamemode to spawn pawns. I want to restrict input of players by changing to specific mapping context inside my character class, which allows them only to look with mouse and open pause menu:

void AG_Character::SetKeyboardInput(bool bEnable)
{
    if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
                ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
        {
            if (bEnable)
            {
                Subsystem->RemoveMappingContext(WaitingForGameMappingContext);
                Subsystem->AddMappingContext(DefaultMappingContext, 0);
            }
            else
            {
                Subsystem->RemoveMappingContext(DefaultMappingContext);
                Subsystem->AddMappingContext(WaitingForGameMappingContext, 0);
            }
        }
    }
}

Firstly I tried to call this function with PlayerController inside PostLogin function:

void AG_BaseGameMode::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);
    
    RestartPlayer(NewPlayer);
    // I know this is a bad line, it's just for a test
    Cast<AG_PlayerController>(NewPlayer)->SetKeyboardInput(false);
}

Here is a SetKeyboardInput function in PlayerController. Here I tried to use client RPC to change mapping context on the client side.

void AG_PlayerController::SetKeyboardInput(bool bEnable)
{
    if (HasAuthority() && IsLocalController())
    {
        G_Character = G_Character.IsValid() ? G_Character : Cast<AG_Character>(GetPawn());
        if (!G_Character.IsValid()) return;

        G_Character->SetKeyboardInput(bEnable);
    }
    else
    {
        Client_SetKeyboardInput(bEnable);
    }
}

void AG_PlayerController::Client_SetKeyboardInput_Implementation(bool bEnable)
{
    if (GetPawn())
    {
        G_Character = G_Character.IsValid() ? G_Character : Cast<AG_Character>(GetPawn());
        if (!G_Character.IsValid()) return;

        G_Character->SetKeyboardInput(bEnable);
    }
}

Server side player works perfectly, but all clients still can move when they spawn. But if I try to call this same function in HandleMatchHasStarted it works perfectly both on clients and server:

void AG_BaseGameMode::HandleMatchHasStarted()
{
    Super::HandleMatchHasStarted();

    for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
    {
        Cast<AG_PlayerController>(Iterator->Get())->SetKeyboardInput(true);
    }
}

I don’t really know what to try next. Maybe there is much easier way to do it, but I didn’t find it.

RPC for disabling client’s input on connection seems quite convoluted approach, wont recommend.

GameMode::PostLogin won’t work since gamemode only exist on server. Your code just is not called on the client.

Of the top of my head, i’d suggest to call AG_Character::SetKeyboardInput(false); at AG_PlayerController::BeginPlay() (without any extra checks) and reenabled it back at … idk, some multicast RPC “match has started”? or replicating matchstate to clients manually and so on, since i don’t see it’s being replicated out of the box.

AG_PlayerController::BeginPlay() will be called on controller construction, which is happens on both sides on player connection, so it’s a fine place to do so.

But if I try to call this same function in HandleMatchHasStarted it works perfectly both on clients and server:
void AG_BaseGameMode::HandleMatchHasStarted()

Either you or me missing something, as to my understanding this code also should only be executing on server and not affect clients in any way.

also, minor note: i find the function void AG_Character::SetKeyboardInput(bool bEnable) more suitable as a controller’s method, as it has nothing to do with character.

1 Like

Thanks for your answer. After many attempts I managed to fix it. I’ll leave my steps if someone will have this problem again.
Reason why I put my logic in PostLogin function is because I already used it to spawn StartGame widget for all clients and it worked fine. Inside PostLogin function I call CreateStartGameWidget function, which calls the same named function in PlayerController:

void AG_PlayerController::CreateStartGameWidget(float DelayBeforeStart)
{
    if (HasAuthority() && IsLocalController())
    {
        ShowStartGameWidget(DelayBeforeStart);
    }
    else
    {
        Client_ShowStartGameWidget(DelayBeforeStart);
    }
}

void AG_PlayerController::Client_ShowStartGameWidget_Implementation(float DelayBeforeStart)
{
    ShowStartGameWidget(DelayBeforeStart);
}

void AG_PlayerController::ShowStartGameWidget(float DelayBeforeStart)
{
    G_HUD = (!G_HUD) ? GetHUD<AG_HUD>() : G_HUD;
    if (!G_HUD) return;

    G_HUD->ShowStartGameWidget(DelayBeforeStart);
}

And it works fine both on the server and clients.

I’m also sure that SetKeyboardInput function in HandleMatchHasStarted affects all clients when the game is starting. I think the problem is that when the pawn spawns in PostLogin, there is a delay before PlayerController possesses it, and during this delay my function is called. Since pawn is not controlled, function just doesn’t do anything. I checked this assumption with debug and saw that in this part:

void AG_PlayerController::Client_SetKeyboardInput_Implementation(bool bEnable)
{
    if (GetPawn())
    {
        G_Character = G_Character.IsValid() ? G_Character : Cast<AG_Character>(GetPawn());
        if (!G_Character.IsValid()) return;

        G_Character->SetKeyboardInput(bEnable);
    }
}

when it is firstly executed on the client it just stops on if (GetPawn()). But when this same function is executed on the client second time, after being called by HandleMatchHasStarted, it works perfectly.

So I tried to call of this function after a pawn is possessed. I used OnPossess and AcknowledgePossession. OnPossess works for the server and the second function is called for the client. Firstly I couldn’t understand why nothing worked, even though in debug every function worked and all pointers were valid. I put DisableInput function instead of my SetKeyboardInput and it worked. So I understood the problem is with adding and removing mapping contexts.

After many attempts I realized that DefaultMappingContext is not removed when SetKeyboardInput is called. Eventually I removed this code from SetupPlayerInputComponent in my Character class:

/*if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
            ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
    {
        Subsystem->AddMappingContext(DefaultMappingContext, 0);
    }
}*/

And it worked! All clients now have this WaitingForGameMappingContext and they cannot move until HandleMatchHasStarted is called. Thanks to your advice I created a replicated field of MatchState in player controller, that is changed by GameMode when its match state is changed.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.