506 lines
17 KiB
C++
506 lines
17 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "IGameInputDeviceInterface.h"
|
|
#include "GameInputDeveloperSettings.h"
|
|
#include "GameInputLogging.h"
|
|
#include "GameInputUtils.h"
|
|
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "GenericPlatform/IInputInterface.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
|
|
#if GAME_INPUT_SUPPORT
|
|
namespace UE::GameInput
|
|
{
|
|
static TAutoConsoleVariable<bool> CVarGameInputEnumerateDeviceTypeOnConnection
|
|
(
|
|
TEXT("gameinput.bEnumerateDeviceTypeOnConnection"),
|
|
true,
|
|
TEXT("If true, EnumerateCurrentlyConnectedDeviceTypes will be called upon device connect and disconnect"),
|
|
ECVF_Default
|
|
);
|
|
};
|
|
#endif // GAME_INPUT_SUPPORT
|
|
|
|
IGameInputDeviceInterface::IGameInputDeviceInterface(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler, IGameInput* InGameInput)
|
|
#if GAME_INPUT_SUPPORT
|
|
: MessageHandler(InMessageHandler)
|
|
, GameInput(InGameInput)
|
|
, CurrentlyConnectedDeviceTypes(GameInputKindUnknown)
|
|
, bWasinitialized(false)
|
|
, bIsAppCurrentlyConstrained(false)
|
|
, bWasAppConstrainedLastTick(false)
|
|
#endif // GAME_INPUT_SUPPORT
|
|
{
|
|
}
|
|
|
|
IGameInputDeviceInterface::~IGameInputDeviceInterface()
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
if (ConnectionChangeCallbackToken)
|
|
{
|
|
if (GameInput)
|
|
{
|
|
GameInput->UnregisterCallback(ConnectionChangeCallbackToken, UINT64_MAX);
|
|
GameInput.Reset();
|
|
}
|
|
|
|
ConnectionChangeCallbackToken = GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
|
|
}
|
|
|
|
FCoreDelegates::ApplicationWillDeactivateDelegate.RemoveAll(this);
|
|
FCoreDelegates::ApplicationHasReactivatedDelegate.RemoveAll(this);
|
|
#endif // GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
void IGameInputDeviceInterface::Initialize()
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
BindDeviceStatusCallbacks();
|
|
bWasinitialized = true;
|
|
#endif // GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
void IGameInputDeviceInterface::Tick(float DeltaTime)
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
|
|
FScopeLock Lock(&DeviceInfoCS);
|
|
|
|
// At this point we normally will have a valid game input object.
|
|
// However, during boot it may be still getting created in the background,
|
|
// so early exit in that case.
|
|
if (!GameInput)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Process any input devices so long as we are not constrained
|
|
if (!bIsAppCurrentlyConstrained)
|
|
{
|
|
// Handle any device connection/disconnection state changes first before attempting to process any devices
|
|
ProcessDeferredDeviceConnectionChanges();
|
|
}
|
|
|
|
#endif // GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
void IGameInputDeviceInterface::SendControllerEvents()
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
|
|
// At this point we normally will have a valid game input object.
|
|
// However, during boot it may be still getting created in the background,
|
|
// so early exit in that case.
|
|
|
|
if (!bWasinitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!GameInput)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FScopeLock Lock(&DeviceInfoCS);
|
|
|
|
// On the first update coming back from being constrained, we want to reset
|
|
// the state of inputs
|
|
if (bIsAppCurrentlyConstrained && !bWasAppConstrainedLastTick)
|
|
{
|
|
DetermineStateAfterFirstUnconstrainedUpdate();
|
|
}
|
|
|
|
// Process any input devices so long as we are not constrained
|
|
if (!bIsAppCurrentlyConstrained)
|
|
{
|
|
// A map of Platform users to a bitmask of any reading kinds that were processed this frame.
|
|
// This is used by the devices to keep track of which users had which readings, and use that
|
|
// state to determine if we can process a given reading.
|
|
TMap<FPlatformUserId, GameInputKind> PlatformUsersWhoHaveHadInputThisFrame;
|
|
|
|
// The allowed input kinds that we can read from the IGameInput interface.
|
|
const GameInputKind AllowedInputKindsThisFrame = GetCurrentGameInputKindSupport();
|
|
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
const FPlatformUserId UserId = KnownDevice->GetPlatformUserId();
|
|
|
|
// Figure out what GameInputKind's this platform user has already handled this frame.
|
|
GameInputKind& AlreadyProcessedInputKinds = PlatformUsersWhoHaveHadInputThisFrame.FindOrAdd(UserId, /* default init value */ GameInputKindUnknown);
|
|
|
|
// Actually process our input here, and get a bitmask back of what GameInputKind's have sent events.
|
|
const GameInputKind InputKindsWithEvents = KnownDevice->ProcessInput(GameInput, AllowedInputKindsThisFrame, AlreadyProcessedInputKinds);
|
|
|
|
// Keep track what kinds of input that this platform user has processed this frame for use on the next device
|
|
AlreadyProcessedInputKinds |= InputKindsWithEvents;
|
|
|
|
// Keep track of the most recent device that is being used by each given platform user
|
|
// We need to do this whenever we receive input, which is true as long as there was an input kind processed this frame.
|
|
if (InputKindsWithEvents != GameInputKindUnknown)
|
|
{
|
|
// If we know input came from a device associated to this platform user already, then check our timestamp to see if it is newer then it
|
|
if (const TSharedPtr<FGameInputDeviceContainer>* MostRecentKnownDevice = PlatformUserIdToMostRecentDeviceContainer.Find(UserId))
|
|
{
|
|
// If the current device that was just processed has a more recent timestamp then the known one, then
|
|
// use it as our most recent device instead
|
|
if (KnownDevice->GetLastReadingTimestamp() > (*MostRecentKnownDevice)->GetLastReadingTimestamp())
|
|
{
|
|
PlatformUserIdToMostRecentDeviceContainer[UserId] = KnownDevice;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PlatformUserIdToMostRecentDeviceContainer.Add(UserId, KnownDevice);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bWasAppConstrainedLastTick = bIsAppCurrentlyConstrained;
|
|
|
|
#endif// GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
void IGameInputDeviceInterface::SetMessageHandler(const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler)
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
MessageHandler = InMessageHandler;
|
|
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
KnownDevice->SetMessageHandler(InMessageHandler);
|
|
}
|
|
#endif // GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
bool IGameInputDeviceInterface::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
|
|
{
|
|
// required by IInputDevice interface
|
|
return false;
|
|
}
|
|
|
|
void IGameInputDeviceInterface::SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value)
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get();
|
|
FPlatformUserId UserId = PLATFORMUSERID_NONE;
|
|
FInputDeviceId DeviceId = INPUTDEVICEID_NONE;
|
|
DeviceMapper.RemapControllerIdToPlatformUserAndDevice(ControllerId, UserId, DeviceId);
|
|
|
|
if (!UserId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameInputRumbleParams RumbleParams = {};
|
|
|
|
switch (ChannelType)
|
|
{
|
|
case FForceFeedbackChannelType::LEFT_LARGE:
|
|
RumbleParams.lowFrequency = FMath::Clamp(Value, 0.0f, 1.0f);
|
|
break;
|
|
case FForceFeedbackChannelType::LEFT_SMALL:
|
|
RumbleParams.leftTrigger = FMath::Clamp(Value, 0.0f, 1.0f);
|
|
break;
|
|
case FForceFeedbackChannelType::RIGHT_LARGE:
|
|
RumbleParams.highFrequency = FMath::Clamp(Value, 0.0f, 1.0f);
|
|
break;
|
|
case FForceFeedbackChannelType::RIGHT_SMALL:
|
|
RumbleParams.rightTrigger = FMath::Clamp(Value, 0.0f, 1.0f);
|
|
break;
|
|
}
|
|
|
|
// Send a rumble event for every input device that is mapped to this platform user
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
if (DeviceMapper.GetUserForInputDevice(KnownDevice->GetDeviceId()) == UserId)
|
|
{
|
|
if (IGameInputDevice* Device = KnownDevice->GetGameInputDevice())
|
|
{
|
|
Device->SetRumbleState(&RumbleParams);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // #if GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
void IGameInputDeviceInterface::SetChannelValues(int32 ControllerId, const FForceFeedbackValues& Values)
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
// TODO: Allow native input device id's to FInputDeviceId's in the platform input device mapper
|
|
IPlatformInputDeviceMapper& DeviceMapper = IPlatformInputDeviceMapper::Get();
|
|
FPlatformUserId UserId = PLATFORMUSERID_NONE;
|
|
FInputDeviceId DeviceId = INPUTDEVICEID_NONE;
|
|
DeviceMapper.RemapControllerIdToPlatformUserAndDevice(ControllerId, UserId, DeviceId);
|
|
|
|
if (!UserId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameInputRumbleParams RumbleParams = {};
|
|
|
|
RumbleParams.lowFrequency = FMath::Clamp(Values.LeftLarge, 0.0f, 1.0f);
|
|
RumbleParams.leftTrigger = FMath::Clamp(Values.LeftSmall, 0.0f, 1.0f);
|
|
RumbleParams.highFrequency = FMath::Clamp(Values.RightLarge, 0.0f, 1.0f);
|
|
RumbleParams.rightTrigger = FMath::Clamp(Values.RightSmall, 0.0f, 1.0f);
|
|
|
|
// Send a rumble event for every input device that is mapped to this platform user
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
if (DeviceMapper.GetUserForInputDevice(KnownDevice->GetDeviceId()) == UserId)
|
|
{
|
|
if (IGameInputDevice* Device = KnownDevice->GetGameInputDevice())
|
|
{
|
|
Device->SetRumbleState(&RumbleParams);
|
|
}
|
|
}
|
|
}
|
|
#endif // #if GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
bool IGameInputDeviceInterface::IsGamepadAttached() const
|
|
{
|
|
#if GAME_INPUT_SUPPORT
|
|
// We will treat both Gamepads and Controller's as "Gamepads" as far as UE is concerned.
|
|
return CurrentlyConnectedDeviceTypes & GameInputKindGamepad || CurrentlyConnectedDeviceTypes & GameInputKindController;
|
|
#else
|
|
return false;
|
|
#endif // GAME_INPUT_SUPPORT
|
|
}
|
|
|
|
#if GAME_INPUT_SUPPORT
|
|
|
|
GameInputKind IGameInputDeviceInterface::GetCurrentGameInputKindSupport() const
|
|
{
|
|
GameInputKind RegisterInputKindMask = GameInputKindUnknown;
|
|
|
|
const UGameInputPlatformSettings* PlatformSettings = UGameInputPlatformSettings::Get();
|
|
|
|
if (PlatformSettings->bProcessController)
|
|
{
|
|
RegisterInputKindMask |= (GameInputKindController | GameInputKindControllerAxis | GameInputKindControllerButton | GameInputKindControllerSwitch);
|
|
}
|
|
|
|
if (PlatformSettings->bProcessRawInput)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindRawDeviceReport;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessGamepad)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindGamepad;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessKeyboard)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindKeyboard;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessMouse)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindMouse;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessRacingWheel)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindRacingWheel;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessArcadeStick)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindArcadeStick;
|
|
}
|
|
|
|
if (PlatformSettings->bProcessFlightStick)
|
|
{
|
|
RegisterInputKindMask |= GameInputKindFlightStick;
|
|
}
|
|
|
|
return RegisterInputKindMask;
|
|
}
|
|
|
|
bool IGameInputDeviceInterface::BindDeviceStatusCallbacks()
|
|
{
|
|
if (!GameInput)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// First, check if we are already bound to something. If so, unbind it
|
|
if (ConnectionChangeCallbackToken)
|
|
{
|
|
GameInput->UnregisterCallback(ConnectionChangeCallbackToken, UINT64_MAX);
|
|
ConnectionChangeCallbackToken = GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
|
|
}
|
|
|
|
// Hook up device callbacks
|
|
auto DeviceCallbackFn = [](GameInputCallbackToken CallbackToken, void* Context, IGameInputDevice* Device, uint64 Timestamp, GameInputDeviceStatus CurrentStatus, GameInputDeviceStatus PreviousStatus)
|
|
{
|
|
static_cast<IGameInputDeviceInterface*>(Context)->OnDeviceConnectionChanged(CallbackToken, Device, Timestamp, CurrentStatus, PreviousStatus);
|
|
};
|
|
|
|
// TODO: should this be a member variable that subclasses can override? That might be desirable on PC
|
|
static constexpr GameInputDeviceStatus DeviceStatusMask = (GameInputDeviceNoStatus | GameInputDeviceConnected);
|
|
|
|
const GameInputKind RegisterInputKindMask = GetCurrentGameInputKindSupport();
|
|
|
|
UE_LOG(LogGameInput, Log, TEXT("Registering Device Callback for GameInputKind: '%s'. Listening for Device Status: '%s'."), *UE::GameInput::LexToString(RegisterInputKindMask), *UE::GameInput::LexToString(DeviceStatusMask));
|
|
|
|
GameInput->RegisterDeviceCallback(nullptr, RegisterInputKindMask, DeviceStatusMask, GameInputBlockingEnumeration, this, DeviceCallbackFn, &ConnectionChangeCallbackToken);
|
|
|
|
// get notified when the app is constrained & unconstrained
|
|
FCoreDelegates::ApplicationWillDeactivateDelegate.AddRaw(this, &IGameInputDeviceInterface::OnAppConstrained);
|
|
FCoreDelegates::ApplicationHasReactivatedDelegate.AddRaw(this, &IGameInputDeviceInterface::OnAppUnconstrained);
|
|
|
|
// We are successful if the callback token is valid
|
|
return ConnectionChangeCallbackToken != GAMEINPUT_INVALID_CALLBACK_TOKEN_VALUE;
|
|
}
|
|
|
|
void IGameInputDeviceInterface::OnDeviceConnectionChanged(GameInputCallbackToken CallbackToken, IGameInputDevice* Device, uint64 Timestamp, GameInputDeviceStatus CurrentStatus, GameInputDeviceStatus PreviousStatus)
|
|
{
|
|
FScopeLock Lock(&DeviceInfoCS);
|
|
|
|
UE_LOG(LogGameInput, Log, TEXT("Device Connection Changed from '%s' to '%s'"), *UE::GameInput::LexToString(PreviousStatus), *UE::GameInput::LexToString(CurrentStatus));
|
|
|
|
// This event may come in async from GameInput from outside the game thread, so defer it until Tick so we can guarantee game thread access
|
|
IGameInputDeviceInterface::FDeferredDeviceConnectionChanges Event = {};
|
|
Event.Device = Device;
|
|
Event.Timestamp = Timestamp;
|
|
Event.Status = UE::GameInput::DeviceStateToConnectionState(CurrentStatus, PreviousStatus);
|
|
Event.CurrentStatus = CurrentStatus;
|
|
Event.PreviousStatus = PreviousStatus;
|
|
|
|
DeferredDeviceConnectionChanges.Emplace(Event);
|
|
}
|
|
|
|
void IGameInputDeviceInterface::ProcessDeferredDeviceConnectionChanges()
|
|
{
|
|
// We only want to actually handle device connection events in the game thread
|
|
// because a lot of listeners will be in game or ui code that reference the FSlateApplication::Get function.
|
|
check(IsInGameThread());
|
|
if (!DeferredDeviceConnectionChanges.IsEmpty())
|
|
{
|
|
for (const FDeferredDeviceConnectionChanges& Event : DeferredDeviceConnectionChanges)
|
|
{
|
|
// No status means that the device has been disconnected
|
|
if (Event.Status == EInputDeviceConnectionState::Disconnected)
|
|
{
|
|
HandleDeviceDisconnected(Event.Device, Event.Timestamp);
|
|
}
|
|
else if (Event.Status == EInputDeviceConnectionState::Connected)
|
|
{
|
|
HandleDeviceConnected(Event.Device, Event.Timestamp);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogGameInput, Error, TEXT("Unexpected state change for device %s (%s -> %s"), *UE::GameInput::LexToString(Event.Device), *UE::GameInput::LexToString(Event.PreviousStatus), *UE::GameInput::LexToString(Event.CurrentStatus));
|
|
}
|
|
}
|
|
DeferredDeviceConnectionChanges.Reset();
|
|
}
|
|
}
|
|
|
|
void IGameInputDeviceInterface::OnAppConstrained()
|
|
{
|
|
bIsAppCurrentlyConstrained = true;
|
|
}
|
|
|
|
void IGameInputDeviceInterface::OnAppUnconstrained()
|
|
{
|
|
bIsAppCurrentlyConstrained = false;
|
|
}
|
|
|
|
void IGameInputDeviceInterface::DetermineStateAfterFirstUnconstrainedUpdate()
|
|
{
|
|
// This should only be called on the first update after coming back from being constrained
|
|
ensure(bIsAppCurrentlyConstrained && !bWasAppConstrainedLastTick);
|
|
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
KnownDevice->ClearInputState(GameInput);
|
|
}
|
|
}
|
|
|
|
FGameInputDeviceContainer* IGameInputDeviceInterface::GetDeviceData(IGameInputDevice* InDevice)
|
|
{
|
|
// Check if we already know about the given device
|
|
if (!InDevice)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Check if we have seen this device's APP_LOCAL_DEVICE_ID before. The IGameInputDevice* could have been invalidated if the device
|
|
// is being re-connected, but it will have the APP_LOCAL_DEVICE_ID would be the same.
|
|
const GameInputDeviceInfo* Info = InDevice->GetDeviceInfo();
|
|
|
|
FGameInputDeviceContainer* RetVal = nullptr;
|
|
|
|
for (const TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
const APP_LOCAL_DEVICE_ID KnownDeviceId = KnownDevice->GetGameInputDeviceId();
|
|
|
|
if (KnownDevice->GetGameInputDevice() == InDevice ||
|
|
(Info && FMemory::Memcmp(&Info->deviceId, &KnownDeviceId, sizeof(KnownDeviceId)) == 0))
|
|
{
|
|
RetVal = KnownDevice.Get();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return RetVal;
|
|
}
|
|
|
|
FGameInputDeviceContainer* IGameInputDeviceInterface::GetOrCreateDeviceData(IGameInputDevice* InDevice)
|
|
{
|
|
// Check if we already know about the given device
|
|
if (!InDevice)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Check if we already have some device data about this...
|
|
if (FGameInputDeviceContainer* ExistingDevice = GetDeviceData(InDevice))
|
|
{
|
|
// For existing devices, we want to ensure that their IGameInputDevice pointer matches up with what was given.
|
|
// This may be the case if you disconnect and then reconnect a device, because we can still find it's associated
|
|
// FGameInputDeviceContainer based on the APP_LOCAL_DEVICE_ID, but the IGameInputDevice pointer would be null.
|
|
ExistingDevice->SetGameInputDevice(InDevice);
|
|
|
|
return ExistingDevice;
|
|
}
|
|
|
|
// ... if not, then create a new one.
|
|
return CreateDeviceData(InDevice);
|
|
}
|
|
|
|
void IGameInputDeviceInterface::EnumerateCurrentlyConnectedDeviceTypes()
|
|
{
|
|
if (!UE::GameInput::CVarGameInputEnumerateDeviceTypeOnConnection.GetValueOnAnyThread())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CurrentlyConnectedDeviceTypes = GameInputKindUnknown;
|
|
|
|
// Check all of our devices and their supported input flags
|
|
for (TSharedPtr<FGameInputDeviceContainer>& KnownDevice : DeviceData)
|
|
{
|
|
if (KnownDevice)
|
|
{
|
|
if (IGameInputDevice* Device = KnownDevice->GetGameInputDevice())
|
|
{
|
|
if (const GameInputDeviceInfo* Info = Device->GetDeviceInfo())
|
|
{
|
|
CurrentlyConnectedDeviceTypes |= Info->supportedInput;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // GAME_INPUT_SUPPORT
|