Problem with Enhanced Input and Split Screen on UE 5.1

Just to reply to this. I’ve been having the same issue and it’s been driving me crazy. None of these solutions worked for me however.

That being said it suddenly just kind of worked for me but in a very strange way.

Essentially if I play in an editor window straight away only P1 seems to work. If I tab out and back in however it just works!

I need to do some more testing to check if this fixes things for my game but it’s at least highlighted that this is simply an editor bug, not a problem with my code.

Wish I’d known that a week ago.

I did a quick screencap showing my issue.

2 Likes

Hello everyone,

I also have this issue, and while I got it to work in the editor, it does not work in the executable.
I did however find a solution in my game. Small intro, my game is a 2-player adversarial game. Each player controls a character and battles each other to see who wins.

I found that the first time I initialize my second player, it will fail to grab the input device, but if I delete the first instantiation and create a new second player, it will grab the gamepad.

To get the input to work I did the following. In my custom Gamemode:

// HEADER
class ACustomGameMode // ...
{
        
	UFUNCTION()
	void DelayedBeginPlay();
}
// CPP
void ACustomGameMode::BeginPlay()
{
	Super::BeginPlay();
        // ... more code ...

        // Create a player controller
	SecondPlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 1);
	if (!SecondPlayerController)
	{
		SecondPlayerController = UGameplayStatics::CreatePlayer(GetWorld(), 1);
	}

        // Set a timer for a delayed BeginPlay
	if (GetWorld())
	{
		GetWorld()->GetTimerManager().SetTimer(DelayedBeginPlayTimer, this, &ABestOutOfGameMode::DelayedBeginPlay, 0.2f);
	}
}

void ACustomGameMode::DelayedBeginPlay()
{
        // Delete the Player Controller we created
	SecondPlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 1);
	if (SecondPlayerController)
	{
		UGameplayStatics::RemovePlayer(SecondPlayerController, false);
		SecondPlayerController = nullptr;
	}

        // Create a new Player Controller
	if (!SecondPlayerController)
	{
		SecondPlayerController = UGameplayStatics::CreatePlayer(GetWorld(), 1);
	}
}

Furthermore, I followed the instructions by @davabase and it seems to work. I do propose another solution that seems to work for me, which is to override the PossessedBy() function and call the BindInputs() there.


void AMyCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	
	BindInputs();
}

It seems to work for me. Try it out and let me know if it works for you.

1 Like

i was facing the same issue, you saved my day! thank you @DokiDerg !

1 Like

Has anyone gotten this to work on a MAC?
I feel like i have tried everything here. I made delays, i tabbed in and out, i made blueprint solutions and c++ solutions. No response ever from my second gamepad.

edit: i installed unreal engine 5.2.1 (previously on 5.1.1) and created the simplest of projects - nothing. No matter what i do, nothing will let me detect inputs on second game controller. I am sure it works, because i have been playing games with both on the same machine.

1 Like

you don’t need the context mapping in the pawn you can actually just add it directly to the controller you create in the gamemode → create local player-> EIS local player subsystem-> add mapping context. This way you don’t need a delay because your not casting from the player to a controller that may not have possessed the pawn just yet.
I’ve done something similar here in the video below.

1 Like

Yes … this is still present in UE5.2 and UE5.3 preview. Window focus appears to affect only the additional controllers but not input context for local controller index 1.

1 Like

can confirm this still happens and your work around works in 5.2

1 Like

I found a simple solution to getting my second controller to work.

i figured out something that might be the issue. The controller stuff seems to have to do with ‘focus’ - which is why alt-tabbing in and out works. since the game requires you to click the screen when testing in editor the focus is delayed.

Another way to give the game window focus immediately is the ‘Game Gets Mouse Control’ checkbox in the Level Editor Play settings in the Editor Preferences. Checking this box seems to give both players focus and the start and then both controllers work.

3 Likes

Same here… Using v5.3.1 on M1 Mac. I am afraid, BP/C++ solutions proposed in this topic do not work for me as well. Not even on a simple 3rd person template.

1 Like

Sad! I am a bit worried that this issue will result in me not being able to support couch multiplayer on a mac platform at all. I get that this is probably 0.01 % of users, but still it is horribly annoying as I am developing here and it would be nice to get it to work. Alternatively I need to switch to PC… I’ve gotten used to mac now… Sigh…

1 Like

Funny thing is that KristofMorva saw this coming back in 2019 and his feature never got approved.
Add functionality to map devices to player controllers by KristofMorva · Pull Request #5209 · EpicGames/UnrealEngine (github.com)

Some context below for those of you without git access.
Assigning player controllers to devices is currently very limited in Unreal Engine, the only possibility (besides coding it manually) is to start assigning gamepads from the second player instead of the first one.
However, there are numerous very general cases, where such functionality is not enough, like:

  • Splitscreen race games, one player with ADWS, another one with arrows. The current system does not support sharing a device between controllers.
  • Turn-based local multiplayer games, with every player having their own controller. We can’t redirect inputs from devices to whichever player controller we would like.

There is a huge amount of discussions regarding this limitation, but just a few one:

This PR proposes a mapping between devices and player controllers to solve this issue. Developers are able to specify, which device should a given player controller get the signal from.
It can be set with a SetPhysicalControllerMapping static function, but it’s currently also placed in the “Maps & Modes → Local Multiplayer” project settings category for convenience so that developer with static requirements can get it done without coding a single line.

It should be entirely backward compatible - not filling up the mapping array will result in the exact same results as it does now.

1 Like

Viewport Utilities in Code Plugins - UE Marketplace (unrealengine.com)
Stopped support after 4.27 but I’ll reach out to see if I can poke for a 5.1 review and maybe get movement on at least a fix for EIS and the Skip Player 1 bool in binary.

Epic deprecated:

ENGINE_API virtual bool InputAxis(FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime, int32 NumSamples=1, bool bGamepad=false) override;

In favor of:

ENGINE_API virtual bool InputAxis(FViewport* Viewport, FInputDeviceId InputDevice, FKey Key, float Delta, float DeltaTime, int32 NumSamples = 1, bool bGamepad = false) override;

Notice the lack of a ControllerId? Yep…

However, they have provided a RemapControllerInput(EventArgs); you can override to increment the ControllerId as needed.

And they have implemented the option to do it for you via bOffsetPlayerGamepadIds which appears in project settings as Skip Assigning Gamepad to Player 1.

However, later in the code it calls:

ULocalPlayer* const TargetPlayer = GEngine->GetLocalPlayerFromInputDevice(this, EventArgs.InputDevice);
if (TargetPlayer && TargetPlayer->PlayerController)
{
	bResult = TargetPlayer->PlayerController->InputKey(FInputKeyParams(EventArgs.Key, (double)Delta, DeltaTime, NumSamples, EventArgs.IsGamepad(), EventArgs.InputDevice));
}

Its using the wrong function, the ControllerId is never used, it needs to use GetLocalPlayerFromControllerId for this to work as intended (or work at all!).

Sloppy work. I looked at the 5.4 source code and it still isn’t fixed in the next release either.

But of course you could override InputAxis, copy/paste it, and fix it – but nope, those input events are private so you cannot do that.

HOWEVER, UGameViewportClient::OnOverrideInputAxisEvent will allow you to implement your own behaviour.

Talk about misusing virtual functions, Epic!

This was my first attempt, but its incorrect:

void UMyGameViewportClient::Init(FWorldContext& WorldContext, UGameInstance* OwningGameInstance,
	bool bCreateNewAudioDevice)
{
	Super::Init(WorldContext, OwningGameInstance, bCreateNewAudioDevice);
	
	OnOverrideInputAxis().BindUObject(this, &ThisClass::OnOverrideInputAxisHandler);
}

bool UMyGameViewportClient::OnOverrideInputAxisHandler(FInputKeyEventArgs& EventArgs, float& Delta, float& DeltaTime,
	int32& NumSamples)
{
	const ULocalPlayer* const TargetPlayer = GEngine->GetLocalPlayerFromControllerId(this, EventArgs.ControllerId);
	if (TargetPlayer && TargetPlayer->PlayerController)
	{
		TargetPlayer->PlayerController->InputKey(FInputKeyParams(EventArgs.Key, (double)Delta, DeltaTime, NumSamples, EventArgs.IsGamepad(), EventArgs.InputDevice));
	}

	// If we failed here, don't let it route back to player 0
	return true;
}

The Player 2 Id is not always local player + 1, if you remove/add players that number keeps going up, even if you only have a total of 2, the first might be 0 and after a few remove/add the second might be 13!

We need to factor that in:

#include "System/MyGameViewportClient.h"

#include "GameFramework/PlayerInput.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(MyGameViewportClient)

void UMyGameViewportClient::Init(FWorldContext& WorldContext, UGameInstance* OwningGameInstance,
	bool bCreateNewAudioDevice)
{
	Super::Init(WorldContext, OwningGameInstance, bCreateNewAudioDevice);

	OnOverrideInputAxis().BindUObject(this, &ThisClass::OnOverrideInputAxisHandler);
	OnOverrideInputKey().BindUObject(this, &ThisClass::OnOverrideInputKeyHandler);
}

APlayerController* UMyGameViewportClient::GetPlayerTwoController(const FInputDeviceId& InputDevice) const
{
	const ULocalPlayer* const PlayerOne = GEngine->GetLocalPlayerFromInputDevice(this, InputDevice);
	const APlayerController* PlayerOneController = PlayerOne->PlayerController;
	
	TArray<APlayerController*> PlayerControllers;
	GEngine->GetAllLocalPlayerControllers(PlayerControllers);

	if (PlayerControllers.Num() != 2)
	{
		return nullptr;
	}

	return (PlayerControllers[0] == PlayerOneController) ? PlayerControllers[1] : PlayerControllers[0];
}

bool UMyGameViewportClient::OnOverrideInputAxisHandler(FInputKeyEventArgs& EventArgs, float& Delta, float& DeltaTime,
	int32& NumSamples)
{
	if (!EventArgs.IsGamepad()) { return false;	}

	if (APlayerController* PlayerController = GetPlayerTwoController(EventArgs.InputDevice))
	{
		PlayerController->InputKey(FInputKeyParams(EventArgs.Key, (double)Delta, DeltaTime, NumSamples, EventArgs.IsGamepad(), EventArgs.InputDevice));
	}

	// If we failed here, don't let it route back to player 0
	return true;
}

bool UMyGameViewportClient::OnOverrideInputKeyHandler(FInputKeyEventArgs& EventArgs)
{
	if (!EventArgs.IsGamepad()) { return false;	}

	if (APlayerController* PlayerController = GetPlayerTwoController(EventArgs.InputDevice))
	{
		PlayerController->InputKey(FInputKeyParams(EventArgs.Key, EventArgs.Event, static_cast<double>(EventArgs.AmountDepressed), EventArgs.IsGamepad(), EventArgs.InputDevice));
	}

	// If we failed here, don't let it route back to player 0
	return true;
}

However, it still didn’t work, and that was because I was missing the setting bFilterInputByPlatformUser (you can find this in project settings, DISABLE it! It blocks any input that comes from a device that isn’t registered to the player. And of course, the engine doesn’t draw any link between these settings.

Now it works.

1 Like