Input events on UObject graph?

I’m implementing a finite state machine where the states inherit from UObject (UMyState : UBaseState (=blueprintable) : UObject). The purpose of this setup is to neatly organize which UMG widgets to show and which player controls to have active based on the current game context, which are all settings of the “state”. Right now, I’m trying to find out whether its possible to have input events in these blueprint graphs.

Actors inherently support input events. Does someone know a way to have this work for UObjects too?

Not without source modifications. Input event blueprint nodes can only be placed in the graph if it’s an actor blueprint being edited. Changing this requires a modification to source code. See UK2Node_InputKey::IsCompatibleWithGraph(), which calls this:



bool UBlueprint::SupportsInputEvents() const
{
    return FBlueprintEditorUtils::IsActorBased(this);
}


To be fair, actors only support input events because there is some engine plumbing in place to create a UInputComponent (EnablePlayerInput), which handles the routing of said input. Without a valid input component, you can’t process input properly anyway really.

Thanks for the information! It seems UBlueprint::SupportsInputEvents() is a virtual function so I will try to see what happens if I override it in a custom UBlueprint class.

As for the input component, the state machine is in an actor component that is added to the PlayerController so its no problem for me to pass its InputComponent down.

I got this to work! I’ll share my steps for future Googlers.

How to support input events on UObject (non-actor) graphs
There are three steps that have to be taken:

  1. Enable support for input events in the graph
    References: Engine/Blueprint.h
  2. Bind the graph’s input events to an InputComponent
    References: Engine/InputDelegateBinding.h
  3. Push the InputComponent onto the PlayerController’s stack
    References: Actor.h

Step 1: Enable support for input events on object graph
The graph editor calls UBlueprint::SupportsInputEvents to check whether input events can be added, so we’ll create a custom UBlueprint class and override that function.



UCLASS()
class CONTROLMODES_API UControlModeBlueprint : public UBlueprint
{
    GENERATED_BODY()

public:
    // Begin UBlueprint
    virtual bool AllowsDynamicBinding() const { return true; }
    virtual bool SupportsInputEvents() const { return true; }
    // End UBlueprint
};


Now to tell the engine that when constructing objects of my custom UObject class, to use this blueprint class. This has to be done in the factory. The method of modifying/replacing the factory behavior depends on how you want to create assets.

Case 1: UCLASS(Blueprintable)

Blueprintable objects are created in the content browser by Right Click > New Blueprint and choosing your class. This will always use UBlueprintFactory as factory, but you can externally affect which blueprint class it will use for specific classes. UBlueprintFactory internally iterates through a global list of IBlueprintCompiler which can suggest a UBlueprint class. Implement an IBlueprintCompiler in an editor module and add it to the global list like below. Add “KismetCompiler” to the editor module’s list of public dependencies.



class FControlModesEditor : public IControlModesEditor, IBlueprintCompiler
{
    /** IModuleInterface implementation */
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;

**  /** IBlueprintCompiler implementation */
    virtual bool CanCompile(const UBlueprint* Blueprint);
    virtual void Compile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results, TArray<UObject*>* ObjLoaded) override;
    virtual bool GetBlueprintTypesForClass(UClass* ParentClass, UClass*& OutBlueprintClass, UClass*& OutBlueprintGeneratedClass) const override;**

};

IMPLEMENT_MODULE( FControlModesEditor, ControlModesEditor )

void FControlModesEditor::StartupModule()
{
    IKismetCompilerInterface& KismetCompilerModule = FModuleManager::LoadModuleChecked<IKismetCompilerInterface>("KismetCompiler");
    KismetCompilerModule.GetCompilers().Add(this);
}

void FControlModesEditor::ShutdownModule()
{
}

bool FControlModesEditor::CanCompile(const UBlueprint* Blueprint)
{
    // This IBlueprintCompiler won't actually take on compiling tasks
    return false;
}

void FControlModesEditor::Compile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results, TArray<UObject*>* ObjLoaded)
{
    // This IBlueprintCompiler won't do any compiling
}

bool FControlModesEditor::GetBlueprintTypesForClass(UClass* ParentClass, UClass*& OutBlueprintClass, UClass*& OutBlueprintGeneratedClass) const
{
    if (ParentClass->IsChildOf(UControlModeBase::StaticClass()))
    {
        // This IBlueprintCompiler will suggest UControlModeBlueprint as blueprint class for UControlModeBase
        OutBlueprintClass = UControlModeBlueprint::StaticClass();
        OutBlueprintGeneratedClass = UBlueprintGeneratedClass::StaticClass();
        return true;
    }

    // Other object classes are unaffected by this
    OutBlueprintClass = nullptr;
    OutBlueprintGeneratedClass = nullptr;
    return false;
}


Case 2: Custom asset type

For custom asset types, you can assign the UBlueprint in the implementation of UMyAssetFactory::FactoryCreateNew().

2. Bind the graph’s input events to an InputComponent

There is some very useful reference code in AActor::EnableInput which calls:



            if (UInputDelegateBinding::SupportsInputDelegate(GetClass()))
            {
                UInputDelegateBinding::BindInputDelegates(GetClass(), InputComponent);
            }


Basically, the UInputDelegateBinding::BindInputDelegates parses the blueprint class’ graphs and binds it to the input component. There is however a problem: internally it assumes that the InputComponent’s owning actor is the instance of that blueprint class. For example, if you open InputKeyDelegateBinding.cpp you’ll find this line:



KB.KeyDelegate.BindDelegate(InputComponent->GetOwner(), Binding.FunctionNameToBind);


which is a big problem because limitating input events to owning actors is exactly what we want to circumvent.

In the future, hopefully UInputDelegateBinding::BindInputDelegates() will get an extra parameter which is the graph owning object. For now though, I’ve made my own variant of UInputDelegateBinding::BindInputDelegates that replaces InputComponent->GetOwner() with my UObject instance, which works! This can be done without altering engine code.

However, to support all 6 input types (key, axis, action, touch, etc) you will have to duplicate some code from all those classes (UInputKeyDelegateBinding, UInputActionDelegateBinding, etc, etc). Below is my example which supports keys and actions, but not other input types. Those can be added but aren’t necessary in my project.



void UControlModesFunctionLibrary::BindInputDelegatesToObject(const UClass* InClass, UInputComponent* InputComponent, UObject* GraphObject)
{
    static UClass* InputBindingClasses] = {
        UInputActionDelegateBinding::StaticClass(),
        UInputAxisDelegateBinding::StaticClass(),
        UInputKeyDelegateBinding::StaticClass(),
        UInputTouchDelegateBinding::StaticClass(),
        UInputAxisKeyDelegateBinding::StaticClass(),
        UInputVectorAxisDelegateBinding::StaticClass(),
    };

    if (InClass)
    {
        // Bind parent class input delegates
        BindInputDelegatesToObject(InClass->GetSuperClass(), InputComponent, GraphObject);

        // Bind own graph input delegates
        for (int32 Index = 0; Index < ARRAY_COUNT(InputBindingClasses); ++Index)
        {
            UInputDelegateBinding* BindingObject = CastChecked<UInputDelegateBinding>(UBlueprintGeneratedClass::GetDynamicBindingObject(InClass, InputBindingClasses[Index]), ECastCheckedType::NullAllowed);
            if (!BindingObject)
                continue;

            if (UInputKeyDelegateBinding* KeyBinding = dynamic_cast<UInputKeyDelegateBinding*>(BindingObject))
            {
                TArray<FInputKeyBinding> BindsToAdd;

                    for (int32 BindIndex = 0; BindIndex < KeyBinding->InputKeyDelegateBindings.Num(); ++BindIndex)
                    {
                        const FBlueprintInputKeyDelegateBinding& Binding = KeyBinding->InputKeyDelegateBindings[BindIndex];
                        FInputKeyBinding KB(Binding.InputChord, Binding.InputKeyEvent);
                        KB.bConsumeInput = Binding.bConsumeInput;
                        KB.bExecuteWhenPaused = Binding.bExecuteWhenPaused;

                        // Originally instead of GraphObject, it said InputComponent->GetOwner() here
                        KB.KeyDelegate.BindDelegate(GraphObject, Binding.FunctionNameToBind);

                        if (Binding.bOverrideParentBinding)
                        {
                            for (int32 ExistingIndex = InputComponent->KeyBindings.Num() - 1; ExistingIndex >= 0; --ExistingIndex)
                            {
                                const FInputKeyBinding& ExistingBind = InputComponent->KeyBindings[ExistingIndex];
                                if (ExistingBind.Chord == KB.Chord && ExistingBind.KeyEvent == KB.KeyEvent)
                                {
                                    InputComponent->KeyBindings.RemoveAt(ExistingIndex);
                                }
                            }
                        }

                        // To avoid binds in the same layer being removed by the parent override temporarily put them in this array and add later
                        BindsToAdd.Add(KB);
                    }

                for (int32 Index = 0; Index < BindsToAdd.Num(); ++Index)
                {
                    InputComponent->KeyBindings.Add(BindsToAdd[Index]);
                }
            }
            else if (UInputActionDelegateBinding* ActionBinding = dynamic_cast<UInputActionDelegateBinding*>(BindingObject))
            {
                TArray<FInputActionBinding> BindsToAdd;

                for (int32 BindIndex = 0; BindIndex < ActionBinding->InputActionDelegateBindings.Num(); ++BindIndex)
                {
                    const FBlueprintInputActionDelegateBinding& Binding = ActionBinding->InputActionDelegateBindings[BindIndex];

                    FInputActionBinding AB(Binding.InputActionName, Binding.InputKeyEvent);
                    AB.bConsumeInput = Binding.bConsumeInput;
                    AB.bExecuteWhenPaused = Binding.bExecuteWhenPaused;

                    // Originally instead of GraphObject, it said InputComponent->GetOwner() here
                    AB.ActionDelegate.BindDelegate(GraphObject, Binding.FunctionNameToBind);

                    if (Binding.bOverrideParentBinding)
                    {
                        for (int32 ExistingIndex = InputComponent->GetNumActionBindings() - 1; ExistingIndex >= 0; --ExistingIndex)
                        {
                            const FInputActionBinding& ExistingBind = InputComponent->GetActionBinding(ExistingIndex);
                            if (ExistingBind.GetActionName() == AB.GetActionName() && ExistingBind.KeyEvent == AB.KeyEvent)
                            {
                                InputComponent->RemoveActionBinding(ExistingIndex);
                            }
                        }
                    }

                    // To avoid binds in the same layer being removed by the parent override temporarily put them in this array and add later
                    BindsToAdd.Add(AB);
                }

                for (int32 Index = 0; Index < BindsToAdd.Num(); ++Index)
                {
                    InputComponent->AddActionBinding(BindsToAdd[Index]);
                }
            }
        }
    }
}



3. Pushing the InputComponent onto the PlayerController’s InputStack

AActor::EnableInput() is good reference for this. What’s worth mentioning is that you can manually create and destroy InputComponents as if they were simple objects. There is no special setup involved and they don’t have to be owned by an actor to work. Ultimately you just register it to the PlayerController will be doing all polling and event passing.

Here is my code for creating and destroy an input component on-demand in my object class:




void UControlModeBase::EnableInput()
{
    APlayerController* PC = GetWorld()->GetFirstPlayerController();
    if (PC)
    {
        // If it doesn't exist create it and bind delegates
        if (!InputComponent)
        {
            static const FName InputCompName("ControlModeInput");
            InputComponent = NewObject<UInputComponent>(this, InputCompName);
            InputComponent->bBlockInput = false;
            InputComponent->Priority = 5;

            if (UInputDelegateBinding::SupportsInputDelegate(GetClass()))
                UControlModesFunctionLibrary::BindInputDelegatesToObject(GetClass(), InputComponent, this);
        }

        PC->PushInputComponent(InputComponent);
    }
}

void UControlModeBase::DisableInput()
{
    APlayerController* PC = GetWorld()->GetFirstPlayerController();
    if (PC)
    {
        if (InputComponent)
        {
            PC->PopInputComponent(InputComponent);
            InputComponent->DestroyComponent();
            InputComponent = nullptr;
        }
    }
}


2 Likes

Good stuff, glad to see you solved it! Extra points for coming back and posting the solution!

Hehe sure thing! Hope someone finds this helpful in the future.