UE5-inspired Enhanced Input System for Godot 4.7 C# - InputForge

Hey everyone,

I’ve been learning Godot for a couple of days, coming from Unreal Engine. While exploring the input system I found myself missing UE5’s Enhanced Input — specifically the ability to have the same physical key mean different things depending on the active context.

I decided to build something similar as a learning exercise, and it turned into a proper plugin. It’s probably rough around the edges since I’m new to Godot, so any feedback is very welcome!

What it does:

  • Context stack — push/pop InputMappingContext resources at runtime. The same Space key can be “Jump” in gameplay and “Confirm” in a menu
  • Unified InputKey — keyboard, mouse, and gamepad in one Inspector-friendly resource
  • Modifier pipeline — Deadzone, Invert, Normalize, Scale, Swizzle
  • Trigger pipeline — OnKeyDown, OnKeyUp, OnChange, Continuous
  • Type-safe callbacks — bind Action<bool>, Action<float>, Action<Vector2>, or Action<Vector3>
  • Zero Godot InputMap dependency
  • Auto-registers EnhancedInputSystem as an autoload on plugin enable

Quick example:

[Export] public InputMappingContext GameplayContext { get; set; }
[Export] public InputAction JumpAction { get; set; }

public override void _Ready()
{
    EnhancedInputSystem.GetInstance().AddContext(GameplayContext);
    GameplayContext.BindAction(JumpAction, OnJump);
}

private void OnJump(bool pressed)
{
    if (!pressed) return;
    GD.Print("Jumped!");
}

Everything is configured from the Inspector — no extra code needed for common setups.

Links:

Would love any feedback, especially from experienced Godot developers who might spot things I’ve missed!

This seems interesting, but a couple of things stood out to me as a little odd.

You can very easily have multiple inputs have the same physical key. Context switching happens inside your code, you only listen to the inputs you currently care about. At least that’s how I’ve been doing it for a while now. You can do GetViewport().SetInputAsHandled() to consume an input event.

This is already built into the default input map.

I overall feel like completely ditching the base InputMap isn’t necessary, since it already does a lot of things really well.

But it looks interesting for sure! Most godot users only use the base, GDScript version of the engine, and unfortunately C# addons do not work in the base engine.

1 Like

Thanks for taking the time to share this — really appreciate the honest feedback!

You make fair points. Manual context switching with SetInputAsHandled() works well, and Godot’s InputMap already covers a lot of ground. InputForge is more about making that pattern declarative and Inspector-driven rather than solving something impossible — if you’re already comfortable managing context in code, you probably don’t need it.

The analog axis case is where the trigger pipeline adds a bit more value over the built-in system, but you’re right that for most use cases the default InputMap is perfectly fine.

And yes, the C# limitation is unfortunately a real barrier for most Godot users. Hopefully that improves over time!

I can do everything you are doing in GDScript with InputMap. Your decoupling from InputMap is not a benefit, but a drawback. You’ve re-created a bunch of C++ functionality in C#, so it’s slower, and is not tested by tens of thousands of people (probably much more TBH).

I like the Delta thing you’re doing with the mouse. I do something similar in my Controller Plugin. It was my GDScript attempt at doing kinda what you are doing here. Centralizing things I needed for re-use. But as time goes on, I find that it’s overkill in most cases, and a lot of the functionality could be handled by Components.

To me, this looks like someone with programming experience re-inventing the wheel because they don’t understand how Godot works yet. It’s a great learning experience for you, but has little value to anyone else as a plugin because there’s already documentation and lots of videos and tutorials on how to do these things with the existing Godot functionality.

You can already do this in GDScript.

Unlikely. C# is a value-add, not a core part of the engine. If you really feel this add-on has value, you need to redo it in GDScript. I’d also recommend some screenshots in your Readme. Configuring input with your plugin seems more involved than the existing Godot solution, so you need to make it as simple as possible to follow.

1 Like

Hello;
Thanks for sharing your plugin — looked through the code and it’s a solid reference.

After comparing the two, we’re solving completely different problems. Your plugin handles gamepad detection, icon mapping, and rebinding — things InputForge doesn’t touch at all.

The core difference is the context stack. InputForge is explicitly inspired by Unreal Engine 5’s Enhanced Input System. Its primary goal is managing the runtime context stack — allowing the exact same physical key to mean entirely different things depending on the game state:

// gameplay — Space = Jump
EnhancedInputSystem.GetInstance().AddContext(GameplayContext);
GameplayContext.BindAction(JumpAction, OnJump);

// entering menu — same Space key now = Confirm, no code change required
EnhancedInputSystem.GetInstance().AddContext(UIContext);
UIContext.BindAction(ConfirmAction, OnConfirm);

Internally, _Input() iterates the context stack in reverse priority and calls SetInputAsHandled() as soon as a context consumes the event — lower-priority contexts never see it:

for (int i = _activeContexts.Count - 1; i >= 0; i--)
{
    if (contextHandledAnyInput && @event is not InputEventMouseMotion)
    {
        GetViewport().SetInputAsHandled();
        return;
    }
}

All input types flow through the same Vector3 carrier — boolean, 1D/2D axis, mouse delta — so the modifier and trigger pipeline is uniform regardless of device:

var value = mapping.InputSource.GetValue();
value = mapping.ApplyModifiers(value);
if (mapping.EvaluateTriggers(value, @event))
    context.PushAction(mapping.TargetAction, value, @event);

Regarding the C# performance concern: there is virtually no overhead. There is no per-frame polling, no string-based action lookups at runtime, and no godot signals on hot paths. The entire pipeline relies on raw method calls and c# delegates, a simple list iteration over the active context stack. Since C# executes this incredibly fast, it is highly optimized for performance.

While C# might be a barrier for some Godot users, InputForge is built specifically for C# developers who want a robust, type-safe, and scalable architecture inspired by industry standards (like UE5) rather than relying on standard InputMap string names.

Also, thanks for the feedback on the README and screenshots! You’re totally right that configuring this through resources can look more involved at first glance compared to standard Godot. I’ll definitely update the documentation with visual guides and screenshots to make the onboarding process simpler.

Different tools for different architectural needs — appreciate the feedback.

correct me if I’m wrong but wasn’t the library going away in favor of the asset store? in any case would you mind adding it there as a free item as well?

seems similar

@DaDarkDragon wasn’t the library going away in favor of the asset store?

You’re right — InputForge is going up on both simultaneously: the Asset Store submission is currently pending approval, and the Library listing is already live.

@struhy_xd seems similar

Good catch, and credit where G.U.I.D.E is a much larger, more comprehensive addon. It ships its own UI bindings and a lot more surface area. I’ll also be upfront: we took a fair bit of inspiration from G.U.I.D.E on the API surface; how the public-facing API is shaped for the end user. So it’s not really a like-for-like comparison; the difference is mostly one of scope and philosophy.

The core mentality behind InputForge is a stable heap profile and minimal GC pressure. That’s the whole reason we go through direct method invocation on raw C# delegates instead of routing through Godot signals — the aim is to keep the per-frame input path free of heap allocations, passing values around as structs that live on the stack rather than generating garbage the collector has to chase. So it’s a lean, allocation-conscious core rather than a batteries-included framework.