Custom Input Devices

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/Game...

Updated over 4 years ago Edit Page Revisions

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" : "mspe044@gmail.com",
    "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 >&amp; 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 >&amp; 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 FPseudoControllerInputDevicePtr;

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

/**
* Interface class for WiiInput devices (wii devices)
*/
class FPseudoControllerInputDevice : public IInputDevice
{
public:
    FPseudoControllerInputDevice(const TSharedRef< FGenericApplicationMessageHandler >&amp; 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 >&amp; InMessageHandler) OVERRIDE;

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

    // IForceFeedbackSystem pass through functions
    virtual void SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) OVERRIDE;
    virtual void SetChannelValues(int32 ControllerId, const FForceFeedbackValues &amp;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 >&amp; 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] 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 >&amp; InMessageHandler) {
    UE_LOG(LogPseudoControllerDevice, Log, TEXT("Set Message Handler"));
    MessageHandler = InMessageHandler;
}

bool FPseudoControllerInputDevice::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice&amp; 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 &amp;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&amp; 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();
};

()