-
-
Save sinbad/9b8f8007fb1e55f1a952cce2d12aaac1 to your computer and use it in GitHub Desktop.
#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); | |
} |
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.
See https://github.com/sinbad/StevesUEHelpers for a packaged version with instructions.
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?
No. Local co-op exists, and input processors are global to the FSlateApplication.
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.
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),
You have an include error:
#include "InputDetector.h"
instead of
#include "InputModeDetector.h"
Also you didnt include InputHelper.h