Fix for Incorrect Gamepad Input After Alt+Tab in Split-Screen and Multi Local Players (CommonInput + CommonUI)

Hello there,

I come across to share with you an Engine bug and its workaround, on 5.4.4 but still there in last release.

Context

In a split-screen setup, or with multiple local players, using :

  • Enhanced Input
  • CommonInput
  • CommonUI

(e.g., in Lyra or derived projects), we encountered a critical input issue:

Users and devices setup :

User Devices
LocalPlayer0 MouseAndKeyboard, Gamepad 1
LocalPlayer1 Gamepad2

After an Alt+Tab and clicking back into the game window, pressing Gamepad_FaceButton_Bottom on Gamepad #2 would wrongly trigger LeftMouseButton input for LocalPlayer2 (which is completely strange).

This was especially problematic in gameplay, where right trigger or other face buttons would no longer correctly trigger input actions—they’d be mapped to unrelated mouse inputs instead.

Analysis

The bug only appeared after refocusing the window with a mouse click.

The AnalogCursor system (used for simulated cursor input from gamepads) was incorrectly restoring focus or routing input through the wrong device.

See the FCommonAnalogCursor code here :

bool FCommonAnalogCursor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
 (...)

		// We support binding actions to the virtual accept key, so it's a special flower that gets processed right now
		const bool bIsVirtualAccept = InKeyEvent.GetKey() == EKeys::Virtual_Accept;
		const EInputEvent InputEventType = InKeyEvent.IsRepeat() ? IE_Repeat : IE_Pressed;
		if (bIsVirtualAccept && ActionRouter.ProcessInput(InKeyEvent.GetKey(), InputEventType) == ERouteUIInputResult::Handled)
		{
			return true;
		}
		else if (!bIsVirtualAccept || ShouldVirtualAcceptSimulateMouseButton(InKeyEvent, IE_Pressed))
		{
			// There is no awareness on a mouse event of whether it's real or not, so mark that here.
			UCommonInputSubsystem& InputSubsytem = ActionRouter.GetInputSubsystem();
			InputSubsytem.SetIsGamepadSimulatedClick(bIsVirtualAccept);
			bool bReturnValue = FAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
			InputSubsytem.SetIsGamepadSimulatedClick(false);

			return bReturnValue;
		}
	}
	return false;
}

Turns out, CommonUI turns the accept gamepad button press into a MouseButtonDown event, in FAnalogCursor::HandleKeyDownEvent (via FCommonAnalogCursor::HandleKeyDownEvent).

Workaround

1. Custom AnalogCursor subclass

Override the relevant behavior (e.g., how it handles simulated key events):

  • Header

class FMyAnalogCursor : public FCommonAnalogCursor
{

public:

  FMyAnalogCursor(const UCommonUIActionRouterBase& InActionRouter);
  
  virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override;
  
  virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override;

};

  • Implem

#include "FMyAnalogCursor.h"

FMyAnalogCursor::FMyAnalogCursor(const UCommonUIActionRouterBase& InActionRouter)
    : FCommonAnalogCursor(InActionRouter)
{
}

bool FMyAnalogCursor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
  if (InKeyEvent.GetUserIndex() != 0)
  {
    return false; // Ignore unrelated input
  }

  return FCommonAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
}

bool FMyAnalogCursor::HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
  if (InKeyEvent.GetUserIndex() != 0)
  {
    return false; // Ignore unrelated input
  }

  return FCommonAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
}

2. Custom UCommonUIActionRouterBase

Override UCommonUIActionRouterBase to return your custom cursor class:

  • Header

UCLASS()

class MY_API UMyCommonUIActionRouter : public UCommonUIActionRouterBase
{
  GENERATED_BODY()

protected:
  virtual TSharedRef<FCommonAnalogCursor> MakeAnalogCursor() const;

};

  • Implem

#include "MyCommonUIActionRouter.h"
#include "FMyAnalogCursor.h"

TSharedRef<FCommonAnalogCursor> UMyCommonUIActionRouter::MakeAnalogCursor() const
{
  return FCommonAnalogCursor::CreateAnalogCursor<FMyAnalogCursor>(*this);
}

Note

Even in Lyra, this bug can occur in local multiplayer/split-screen mode.

If you’re building on Lyra or using CommonInput with Enhanced Input, and needs this kind of setup, this fix is likely necessary.

Let me know if you need help reproducing it or implementing the override.


Helpful link (already a problem in 2022) : 0002472: Incompatibility with Unreal Engine CommonUI plugin - NoesisGUI Issue Tracker