Skip to content

Instantly share code, notes, and snippets.

@sinbad
Last active March 18, 2024 19:02
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sinbad/9b8f8007fb1e55f1a952cce2d12aaac1 to your computer and use it in GitHub Desktop.
Save sinbad/9b8f8007fb1e55f1a952cce2d12aaac1 to your computer and use it in GitHub Desktop.
UE4 detecting which input method was last used by each player
#include "InputModeDetector.h"
#include "Input/Events.h"
FInputModeDetector::FInputModeDetector()
{
// 4 local players should be plenty usually (will expand if necessary)
LastInputModeByPlayer.Init(EInputMode::Mouse, 4);
}
bool FInputModeDetector::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
// Key down also registers for gamepad buttons
ProcessKeyOrButton(InKeyEvent.GetUserIndex(), InKeyEvent.GetKey());
// Don't consume
return false;
}
bool FInputModeDetector::HandleAnalogInputEvent(FSlateApplication& SlateApp,
const FAnalogInputEvent& InAnalogInputEvent)
{
if (InAnalogInputEvent.GetAnalogValue() > GamepadAxisThreshold)
SetMode(InAnalogInputEvent.GetUserIndex(), EInputMode::Gamepad);
// Don't consume
return false;
}
bool FInputModeDetector::HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
FVector2D Dist = MouseEvent.GetScreenSpacePosition() - MouseEvent.GetLastScreenSpacePosition();
if (FMath::Abs(Dist.X) > MouseMoveThreshold || FMath::Abs(Dist.Y) > MouseMoveThreshold)
{
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse);
}
// Don't consume
return false;
}
bool FInputModeDetector::HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent)
{
// We don't care which button
SetMode(MouseEvent.GetUserIndex(), EInputMode::Mouse);
// Don't consume
return false;
}
bool FInputModeDetector::HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
const FPointerEvent* InGestureEvent)
{
SetMode(InWheelEvent.GetUserIndex(), EInputMode::Mouse);
// Don't consume
return false;
}
void FInputModeDetector::Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor)
{
// Required, but do nothing
}
EInputMode FInputModeDetector::GetLastInputMode(int PlayerIndex)
{
if (PlayerIndex >= 0 && PlayerIndex < LastInputModeByPlayer.Num())
return LastInputModeByPlayer[PlayerIndex];
// Assume default if never told
return DefaultInputMode;
}
void FInputModeDetector::ProcessKeyOrButton(int PlayerIndex, FKey Key)
{
if (Key.IsGamepadKey())
{
SetMode(PlayerIndex, EInputMode::Gamepad);
}
else if (Key.IsMouseButton())
{
// Assuming mice don't have analog buttons!
SetMode(PlayerIndex, EInputMode::Mouse);
}
else
{
// We assume anything that's not mouse and not gamepad is a keyboard
// Assuming keyboards don't have analog buttons!
SetMode(PlayerIndex, EInputMode::Keyboard);
}
}
void FInputModeDetector::SetMode(int PlayerIndex, EInputMode NewMode)
{
if (NewMode != EInputMode::Unknown && NewMode != GetLastInputMode(PlayerIndex))
{
if (PlayerIndex >= LastInputModeByPlayer.Num())
LastInputModeByPlayer.SetNum(PlayerIndex + 1);
LastInputModeByPlayer[PlayerIndex] = NewMode;
OnInputModeChanged.ExecuteIfBound(PlayerIndex, NewMode);
UE_LOG(LogTemp, Warning, TEXT("Input mode for player %d changed: %s"), PlayerIndex, *UEnum::GetValueAsString(NewMode));
}
}
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Framework/Application/IInputProcessor.h"
#include "UObject/ObjectMacros.h" // for UENUM
UENUM(BlueprintType)
enum class EInputMode : uint8
{
Mouse,
Keyboard,
Gamepad,
Unknown
};
DECLARE_DELEGATE_TwoParams(FOnInputModeForPlayerChanged, int /* PlayerIndex */, EInputMode)
/**
* This class should be registered as an input processor in order to capture all input events & detect
* what kind of devices are being used. We can't use PlayerController to do this reliably because in UMG
* mode, all the mouse move events are consumed by Slate and you never see them, so it's not possible to
* detect when the user moved a mouse.
*
* This class should be instantiated and used from some UObject of your choice, e.g. your GameInstance class,
* something like this:
*
* InputDetector = MakeShareable(new FInputModeDetector());
* FSlateApplication::Get().RegisterInputPreProcessor(InputDetector);
* InputDetector->OnInputModeChanged.BindUObject(this, &UMyGameInstance::OnInputDetectorModeChanged);
*
* Note how the OnInputModeChanged on this object is a simple delegate, not a dynamic multicast etc, because
* this is not a UObject. You should relay the input mode event changed through the owner if you want to distribute
* the information further.
*/
class PROJECT_API FInputModeDetector : public IInputProcessor, public TSharedFromThis<FInputModeDetector>
{
protected:
TArray<EInputMode> LastInputModeByPlayer;
public:
EInputMode DefaultInputMode = EInputMode::Mouse;
float MouseMoveThreshold = 1;
float GamepadAxisThreshold = 0.2;
// Single delegate caller, owner should propagate if they want (this isn't a UObject)
FOnInputModeForPlayerChanged OnInputModeChanged;
FInputModeDetector();
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override;
virtual bool
HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override;
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override;
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent,
const FPointerEvent* InGestureEvent) override;
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override;
EInputMode GetLastInputMode(int PlayerIndex = 0);
protected:
void ProcessKeyOrButton(int PlayerIndex, FKey Key);
void SetMode(int PlayerIndex, EInputMode NewMode);
};
// You probably want to do this from your GameInstance subclass
...
// Namespace level in header
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, int, PlayerIndex, EInputMode, InputMode);
...
// In class declaration:
protected:
TSharedPtr<FInputModeDetector> InputDetector;
public:
/// Event raised when input mode changed between gamepad / keyboard / mouse
UPROPERTY(BlueprintAssignable)
FOnInputModeChanged OnInputModeChanged;
UFUNCTION(BlueprintCallable)
EInputMode GetLastInputModeUsed(int PlayerIndex = 0) const { return InputDetector->GetLastInputMode(PlayerIndex); }
UFUNCTION(BlueprintCallable)
bool LastInputWasGamePad(int PlayerIndex = 0) const { return GetLastInputModeUsed(PlayerIndex) == EInputMode::Gamepad; }
...
// In source
// Do this at startup somewhere
void MyExampleGameInstance::CreateInputDetector()
{
if (!InputDetector.IsValid())
{
InputDetector = MakeShareable(new FInputModeDetector());
FSlateApplication::Get().RegisterInputPreProcessor(InputDetector);
InputDetector->OnInputModeChanged.BindUObject(this, &USnukaGameInstance::OnInputDetectorModeChanged);
}
}
// Do this at shutdown
void MyExampleGameInstance::DestroyInputDetector()
{
if (InputDetector.IsValid())
{
FSlateApplication::Get().UnregisterInputPreProcessor(InputDetector);
InputDetector.Reset();
}
}
void MyExampleGameInstance::OnInputDetectorModeChanged(int PlayerIndex, EInputMode NewMode)
{
// Propagate dynamic multicast event, everyone else should listen on this
OnInputModeChanged.Broadcast(PlayerIndex, NewMode);
}
@grujicbr
Copy link

You have an include error:
#include "InputDetector.h"
instead of
#include "InputModeDetector.h"

Also you didnt include InputHelper.h

@sinbad
Copy link
Author

sinbad commented Aug 26, 2020

Fixed, thanks - I named the file in Gist incorrectly and the InputHelper isn't needed any more, that was a hangover from a previous attempt that didn't work.

@lucastucious
Copy link

I can't build in 4.26.2, but i don't any clue for what is wrong
image

@sinbad
Copy link
Author

sinbad commented Nov 11, 2021

See https://github.com/sinbad/StevesUEHelpers for a packaged version with instructions.

@PancioCiancio
Copy link

PancioCiancio commented Jan 26, 2022

Why pass the player index as parameter here
EInputMode GetLastInputMode(int PlayerIndex = 0);?

HUD instance should be not per player controller?
If yes, this means that you register an InputProcessor for each player controller and there is no reason to pass the index. Is it right?

@sinbad
Copy link
Author

sinbad commented Jan 26, 2022

No. Local co-op exists, and input processors are global to the FSlateApplication.

@PancioCiancio
Copy link

Hi sinbad, I think that your theory is valid for legacy ue4 versions because I look at the source code and when we register an input processor it will add it to a list.

bool FSlateApplication::RegisterInputPreProcessor(TSharedPtr<IInputProcessor> InputProcessor, const int32 Index /*= INDEX_NONE*/)
{
	bool bResult = false;
	if ( InputProcessor.IsValid() )
	{
		bResult = InputPreProcessors.Add(InputProcessor, Index);
	}

	return bResult;
}

When a local player is created, a new HUD will be created as well, which means a new Input processor will be registered. I think I miss something but right now I do not understand why we need user index.

@PancioCiancio
Copy link

PancioCiancio commented Jan 29, 2022

Ok I debug more. What I understand is that the application store all the number of local player. However every player create his own player controller, hud, game instance, etc...
I create and register the InputProcessor in the HUD which is created per player. This means that in my case I don't need to distinguish the player index. I hope my logic is correct, I'll be happy if someone can confirm my theory.
..............................................................................................................................................................................................................................................................................................
I explored more the source code and result that InputProcessor is created in the SubSystem which has a unique instance (Singleton),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment