-
-
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); | |
} |
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),
No. Local co-op exists, and input processors are global to the FSlateApplication.