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