I’m working on a game where we want to have multiple controllers on the same machine controlling different parts of a pawn. So one controller will take over movement while another controller will handle abilities and so forth. Right now all those controls are handled by the player one controller and this needs to remain an option when not playing in this multiplayer mode. I’m looking for some way of rerouting the inputs from these other controllers to act as though they are coming from the 1st controller. I’m not sure on what is the best way to go about this. I’d prefer something that allows me to just keep the one main character pawn and have the controllers direct input towards different parts of it’s controls. Is there a good way to do this?
Hello [mention removed],
This setup is definitely possible. The key idea is to use multiple local player controllers on the same machine, with only one possessing the visible pawn. The others are set up to route their input manually to that shared pawn. The extra controller is essentially a second player in a local multiplayer setup, but with splitscreen explicitly disabled so only the main pawn and camera are rendered. This is important because creating a second local player is what allows input from the second physical controller to be detected and processed by the engine.
I’m going to share an example on how to do this. You may start by disabling splitscreen in your project Local Multiplayer settings.
Now, edit your custom Game Mode class and define two configurable properties to control how the extra player is spawned:
`UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = “MultiControl”)
TSubclassOf ExtraControllerClass;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = “MultiControl”)
TSubclassOf ExtraPawnClass;`ExtraControllerClass defines the custom controller to use for the secondary player, and ExtraPawnClass is the pawn it will possess. Since this pawn exists only to enable input and doesn’t need to appear in-game, it can be set to a base APawn class or a hidden placeholder.
At the game mode’s BeginPlay, I create a new local player for the second controller manually using CreateLocalPlayer and then spawn their controller with custom login options:
`void AMultiControlGameMode::BeginPlay()
{
Super::BeginPlay();
AddExtraLocalPlayer();
}
void AMultiControlGameMode::AddExtraLocalPlayer()
{
if (UGameInstance* GameInstance = GetGameInstance())
{
FString CreateLocalError;
if (ULocalPlayer* NewPlayer = GameInstance->CreateLocalPlayer(1, CreateLocalError, false))
{
FString CustomOptions = TEXT(“?bIsExtra=true”);
FString OutError;
NewPlayer->SpawnPlayActor(CustomOptions, OutError, GetWorld());
}
}
}`The Logic function in the GameMode is overridden to check for the ?bIsExtra=true flag and spawn the appropiate player controller and pawn for the extra player:
`APlayerController* AMultiControlGameMode::Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
if (Options.Contains(TEXT(“bIsExtra=true”)))
{
DefaultPawnClass = ExtraPawnClass;
APlayerController* NewPC = SpawnPlayerControllerCommon(InRemoteRole, FVector::ZeroVector, FRotator::ZeroRotator, ExtraControllerClass);
return NewPC;
}
return Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
}`Once the game starts, two players will exist in the level. The main player uses the standard pawn and controller, while the extra player is created with a custom controller and hidden pawn.
In this example, the extra player controller class is set up to make the Third Person Template character jump, while the main controller handles movement and rotation. In the header for the extra player controller I define the action mapping this controller will take and the jump action.
`// ExtraPlayerController.h
UCLASS()
class YOURPROJECT_API AExtraPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// Input Mapping Context for this controller
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = “Input”)
TObjectPtr ExtraMappingContext;
// Jump input action
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = “Input”)
TObjectPtr JumpAction;
protected:
// Retrieves the shared pawn from the main controller
APawn* GetSharedPawn() const;
…`GetSharedPawn function is meant to get the main pawn so input is directly applied to it. This is how it works in the definition:
`// ExtraPlayerController.cpp
APawn* AExtraPlayerController::GetSharedPawn() const
{
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
// My main controller class is called “AMultiDevicePlayerController”. Change this to your controller class.
if (AMultiDevicePlayerController* MainPC = Cast(*It))
{
if (MainPC->GetPawn())
{
return MainPC->GetPawn();
}
}
}
return nullptr;
}
void AExtraPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// Add the input mapping context and bind the input actions as you would in a regular controller
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(GetLocalPlayer()))
{
if (ExtraMappingContext)
{
Subsystem->AddMappingContext(ExtraMappingContext, 0);
UE_LOG(LogTemp, Log, TEXT(“Added ExtraMappingContext to ExtraPlayerController.”));
}
else
{
UE_LOG(LogTemp, Warning, TEXT(“ExtraMappingContext is not assigned.”));
}
}
if (UEnhancedInputComponent* EIC = Cast(InputComponent))
{
if (JumpAction)
{
EIC->BindAction(JumpAction, ETriggerEvent::Started, this, &AExtraPlayerController::OnExtraJumpPressed);
EIC->BindAction(JumpAction, ETriggerEvent::Completed, this, &AExtraPlayerController::OnExtraJumpStopped);
UE_LOG(LogTemp, Log, TEXT(“JumpAction bound in ExtraPlayerController.”));
}
else
{
UE_LOG(LogTemp, Warning, TEXT(“JumpAction is not assigned.”));
}
}
}
void AExtraPlayerController::OnExtraJumpPressed()
{
if (APawn* SharedPawn = GetSharedPawn())
{
// Cast to your main pawn class, in this example I’m just doing Jump logic when the input is detected
APawnMultiControlCaseCharacter* CastedPawn = Cast(SharedPawn);
if (CastedPawn)
{
CastedPawn->Jump();
UE_LOG(LogTemp, Log, TEXT(“Extra jump pressed on shared pawn.”));
}
UE_LOG(LogTemp, Log, TEXT(“Shared pawn jump triggered from ExtraPlayerController.”));
}
else
{
UE_LOG(LogTemp, Warning, TEXT(“No shared pawn found for ExtraPlayerController.”));
}
}
void AExtraPlayerController::OnExtraJumpStopped()
{
if (APawn* SharedPawn = GetSharedPawn())
{
// Cast to your main pawn class, in this example I’m just doing Jump logic when the input is detected
if (APawnMultiControlCaseCharacter* CastedPawn = Cast(SharedPawn))
{
CastedPawn->StopJumping();
UE_LOG(LogTemp, Log, TEXT(“Extra jump stopped on shared pawn.”));
}
else
{
UE_LOG(LogTemp, Warning, TEXT(“Shared pawn is not of type APawnMultiControlCaseCharacter.”));
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT(“No shared pawn found for ExtraPlayerController.”));
}
}`With these changes in place, testing the game with two connected controllers will assign one to the main player and the other to the extra player. The second controller will be able to send input into the shared pawn, allowing you to separate responsibilities like movement and abilities across different devices.
If needed, you can easily disable this behavior by skipping the AddExtraPlayer call in your GameMode (for example, when the game is set to single-player mode). This approach keeps your existing systems intact and avoids major changes to your main gameplay classes.
Please let me know if this information helps solve your case.
Best,
Francisco