Custom Input Devices

From Epic Wiki

Overview

This page will detail how to create custom Input Device plugins in order to add support for additional controller/input types. It will also show example code for adding additional Key/Gamepad Input Names. The code will show how to fire events from the existing Input Names via a MessageHandler and how to fire events from any Key/Gamepad Input directly.

The following is based on a private plugin (Integrating Nintendo Wii Controllers/Sensors into UE4), and an Oculus plugin shipped with the engine (Engine/Plugins/Runtime/OculusInput).

The actor is a useful way of referencing the InputDevice to run arbitrary methods. For some reason if the two actor classes are deleted this all fails to compile (Possibly related to *.generated.h files). If someone figures out why I would love to know (mspe044).

Example Code

The code will show how to create a plugin for an input device named PseudoController which will simulate controller#1 pressing and holding the bottom face button (jump) and moving the right analog stick wildly. It will also create a new custom Gamepad Input called "Pseudo Player Weight" and fire events for this input with a value of 75.0 (kg).

In your own code you will most likely link your plugin with a static/dynamic library which communicates with you Input Device. When the engine calls FPseudoControllerInputDevice::SendControllerEvents() you can then pass on any events/polled controller states using the MessageHandler in a generic way.

uPlugin Ddefinition

/Plugins/PseudoController/PseudoController.uplugin

{
    "FileVersion" : 0,
	
	"FriendlyName" : "Pseudo Controller Plugin",
	"Version" : 0,
	"VersionName" : "0.2",
	"CreatedBy" : "{}",
	"EngineVersion" : 1579795,
	"Description" : "I wish I was a real input! :'(",
	"Category" : "Tutorial",
	
	"Modules" :
	[
		{
			"Name" : "PseudoController",
			"Type" : "Runtime",
			"LoadingPhase" : "PreDefault",
			"WhitelistPlatforms" : [ "Win64", "Win32" ]
		}
	]
}

Module Build File

This is where you link to any library supporting your Input Device. There is a sample method 'LoadYourThirdPartyLibraries()' to help you do this however the call to it is currently commented out.

This method will will link to a static library in .../Plugins/PseudoController/Source/PseudoController/ThirdParty/LibraryDirName/... with include files in the sub directory .../include/ and library code in subdirectoires seprated by compile arcitecture I.E. .../Win64/VS2013/MyLibrary.lib

/Plugins/PseudoController/Source/PseudoController/PseudoController.Build.cs

namespace UnrealBuildTool.Rules
{
    using System.IO; // ToDo: Replace with standard mechenism

    public class PseudoController : ModuleRules
    {
        public PseudoController(TargetInfo Target)
        {
            PCHUsage = PCHUsageMode.NoSharedPCHs;

            // ... add public include paths required here ...
            PublicIncludePaths.AddRange( new string[] {
                "PseudoController/Public",
                "PseudoController/Classes",
            });

            // ... add other private include paths required here ...
            PrivateIncludePaths.AddRange( new string[] {
                "PseudoController/Private",
            });

            // ... add other public dependencies that you statically link with here ...
            PublicDependencyModuleNames.AddRange( new string[] { 
                "Core", 
                "CoreUObject",      // Provides Actors and Structs
                "Engine",           // Used by Actor
                "Slate",            // Used by InputDevice to fire bespoke FKey events
                "InputCore",        // Provides LOCTEXT and other Input features
                "InputDevice",      // Provides IInputInterface
            });

            // ... add private dependencies that you statically link with here ...
            PrivateDependencyModuleNames.AddRange( new string[] {
            });

            // ... add any modules that your module loads dynamically here ...
            DynamicallyLoadedModuleNames.AddRange( new string[] { 
            });

            // !!!!!!!!!! UNCOMMENT THIS IF YOU WANT TO CALL A LIBRARY !!!!!!!!!!
            //LoadYourThirdPartyLibraries(Target);
        }

        public bool LoadYourThirdPartyLibraries(TargetInfo Target)
        {
            bool isLibrarySupported = false;

            // This will give oyu a relitive path to the module ../PseudoController/
            string ModulePath = Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name));
            // This will give you a relative path to ../PseudoController/ThirdParty/"LibraryDirName"/
            string MyLibraryPath = Path.Combine(ModulePath, "ThirdParty", "LibraryDirName");

            // Use this to keep Win32/Win64/e.t.c. library files in seprate subdirectories
            string ArchitecturePath = "";

            // When you are building for Win64
            if (Target.Platform == UnrealTargetPlatform.Win64 &&
                WindowsPlatform.Compiler == WindowsCompiler.VisualStudio2013)
            {
                // We will look for the library in ../PseudoController/ThirdParty/MyLibrary/Win64/VS20##/
                ArchitecturePath = Path.Combine("Win64", "VS" + WindowsPlatform.GetVisualStudioCompilerVersionName());

                isLibrarySupported = true;
            }
            // When you are building for Win32
            else if (Target.Platform == UnrealTargetPlatform.Win32 &&
                WindowsPlatform.Compiler == WindowsCompiler.VisualStudio2013)
            {
                // We will look for the library in ../PseudoController/ThirdParty/MyLibrary/Win32/VS20##/
                ArchitecturePath = Path.Combine("Win32", "VS" + WindowsPlatform.GetVisualStudioCompilerVersionName());

                isLibrarySupported = true;
            }
            // Add mac/linux/mobile support in much the same way

            // If the current build architecture was supported by the above if statements
            if (isLibrarySupported)
            {
                // Add the architecture spacific path to the library files
                PublicAdditionalLibraries.Add(Path.Combine(MyLibraryPath, "lib", ArchitecturePath, "MyLibrary.lib"));
                // Add a more generic path to the include header files
                PublicIncludePaths.Add(Path.Combine(MyLibraryPath, "include"));
            }

            // Defination lets us know whether we successfully found our library!
            Definitions.Add(string.Format("WITH_MY_LIBRARY_PATH_USE={0}", isLibrarySupported ? 1 : 0));

            return isLibrarySupported;
        }
    }
}

IPlugin Header File

/Plugins/PseudoController/Source/PseudoController/Public/IPseudoControllerPlugin.h

#pragma once

#include "ModuleManager.h"
#include "IInputDeviceModule.h"

#include "InputCoreTypes.h"

/**
 * The public interface to this module.  In most cases, this interface is only public to sibling modules 
 * within this plugin.
 */
class IPseudoControllerPlugin : public IInputDeviceModule
{
public:
	/**
	 * Singleton-like access to this module's interface.  This is just for convenience!
	 * Beware of calling this during the shutdown phase, though.  Your module might have been unloaded already.
	 *
	 * @return Returns singleton instance, loading the module on demand if needed
	 */
	static inline IPseudoControllerPlugin& Get() {
		return FModuleManager::LoadModuleChecked< IPseudoControllerPlugin >("PseudoController");
	}

	/**
	 * Checks to see if this module is loaded and ready.  It is only valid to call Get() if IsAvailable() returns true.
	 *
	 * @return True if the module is loaded and ready to use
	 */
	static inline bool IsAvailable() {
		return FModuleManager::Get().IsModuleLoaded( "PseudoController" );
	}
};

PCH File

/Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerPrivatePCH

// You should place include statements to your module's private header files here.  You only need to
// add includes for headers that are used in most of your module's source files though.

#include "Core.h"
#include "CoreUObject.h"

#include "IPseudoControllerPlugin.h"

Plugin Header File

/Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerPlugin.h

#pragma once
#include "PseudoControllerPrivatePCH.h"

class FPseudoControllerPlugin : public IPseudoControllerPlugin
{
public:
	/** IPseudoControllerInterface implementation */
	virtual TSharedPtr< class IInputDevice > CreateInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler);

	//virtual void StartupModule() OVERRIDE; // This is not required as IInputDeviceModule handels it!
	virtual void ShutdownModule() OVERRIDE;

	TSharedPtr< class FPseudoControllerInputDevice > PseudoInputDevice;
};

Plugin Cpp File

/Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerPlugin.cpp

#include "PseudoControllerPrivatePCH.h"

#include "Internationalization.h" // LOCTEXT
#include "InputCoreTypes.h"

#include "PseudoControllerPlugin.h"

#include "Engine.h" // Are these both necessary?
#include "EngineUserInterfaceClasses.h" // Are these both necessary?

#include "IPseudoControllerPlugin.h"

#include "PseudoController.generated.inl"

IMPLEMENT_MODULE(FPseudoControllerPlugin, PseudoController)
DEFINE_LOG_CATEGORY_STATIC(PseudoControllerPlugin, Log, All);

#define LOCTEXT_NAMESPACE "InputKeys"


// This function is called by *Application.cpp after startup to instantiate the modules InputDevice
TSharedPtr< class IInputDevice > FPseudoControllerPlugin::CreateInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler)
{
	UE_LOG(PseudoControllerPlugin, Log, TEXT("Create Input Device"));

	FPseudoControllerPlugin::PseudoInput = MakeShareable(new FPseudoControllerInputDevice(InMessageHandler));

	return PseudoInput;
}

#undef LOCTEXT_NAMESPACE

// This function may be called during shutdown to clean up the module.
void FPseudoControllerPlugin::ShutdownModule()
{
	FPseudoControllerPlugin::PseudoInput->~FPseudoControllerInputDevice();

	UE_LOG(PseudoControllerPlugin, Log, TEXT("Shutdown Module"));
}

Input Device Header File

/Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerInputDevice.h

#pragma once

#include "PseudoControllersInputState.h"
#include "IInputDevice.h"

#define MAX_NUM_PSEUDO_INPUT_CONTROLLERS	4 // We dont realy have any input controllers, this is a sham! :P
#define NUM_PSEUDO_INPUT_BUTTONS			6 // I've only used the one button but w/evs

/**
* Type definition for shared pointers to instances of FMessageEndpoint.
*/
// ToDo: Is this necessary?
typedef TSharedPtr<class FPseudoControllerInputDevice> FPseudoControllerInputDevicePtr;

/**
* Type definition for shared references to instances of FMessageEndpoint.
*/
// ToDo: Is this necessary?
typedef TSharedRef<class FPseudoControllerInputDevice> FPseudoControllerInputDeviceRef;

/**
* Interface class for WiiInput devices (wii devices)
*/
class FPseudoControllerInputDevice : public IInputDevice
{
public:
	FPseudoControllerInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& MessageHandler);

	/** Tick the interface (e.g. check for new controllers) */
	virtual void Tick(float DeltaTime) OVERRIDE;

	/** Poll for controller state and send events if needed */
	virtual void SendControllerEvents() OVERRIDE;

	/** Set which MessageHandler will get the events from SendControllerEvents. */
	virtual void SetMessageHandler(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler) OVERRIDE;

	/** Exec handler to allow console commands to be passed through for debugging */
	virtual bool Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) OVERRIDE;

	// IForceFeedbackSystem pass through functions
	virtual void SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) OVERRIDE;
	virtual void SetChannelValues(int32 ControllerId, const FForceFeedbackValues &values) OVERRIDE;

	virtual ~FPseudoControllerInputDevice();
private:
	// ToDo: Is this necessary?
	bool Active;

	/** Delay before sending a repeat message after a button was first pressed */
	float InitialButtonRepeatDelay; // How long a button is held for before you send a 2nd event

	/** Delay before sending a repeat message after a button has been pressed for a while */
	float ButtonRepeatDelay; // How long a button is held for before you send a 3rd/4th/e.t.c event

	EControllerButtons::Type PseudoInputButtonMapping[NUM_PSEUDO_INPUT_BUTTONS];

	/** Last frame's button states, so we only send events on edges */
	bool PreviousButtonStates[NUM_PSEUDO_INPUT_BUTTONS];
	
	/** Next time a repeat event should be generated for each button */
	double NextRepeatTime[NUM_PSEUDO_INPUT_BUTTONS];

	TSharedRef< FGenericApplicationMessageHandler > MessageHandler;
	FPseudoControllerSensorState PseudoControllerStates[MAX_NUM_PSEUDO_INPUT_CONTROLLERS];
};

Input Device Cpp File

/Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerInputDevice.cpp

#include "PseudoControllerPrivatePCH.h"

#include "GenericPlatformMath.h"

#include "PseudoControllerInputDevice.h"

#include "SlateBasics.h"

#include "WindowsApplication.h"
#include "WindowsWindow.h"
#include "WindowsCursor.h"
#include "GenericApplicationMessageHandler.h"
#include "IInputDeviceModule.h"
#include "IInputDevice.h"

DEFINE_LOG_CATEGORY_STATIC(LogPseudoControllerDevice, Log, All);

const FKey FPseudoControllerKey::Pseudo_WeighingSensor1("Pseudo_WeighingSensor1");

const FPseudoControllerKeyNames::Type FPseudoControllerKeyNames::Pseudo_WeighingSensor1("Pseudo_WeighingSensor1");

FPseudoControllerInputDevice::FPseudoControllerInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler) : Active(true), MessageHandler(InMessageHandler) {
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Starting PseudoControllerInputDevice"));

	// Initialize button repeat delays
	InitialButtonRepeatDelay = 0.2f;
	ButtonRepeatDelay = 0.1f;

	// Register the FKeys (Gamepad key for controllers, Mouse for mice, FloatAxis for non binary values e.t.c.)
	EKeys::AddKey(FKeyDetails(FPseudoControllerKey::Pseudo_WeighingSensor1, LOCTEXT("Pseudo_WeighingSensor1", "Pseudo Weighing Sensor #1"), FKeyDetails::GamepadKey | FKeyDetails::FloatAxis));

	// Setting all buttons on my pseudo controllers to 'not pressed' 
	// (You should check the inital state of your controllers)
	PreviousButtonStates[NUM_PSEUDO_INPUT_BUTTONS] = { 0 };
	//NextRepeatTime[NUM_PSEUDO_INPUT_BUTTONS] = { 0.0 };

	// Initialize mapping of controller button mask to unreal button mask
	// - mapping 0-3 to the 'FaceButtons'
	// --- 'X','Square','Circle','Triangle' for playstation
	// --- 'A','X','B','Y' on xbox
	PseudoInputButtonMapping[0] = EControllerButtons::FaceButtonTop;		// PSEUDO_BUTTON_ZERO
	PseudoInputButtonMapping[1] = EControllerButtons::FaceButtonBottom;	// PSEUDO_BUTTON_ONE
	PseudoInputButtonMapping[2] = EControllerButtons::FaceButtonLeft;	// PSEUDO_BUTTON_TWO
	PseudoInputButtonMapping[3] = EControllerButtons::FaceButtonRight;	// PSEUDO_BUTTON_THREE
	// - mapping 4-5 to the left joystick (traditionally used for movement controls)
	PseudoInputButtonMapping[4] = EControllerButtons::LeftAnalogX;		// PSEUDO_BUTTON_FOUR
	PseudoInputButtonMapping[5] = EControllerButtons::LeftAnalogY;		// PSEUDO_BUTTON_FIVE
}

void FPseudoControllerInputDevice::Tick(float DeltaTime) {
	// This will spam the log heavily, comment it out for real plugins :)
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Tick %f"), DeltaTime);
}

void FPseudoControllerInputDevice::SendControllerEvents() {
	// Here is where we check the state of our input device proberbly by calling a method in your third party libary...
	//  - I dont have a real device (xbox controller, wiimote, e.t.c.) in this tutorial :'( so we will just pretend!!!

	// I could make libary to read from a fancy set of 'matrix' cerebellum jacks and iterate over each of those 'controllers'.. but ill save that for the next tutorial
	int controllerIndex = 0; // Apparantly I was lazy so there is only one controller firing events today!

	const double CurrentTime = FPlatformTime::Seconds(); 
	const float CurrentTimeFloat = FPlatformTime::ToSeconds(FPlatformTime::Cycles()); // Works with FMath functions

	// This weard statement simulates user holding down the button 1/3 of the time
	if (FMath::Fmod(CurrentTimeFloat, 1.5f) < .5f) {
		// If the button was pressed this tick
		if (!PreviousButtonStates[1]) {
			// Fire button pressed event
			MessageHandler->OnControllerButtonPressed(PseudoInputButtonMapping[1], controllerIndex, false);

			// this button was pressed - set the button's NextRepeatTime to the InitialButtonRepeatDelay
			NextRepeatTime[1] = CurrentTime + InitialButtonRepeatDelay;
			PreviousButtonStates[1] = true;
		// If the buttons has been held long enough to fire a nth event
		} else if (NextRepeatTime[1] <= CurrentTime) {
			// Fire button held event
			MessageHandler->OnControllerButtonPressed(PseudoInputButtonMapping[1], controllerIndex, true);

			// set the button's NextRepeatTime to the ButtonRepeatDelay
			NextRepeatTime[1] = CurrentTime + ButtonRepeatDelay;
		}
	// If the button was released this tick
	}
	else if (PreviousButtonStates[1]) {
		UE_LOG(LogPseudoControllerDevice, Log, TEXT("Release"));
		// Fire button released event
		MessageHandler->OnControllerButtonReleased(PseudoInputButtonMapping[1], controllerIndex, false);
		PreviousButtonStates[1] = false;
	}

	// This simulates the user moving the stick left and right repeatedly
	float xMove = FMath::Sin(CurrentTimeFloat * .223f * 2 * PI);
	// Fire analog input event
	MessageHandler->OnControllerAnalog(PseudoInputButtonMapping[4], controllerIndex, xMove);

	// This simulates the user moving the stick up and down repeatedly
	float yMove = FMath::Cos(CurrentTimeFloat * .278f * 2 * PI);
	// Fire analog input event
	MessageHandler->OnControllerAnalog(PseudoInputButtonMapping[5], controllerIndex, yMove);

	// This will spam the log heavily, comment it out for real plugins :)
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Sending Controller Events jump%d, x%f, y%f"), PreviousButtonStates[1], xMove, yMove);

	// This is how you fire your fancypantz new controller events... the ones you added because you couldn't find an existing EControllerButton that matched your needs!

	// We 'read' the value 75 from the weight sensor
	const unsigned short int sensorValue = 75;

	FPseudoControllerSensorState Sensor1 = PseudoControllerStates[controllerIndex].WeightAxes[(int32)EPseudoControllerAxes::WeightSensor1];
	if(sensorValue != Sensor1.State)
	{
		Sensor1.State = sensorValue;

		FSlateApplication::Get().OnControllerAnalog(Sensor1.Axis, controllerIndex, Sensor1.GetValue()); // This will spam the calculated weight to my fancy new output type!
	}
}

void FPseudoControllerInputDevice::SetMessageHandler(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler) {
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Set Message Handler"));
	MessageHandler = InMessageHandler;
}

bool FPseudoControllerInputDevice::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) {
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Execute Console Command: %s"), Cmd);

	// Put your fancy custom console command code here... 
	// ToDo: use this to let you fire pseudo controller events

	return true;
}

// IForceFeedbackSystem pass through functions
//  - I *believe* this is a handel for the game to communicate back to your third party libary (i.e. game tells joystick to increase force feedback/vibrate/turn on/off a light)
void FPseudoControllerInputDevice::SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) {
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Set Force Feedback %f"), Value);
}
void FPseudoControllerInputDevice::SetChannelValues(int32 ControllerId, const FForceFeedbackValues &values) {
	// This will spam the log heavily, comment it out for real plugins :)
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Set Force Feedback Values"));
}

// This is where you nicely clean up your plugin when its told to shut down!
//  - USE THIS PLEASE!!! no one likes memory leaks >_<
FPseudoControllerInputDevice::~FPseudoControllerInputDevice() {
	UE_LOG(LogPseudoControllerDevice, Log, TEXT("Closing PseudoControllerInputDevice"));
}

Input State Header File

This is where you define custom struts to rember FKeys / FNames / Values for custom input types (Pseudo_WeighingSensor1) /Plugins/PseudoController/Source/PseudoController/Private/PseudoControllerInputState.h

#pragma once

#include "InputCoreTypes.h"

/**
 * Digital weight sensors on the Wii Balance Board
 */


enum class EPseudoControllerAxes
{
	WeightSensor1 = 0,

	/** Total number of weight axes */
	TotalAxisCount = 1
};

struct FPseudoControllerKey
{
	static const FKey Pseudo_WeighingSensor1;
};

struct FPseudoControllerKeyNames
{
	typedef FName Type;

	static const FName Pseudo_WeighingSensor1;
};

/**
 * Capacitive Axis State
 */
struct FPseudoControllerSensorState
{
	/** The axis that this sensor state maps to */
	FName Axis;

	/** The zero value (no additional weight) */
	unsigned short int Zero;

	/** What is the current sensor reading, from 0.f to 1.f */
	float Scale;

	/** What is the current sensor reading, from 0.f to 1.f */
	unsigned short int State;

	FPseudoControllerSensorState()
		: Axis(NAME_None)
		, Zero(900)
		, Scale(100.f)
		, State(0)
	{
	}

	float GetValue() {
		if(State < Zero) {
			return 0.f;
		}

		return (float)(State - Zero) * Scale;
	}
};

/**
 * Input state for an pseudo controller
 */
struct FPseudoControllerState
{
	/** Weight axes */
	FPseudoControllerSensorState WeightAxes[(int32)EWiiBalanceAxes::TotalAxisCount];

	/** Default constructor */
	FPseudoControllerState()
	{
		WeightAxes[(int32)EPseudoControllerAxes::WeightSensor1].Axis = FPseudoControllerKeyNames::Pseudo_WeighingSensor1;
	}
};

Input Actor Cpp File

/Plugins/PseudoController/Source/PseudoController/Classes/PseudoActor.cpp

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "PseudoControllerPrivatePCH.h"

#include "Engine.h"
#include "PseudoActorObject.h"

#include "PseudoControllerPlugin.h"
#include "PseudoControllerInputDevice.h"

DEFINE_LOG_CATEGORY_STATIC(LogPseudoActor, Log, All);

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.

APseudoActorObject::APseudoActorObject(const class FObjectInitializer& PCIP) : Super(PCIP) {
	UE_LOG(LogPseudoActor, Log, TEXT("Construct"));
}

void APseudoActorObject::FunkyMethod() {
	UE_LOG(LogPseudoActor, Log, TEXT("FunkyMethod()"));
}

Input Actor Header File

/Plugins/PseudoController/Source/PseudoController/Classes/PseudoActor.h

#pragma once
#include "GameFramework/Actor.h"
#include "PseudoActorObject.generated.h"

UCLASS(MinimalAPI, hidecategories = (Input))
class APseudoActorObject : public AActor
{
	GENERATED_UCLASS_BODY()

	UFUNCTION(BlueprintCallable, Category = "Development")
	virtual void FunkyMethod();
};

( )