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:
- Enable support for input events in the graph
References: Engine/Blueprint.h
- Bind the graph’s input events to an InputComponent
References: Engine/InputDelegateBinding.h
- 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;
}
}
}