Merge branch 'enhanced-input'

This commit is contained in:
Steve Streeting 2023-03-08 14:08:50 +00:00
commit 46e1a18187
14 changed files with 359 additions and 17 deletions

View File

@ -1,5 +1,9 @@
#include "StevesGameSubsystem.h"
#include "EngineUtils.h"
#include "EnhancedInputSubsystems.h"
#include "StevesGameViewportClientBase.h"
#include "StevesPluginSettings.h"
#include "StevesUEHelpers.h"
#include "Engine/AssetManager.h"
#include "Engine/GameInstance.h"
@ -20,6 +24,7 @@ void UStevesGameSubsystem::Initialize(FSubsystemCollectionBase& Collection)
CreateInputDetector();
InitTheme();
InitForegroundCheck();
NotifyEnhancedInputMappingsChanged();
#endif
}
@ -58,6 +63,50 @@ void UStevesGameSubsystem::DestroyInputDetector()
#endif
}
void UStevesGameSubsystem::NotifyEnhancedInputMappingsChanged()
{
// delay to ensure there's a tick in between which updates the mappings, it's not synchronous
auto DelayedFunc = [this]()
{
OnEnhancedInputMappingsChanged.Broadcast();
};
FTimerHandle TempHandle;
GetWorld()->GetTimerManager().SetTimer(TempHandle, FTimerDelegate::CreateLambda(DelayedFunc), 0.05, false);
}
TSoftObjectPtr<UInputAction> UStevesGameSubsystem::FindEnhancedInputAction(const FString& Name)
{
if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::LoadModulePtr<FAssetRegistryModule>(TEXT("AssetRegistry")))
{
IAssetRegistry& AssetRegistry = AssetRegistryModule->Get();
if (auto Settings = GetDefault<UStevesPluginSettings>())
{
for (const auto& Dir : Settings->EnhancedInputActionSearchDirectories)
{
if (!FPackageName::IsValidPath(Dir.Path))
{
continue;
}
TArray<FAssetData> Assets;
FString Package = FPaths::Combine(Dir.Path, Name);
if (AssetRegistry.GetAssetsByPackageName(FName(*Package), Assets, true))
{
for (const FAssetData& Asset : Assets)
{
if (Asset.GetClass() == UInputAction::StaticClass())
{
return TSoftObjectPtr<UInputAction>(Asset.GetSoftObjectPath());
}
}
}
}
}
}
return nullptr;
}
void UStevesGameSubsystem::InitTheme()
{
DefaultUiTheme = LoadObject<UUiTheme>(nullptr, *DefaultUiThemePath, nullptr);
@ -224,6 +273,42 @@ UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromAxis(const FName& Nam
return nullptr;
}
UPaperSprite* UStevesGameSubsystem::GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
APlayerController* PC,
UUiTheme* Theme)
{
if (const UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
const TArray<FKey> Keys = Subsystem->QueryKeysMappedToAction(Action);
// For default, prefer mouse for axes
if (DevicePreference == EInputImageDevicePreference::Auto)
{
if (Action->ValueType == EInputActionValueType::Boolean)
{
DevicePreference = EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button;
}
else
{
DevicePreference = EInputImageDevicePreference::Gamepad_Mouse_Keyboard;
}
}
const EInputMode LastInput = GetLastInputModeUsed(PlayerIdx);
const EInputMode LastButtonInput = GetLastInputButtonPressed(PlayerIdx);
const EInputMode LastAxisInput = GetLastInputAxisMoved(PlayerIdx);
if (const FKey* PreferredKey = GetPreferedKeyMapping(Keys, DevicePreference, LastInput, LastButtonInput, LastAxisInput))
{
return GetInputImageSpriteFromKey(*PreferredKey, PlayerIdx, Theme);
}
}
return nullptr;
}
TSoftObjectPtr<UDataTable> UStevesGameSubsystem::GetGamepadImages(int PlayerIndex, const UUiTheme* Theme)
{
// TODO: determine type of controller

View File

@ -0,0 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "StevesPluginSettings.h"

View File

@ -1,5 +1,9 @@
#include "StevesUEHelpers.h"
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#include "StevesPluginSettings.h"
#define LOCTEXT_NAMESPACE "FStevesUEHelpers"
DEFINE_LOG_CATEGORY(LogStevesUEHelpers)
@ -8,6 +12,19 @@ void FStevesUEHelpers::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
UE_LOG(LogStevesUEHelpers, Log, TEXT("Steve's UE Helpers Module Started"))
// register settings
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
if (SettingsModule)
{
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "StevesUEHelpers",
LOCTEXT("StevesUEHelpersSettingsName", "StevesUEHelpers"),
LOCTEXT("StevesUEHelpersSettingsDescription", "Configure the helpers plug-in."),
GetMutableDefault<UStevesPluginSettings>()
);
}
}
void FStevesUEHelpers::ShutdownModule()

View File

@ -16,7 +16,9 @@ TSharedRef<SWidget> UInputImage::RebuildWidget()
GS->OnInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
GS->OnButtonInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
GS->OnAxisInputModeChanged.AddUniqueDynamic(this, &UInputImage::OnInputModeChanged);
GS->OnEnhancedInputMappingsChanged.AddUniqueDynamic(this, &UInputImage::OnEnhancedInputMappingsChanged);
}
UpdateImage();
return Ret;
@ -35,6 +37,11 @@ void UInputImage::OnInputModeChanged(int ChangedPlayerIdx, EInputMode InputMode)
}
}
void UInputImage::OnEnhancedInputMappingsChanged()
{
MarkImageDirty();
}
void UInputImage::SetCustomTheme(UUiTheme* Theme)
{
CustomTheme = Theme;
@ -94,18 +101,51 @@ void UInputImage::SetFromKey(FKey K)
UpdateImage();
}
void UInputImage::SetFromInputAction(UInputAction* Action)
{
BindingType = EInputBindingType::EnhancedInputAction;
InputAction = Action;
UpdateImage();
}
void UInputImage::UpdateImage()
{
auto GS = GetStevesGameSubsystem(GetWorld());
if (GS)
{
auto Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex, CustomTheme);
UPaperSprite* Sprite = nullptr;
if (BindingType == EInputBindingType::EnhancedInputAction && !InputAction.IsNull())
{
if (auto IA = InputAction.LoadSynchronous())
{
Sprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, DevicePreference, PlayerIndex, GetOwningPlayer(), CustomTheme);
}
}
else
{
Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex, CustomTheme);
}
if (Sprite)
{
if (bHiddenBecauseBlank)
{
SetVisibility(OldVisibility);
bHiddenBecauseBlank = false;
}
// Match size is needed incase size has changed
// Need to make it update region in case inside a scale box or something else that needs to adjust
SetBrushFromAtlasInterface(Sprite, true);
}
else
{
if (IsVisible())
{
bHiddenBecauseBlank = true;
OldVisibility = GetVisibility();
SetVisibility(ESlateVisibility::Hidden);
}
}
}
bIsDirty = false;
DelayUpdate = 0;
@ -114,7 +154,7 @@ void UInputImage::UpdateImage()
void UInputImage::MarkImageDirty()
{
bIsDirty = true;
DelayUpdate = 0.5f;
DelayUpdate = 0.1f;
}
// Tickables

View File

@ -4,6 +4,7 @@
#include "StevesHelperCommon.h"
#include "StevesUEHelpers.h"
#include "Fonts/FontMeasure.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/DefaultValueHelper.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Widgets/Images/SImage.h"
@ -18,6 +19,8 @@ struct FRichTextInputImageParams
FName ActionOrAxisName;
/// If BindingType is Key, the key
FKey Key;
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
TSoftObjectPtr<UInputAction> InputAction;
/// Player index, if binding type is action or axis
int PlayerIndex;
/// Where there are multiple mappings, which to prefer
@ -38,6 +41,8 @@ protected:
FName ActionOrAxisName;
/// If BindingType is Key, the key
FKey Key;
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
TSoftObjectPtr<UInputAction> InputAction;
/// Player index, if binding type is action or axis
int PlayerIndex = 0;
/// Where there are multiple mappings, which to prefer
@ -66,6 +71,7 @@ public:
ActionOrAxisName = InParams.ActionOrAxisName;
DevicePreference = InParams.DevicePreference;
Key = InParams.Key;
InputAction = InParams.InputAction;
PlayerIndex = InParams.PlayerIndex;
Decorator = InParams.Decorator;
RequestedWidth = Width;
@ -77,7 +83,7 @@ public:
// We will need to do the work to update the brush from the main thread later
// We can use static methods though
if (InParams.InitialSprite)
if (IsValid(InParams.InitialSprite))
UStevesGameSubsystem::SetBrushFromAtlas(&Brush, InParams.InitialSprite, true);
TimeUntilNextSpriteCheck = 0.25f;
@ -128,7 +134,19 @@ public:
if (GS)
{
// Can only support default theme, no way to edit theme in decorator config
auto Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex);
UPaperSprite* Sprite = nullptr;
if (BindingType == EInputBindingType::EnhancedInputAction && !InputAction.IsNull())
{
if (auto IA = InputAction.LoadSynchronous())
{
auto PC = UGameplayStatics::GetPlayerController(Decorator->GetWorld(), PlayerIndex);
Sprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, DevicePreference, PlayerIndex, PC);
}
}
else
{
Sprite = GS->GetInputImageSprite(BindingType, ActionOrAxisName, Key, DevicePreference, PlayerIndex);
}
if (Sprite && Brush.GetResourceObject() != Sprite)
{
UStevesGameSubsystem::SetBrushFromAtlas(&Brush, Sprite, true);
@ -175,7 +193,8 @@ public:
{
return RunParseResult.MetaData.Contains(TEXT("key")) ||
RunParseResult.MetaData.Contains(TEXT("action")) ||
RunParseResult.MetaData.Contains(TEXT("axis"));
RunParseResult.MetaData.Contains(TEXT("axis")) ||
RunParseResult.MetaData.Contains(TEXT("eaction"));
}
return false;
@ -191,6 +210,8 @@ protected:
Params.Key = EKeys::AnyKey;
Params.Decorator = Decorator;
auto GS = GetStevesGameSubsystem(Decorator->GetWorld());
if (const FString* PlayerStr = RunInfo.MetaData.Find(TEXT("player")))
{
int PTemp;
@ -212,6 +233,15 @@ protected:
Params.BindingType = EInputBindingType::Axis;
Params.ActionOrAxisName = **AxisStr;
}
else if (const FString* EInputStr = RunInfo.MetaData.Find(TEXT("eaction")))
{
Params.BindingType = EInputBindingType::EnhancedInputAction;
// Try to find the input action
if (GS)
{
Params.InputAction = GS->FindEnhancedInputAction(*EInputStr);
}
}
if (const FString* PreferStr = RunInfo.MetaData.Find(TEXT("prefer")))
{
@ -240,12 +270,22 @@ protected:
// Look up the initial sprite here
// The Slate widget can't do it in Construct because World pointer doesn't work (thread issues?)
// Also annoying: can't keep Brush on this class because this method is const. UGH
auto GS = GetStevesGameSubsystem(Decorator->GetWorld());
if (GS)
{
if (Params.BindingType == EInputBindingType::EnhancedInputAction && !Params.InputAction.IsNull())
{
if (auto IA = Params.InputAction.LoadSynchronous())
{
auto PC = UGameplayStatics::GetPlayerController(Decorator->GetWorld(), Params.PlayerIndex);
Params.InitialSprite = GS->GetInputImageSpriteFromEnhancedInputAction(IA, Params.DevicePreference, Params.PlayerIndex, PC);
}
}
else
{
// Can only support default theme, no way to edit theme in decorator config
Params.InitialSprite = GS->GetInputImageSprite(Params.BindingType, Params.ActionOrAxisName, Params.Key, Params.DevicePreference, Params.PlayerIndex);
}
}
else
{
// Might be false because this gets executed in the editor too

View File

@ -49,3 +49,67 @@ void SetWidgetFocusProperly(UWidget* Widget)
Widget->SetFocus();
}
const FKey* GetPreferedKeyMapping(const TArray<FKey>& AllKeys,
EInputImageDevicePreference DevicePreference,
EInputMode LastInputDevice,
EInputMode LastButtonInputDevice,
EInputMode LastAxisInputDevice)
{
// Same as GetPreferedActionOrAxisMapping, just with key directly
const FKey* MouseMapping = nullptr;
const FKey* KeyboardMapping = nullptr;
const FKey* GamepadMapping = nullptr;
for (const FKey& Key : AllKeys)
{
// notice how we take the LAST one in the list as the final version
// this is because UInputSettings::GetActionMappingByName *reverses* the mapping list from Project Settings
if (Key.IsGamepadKey())
{
GamepadMapping = &Key;
}
else if (Key.IsMouseButton()) // registers true for mouse axes too
{
MouseMapping = &Key;
}
else
{
KeyboardMapping = &Key;
}
}
const FKey* Preferred = nullptr;
if (GamepadMapping && LastInputDevice == EInputMode::Gamepad)
{
// Always prefer gamepad if used last
Preferred = GamepadMapping;
}
else
{
switch (DevicePreference)
{
// Auto should be pre-converted to another
case EInputImageDevicePreference::Auto:
UE_LOG(LogStevesUI, Error, TEXT("Device Preference should have been converted before this call"))
break;
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse:
Preferred = KeyboardMapping ? KeyboardMapping : MouseMapping;
break;
case EInputImageDevicePreference::Gamepad_Mouse_Keyboard:
Preferred = MouseMapping ? MouseMapping : KeyboardMapping;
break;
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Button:
// Use the latest button press
Preferred = (MouseMapping && (LastButtonInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
break;
case EInputImageDevicePreference::Gamepad_Keyboard_Mouse_Axis:
// Use the latest button press
Preferred = (MouseMapping && (LastAxisInputDevice == EInputMode::Mouse || !KeyboardMapping)) ? MouseMapping : KeyboardMapping;
break;
default:
break;
}
}
return Preferred;
}

View File

@ -89,3 +89,10 @@ const T* GetPreferedActionOrAxisMapping(const TArray<T>& AllMappings, const FNam
}
return Preferred;
}
const FKey* GetPreferedKeyMapping(const TArray<FKey>& AllKeys,
EInputImageDevicePreference DevicePreference,
EInputMode LastInputDevice,
EInputMode LastButtonInputDevice,
EInputMode LastAxisInputDevice);

View File

@ -8,11 +8,13 @@
#include "StevesHelperCommon.h"
#include "StevesTextureRenderTargetPool.h"
#include "StevesUI/FocusSystem.h"
#include "StevesUI/InputImage.h"
#include "StevesUI/UiTheme.h"
#include "StevesGameSubsystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, int, PlayerIndex, EInputMode, InputMode);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEnhancedInputMappingsChanged);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWindowForegroundChanged, bool, bFocussed);
/// Entry point for all the top-level features of the helper system
@ -37,7 +39,6 @@ public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
protected:
DECLARE_DELEGATE_TwoParams(FInternalInputModeChanged, int /* PlayerIndex */, EInputMode)
/**
@ -161,6 +162,12 @@ public:
UPROPERTY(BlueprintAssignable)
FOnInputModeChanged OnAxisInputModeChanged;
/// Event raised justr after the Enhanced Input mappings have changed
/// Right now, this has to be user-triggered via NotifyEnhancedInputMappingsChanged, because the Enhanced Input
/// plugin provides NO events to monitor it (sigh)
UPROPERTY(BlueprintAssignable)
FOnEnhancedInputMappingsChanged OnEnhancedInputMappingsChanged;
/// Event raised when the game window's foreground status changes
UPROPERTY(BlueprintAssignable)
FOnWindowForegroundChanged OnWindowForegroundChanged;
@ -209,6 +216,21 @@ public:
int PlayerIndex = 0,
const UUiTheme* Theme = nullptr);
/**
* @brief Get an input button / key / axis image as a sprite based on an enhanced input action
* @param Action The input action
* @param DevicePreference The order of preference for images where multiple devices have mappings. In the case of multiple mappings for the same device, the first one will be used.
* @param PlayerIdx The player index to look up the binding for
* @param PC The player controller to look up the binding for
* @param Theme Optional explicit theme, if blank use the default theme
* @return
*/
UPaperSprite* GetInputImageSpriteFromEnhancedInputAction(UInputAction* Action,
EInputImageDevicePreference DevicePreference,
int PlayerIdx,
APlayerController* PC,
UUiTheme* Theme = nullptr);
/**
* @brief Get an input button / key image from an action
* @param Name The name of the action
@ -266,4 +288,16 @@ public:
*/
FStevesTextureRenderTargetPoolPtr GetTextureRenderTargetPool(FName Name, bool bAutoCreate = true);
/**
* Notify this subsystem that changes have been made to the Enhanced Input mappings, e.g. adding or removing a context.
* Unfortunately, the Enhanced Input plugin currently provides NO WAY for us to monitor context changes automatically,
* so we need the user to tell us when they make a change.
* This call is however slightly delayed before being acted upon, because EI defers the rebuild of mappings until the next tick.
*/
void NotifyEnhancedInputMappingsChanged();
/** Attempt to find an enhanced input action by name in the configured folders.
*/
TSoftObjectPtr<UInputAction> FindEnhancedInputAction(const FString& Name);
};

View File

@ -41,12 +41,14 @@ enum class EGamePauseChange : uint8
UENUM(BlueprintType)
enum class EInputBindingType : uint8
{
/// A button action, will be looked up based on input mappings
/// A legacy button action, will be looked up based on input mappings
Action = 0,
/// An axis action, will be looked up based on input mappings
/// An legacy axis action, will be looked up based on input mappings
Axis = 1,
/// A manually specified FKey (which can be key, button, axis)
Key = 2
Key = 2,
/// An EnhancedInput action
EnhancedInputAction = 3
};
/// What order of preference should we return input images where an action/axis has multiple mappings

View File

@ -0,0 +1,23 @@
#pragma once
#include "CoreMinimal.h"
#include "StevesPluginSettings.generated.h"
/**
* Settings for the plug-in.
*/
UCLASS(config=Engine)
class STEVESUEHELPERS_API UStevesPluginSettings
: public UObject
{
GENERATED_BODY()
public:
/// Which directories to search for Enhanced Input Actions when referenced just by name in e.g. Rich Text Decorator
UPROPERTY(config, EditAnywhere, Category = StevesUEHelpers, meta = (DisplayName = "Directories to search for Enhanced Input Actions", RelativeToGameContentDir, LongPackageName))
TArray<FDirectoryPath> EnhancedInputActionSearchDirectories;
UStevesPluginSettings() {}
};

View File

@ -23,6 +23,10 @@ protected:
UPROPERTY(EditAnywhere)
FName ActionOrAxisName;
/// If binding type is EnhancedInputAction, a reference to an enhanced input action
UPROPERTY(EditAnywhere) // can't be inside #if
TSoftObjectPtr<UInputAction> InputAction;
/// Where there are multiple mappings, which to prefer
UPROPERTY(EditAnywhere)
EInputImageDevicePreference DevicePreference = EInputImageDevicePreference::Auto;
@ -42,6 +46,8 @@ protected:
bool bSubbedToInputEvents = false;
bool bIsDirty = true;
float DelayUpdate = 0;
bool bHiddenBecauseBlank;
ESlateVisibility OldVisibility;
public:
@ -57,6 +63,10 @@ public:
UFUNCTION(BlueprintCallable)
virtual void SetFromKey(FKey K);
/// Tell this image to display Enhanced InputAction
UFUNCTION(BlueprintCallable)
virtual void SetFromInputAction(UInputAction* Action);
/// Get the binding type that we'll use to populate the image
UFUNCTION(BlueprintCallable)
virtual EInputBindingType GetBindingType() const { return BindingType; }
@ -96,5 +106,7 @@ protected:
UFUNCTION()
void OnInputModeChanged(int ChangedPlayerIdx, EInputMode InputMode);
UFUNCTION()
void OnEnhancedInputMappingsChanged();
};

View File

@ -25,10 +25,11 @@ public class StevesUEHelpers : ModuleRules
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"Slate",
"SlateCore",
"UMG",
"Paper2D"
"Paper2D",
}
);

View File

@ -17,10 +17,15 @@ InputImage requires a [UiTheme](UiTheme.md) to operate, which links to the image
### Binding Type
* "Action" if the image should display the current mapping for an input action
* "Axis" to look up an input axis
* "Enhanced Input Action" to specify an [Enhanced Input](https://docs.unrealengine.com/5.1/en-US/enhanced-input-in-unreal-engine/) action
* "Action" if the image should display the current mapping for a legacy input action
* "Axis" to look up a legacy input axis
* "Key" to manually specify a key (which can be gamepad or mouse too)
### Enhanced Input Action
Pick an Enhanced Input action from the asset browser interface.
### Action or Axis Name
The name of the input action or axis that should be looked up to determine the

View File

@ -33,13 +33,21 @@ related to input controls. There are various options:
## Adding input images to rich text
### Input Actions
### Enhanced Input Actions
`<input eaction="IA_MyAction"/>`
This displays the image for a bound Enhanced Input action. The name of the action
should match the action name, which is relative to one of the directories
you specify in Project Settings > Plugins > StevesUEHelpers.
### Legacy Input Actions
`<input action="TheActionName"/>`
This displays the image for a bound action input, as configured in project settings.
### Input Axes
### Legacy Input Axes
`<input axis="TheAxisName"/>`