System Architecture & Deep Dive
This document is for anyone who wants to know what goes on under the hood of the Godot Advanced Input System (GAIS). If you just want to get your game up and running, you can safely skip this and check the Getting Started Guide.
How Input Flows Through GAIS
Think of GAIS as a simple pipeline. When you press a key on your keyboard or push a joystick on your controller, the input travels through 5 clear stages before reaching your player:
[ Physical Button / Stick / Key ]
│
▼
1. InputManager (Autoload)
- Catches raw inputs first
- Keeps track of buttons being held down
- Routes inputs to the right device ID
│
▼
2. PlayerController (Device)
- Runs unblockable profiles (Pause / Dev Console)
- Runs your active profile stack (Gameplay / Menus)
│
▼
3. InputProfile (Context)
- Turns raw buttons into Vector2 values
- Applies key-level modifiers (WASD math)
- Combines multiple inputs into one vector
│
▼
4. InputAction (The Intent)
- Applies final modifiers (like Normalize)
- Converts to Bool, Float, or Vector2
- Runs the state machine (STARTED -> PROCESSING -> ENDED)
│
┌───────┴───────┐
│ (Direct) │ (Optional Hijack)
▼ ▼
[ Your Game ] 5. InputProxy
- Adds Holds, Combos, or Chords
- Adds PENDING and CANCELLED states
│
▼
[ Your Game ]
1. InputManager (The Central Hub)
The InputManager is an autoload script that sits at the very start of the input pipeline.
Why Not Just Use Normal Godot Events?
Normally, Godot only tells you when an input changes (you press a key down, or let it go). If you hold a key down for 5 seconds without moving it, Godot doesn’t keep sending events every frame.
Because GAIS needs to know when something is continuously held down (to run the PROCESSING state and track hold timers), InputManager steps in:
- It grabs raw events as soon as they arrive.
- Its
_process()runs with a low priority number (-1000), ensuring it updates before the rest of your game scripts run. - If a button is held down, it keeps that input alive every frame until the release event finally arrives.
Device Routing
PlayerController objects are created automatically on demand. If you ask for controller 0, it creates it. If an input comes from a device nobody is listening to, it gets ignored.
- Default Mode
MERGED:ID 0: Shared by Keyboard, Mouse, Touchscreen, and your first connected controller.ID 1+: Your second controller, third controller, etc.
SPLIT_KEYBOARDMode:ID 0: Only Keyboard and Mouse.ID 1+: The Touchscreen and all controllers shift forward by one slot (your first controller is nowID 1).
SPLIT_ALLMode:ID 0: Only Keyboard and Mouse.ID 1: The TouchscreenID 2+: All controllers shift forward by two slot (your first controller is nowID 2).
2. PlayerController (Handling Devices and Menus)
A PlayerController represents a single input device (like Keyboard 1 or Gamepad 1). It takes the events from the InputManager and pushes them through two sets of profiles:
1. Absolute Profiles (Unstoppable)
These profiles are checked first. They ignore all blocking rules and cannot consume events.
- Why use this? For things that must always work no matter what menu is open—like pressing
Escfor the Pause Menu or~for the Dev Console.
2. The Profile Stack (Last In, First Out)
Next, it runs the normal profile stack in reverse order—the most recently added profile gets checked first.
- Action Consumption: If an action inside a profile has
consume_input = true, that physical key event is swallowed immediately. Actions lower down the list won’t see it. - Profile Blocking: If a profile has its
block_propagationbox checked, it stops the whole chain right there. Profiles below it on the stack won’t receive any inputs. This is how opening an inventory screen stops your character from walking around in the background.
3. InputProfile (Mapping Keys to Actions)
An InputProfile is your binding spreadsheet. It maps physical buttons and sticks to logical actions.
- Everything Starts as a Vector2: Whenever a button or key is pressed, GAIS treats it as a
Vector2:- Normal buttons/keys become
(1.0, 0.0)when pressed, and(0.0, 0.0)when released. - 1D triggers and sliders place their value on the X-axis:
(value, 0.0). - Joysticks and mouse deltas keep their full
(x, y).
- Normal buttons/keys become
- Key Modifiers: The vector passes through any modifiers attached to that specific key (like
SwitchAxisorNegate). - Combining Keys: The modified vectors for all keys assigned to the same action are added together.
- Sent to Action: That combined vector is handed over to the
InputAction.
4. InputAction (The State Machine)
An InputAction is the action itself (like “Jump” or “Move”). It takes the combined vector from the profile, applies any final action-level modifiers (like Normalize), and casts it to the format your game wants:
BOOLEAN: Looks at the X-axis. If it is not zero, it returnstrue.FLOAT: Grabs the X-axis value directly.VECTOR2: Forwards the full 2D vector as-is.
How the State Machine Works
Every frame, the action checks its final vector:
STARTED: The exact frame the input stops being zero. Fires once.PROCESSING: Fires every frame the input is held down.ENDED: The exact frame the input drops back to zero. Fires once.INTERRUPTED: Fires if the input was cut short unexpectedly (such as Alt-Tabbing or a menu suddenly swallowing the input).
Every time a state fires, it sends an InputContext containing the value, how long the button was held, and the original Godot event.
5. InputModifiers (Math Helpers)
An InputModifier is a tiny resource with a single job: take a Vector2, change its numbers, and return it.
Because they are modular resources, you can stack them anywhere:
SwitchAxis: Swaps X and Y:(x, y) -> (y, x).Negate: Flips the signs:(x, y) -> (-x, -y).Deadzone: Ignores tiny joystick drift near the center.Clamp: Keeps values within a set minimum and maximum.Accumulate: Measures the total length of the vector and puts that number on the X-axis.Normalize: Keeps diagonal movement from being faster than straight movement.Scale: To increase the magnitude/length of the vector to a new value.
6. InputProxies (State Hijackers & Interceptors)
An InputActionProxy is a specialized Resource that sits between an InputAction and your gameplay code. Instead of your player script connecting directly to an InputAction, the proxy intercepts the action’s signal, inspects how and when the player interacts with it, and emits its own modified InputContext through proxy_action.
┌───────────────────────────┐
│ InputAction (or Actions) │
└─────────────┬─────────────┘
│ action.connect()
▼
┌───────────────────────────┐
│ InputActionProxy │
│ - Intercepts Context │
│ - Evaluates Conditions │
│ - Emits Custom States │
└─────────────┬─────────────┘
│ proxy_action.emit()
▼
┌───────────────────────────┐
│ Gameplay Script (Player) │
└───────────────────────────┘
Execution Pipelines: Signal-Driven vs. Frame-Evaluated
How a proxy processes input depends directly on whether it can ride the underlying action’s active signals or needs to tick independently during periods of silence:
1. Instant & Signal-Driven (SingleActionProxy)
Because a standard InputAction already tracks hold time and drives its own continuous states (STARTED $\to$ PROCESSING $\to$ ENDED), most single-action behaviors do not need a separate frame loop.
- Proxies like Hold, Charge, or Pulse connect directly to their target action and evaluate conditions inside
_input_action_fired()using the action’s existingelapsed_durationand states. - Modifications happen on the spot with zero latency and zero polling overhead.
2. Two-Stage Frame Evaluation (MultiActionProxy & SingleActionProcessingProxy)
A proxy requires its own tick loop (_evaluate_proxy(delta)) hooked into InputManager.inputs_processed under two specific circumstances:
- Evaluating Across Empty Gaps (
SingleActionProcessingProxy):
Used when a proxy must stay active and keep ticking after an action has completely stopped. A prime example isMultiTapProxy: after the first tap ends, the targetInputActionreturns toNONEand stops emitting. The proxy must tick independently across those empty in-between frames to measure the delay before the next tap arrives or time out intoCANCELLED. - Synchronizing Multiple Actions (
MultiActionProxy):
Used for multi-key coordination likeConcurrentProxy(chords) orSequenceProxy(combos), where several distinct actions must be cached, synchronized, and resolved together at the end of the frame step.
Interruption Safety (_handle_interruption_reroute)
All proxies automatically monitor incoming actions for the INTERRUPTED state (triggered by losing window focus, Alt-Tabbing, or a higher-priority menu swallowing inputs). If an active action gets cut short, the proxy catches it, fires its own INTERRUPTED signal via _fire_interrupt(), resets its internal tracking values, and cleanly reverts to NONE.
The Three Base Proxy Classes
GAIS provides three abstract base classes depending on the scope of your input logic:
SingleActionProxy: Directly intercepts one action (proxy_of) via signals without tick overhead. Used for state modifications, instantaneous conversions, and actions whose conditions resolve while the key is active (such asHoldProxyorPulseProxy).SingleActionProcessingProxy: Intercepts one action (proxy_of) but hooks intoInputManager.inputs_processedfor per-frame_evaluate_proxy(delta)updates. Specifically designed for mechanics likeMultiTapProxythat must track timeouts across cycles of inactive/ended states.MultiActionProxy: Intercepts an array of actions (proxy_of_actions) and receives per-frame delta ticks. Designed for multi-button coordination like chords (ConcurrentProxy) or sequential directional combos.
Modifying Multi-Action Lists: When configuring a
MultiActionProxyvia code, never modifyproxy_of_actionsdirectly. Always useadd_action(action)andremove_action(action)so the proxy can safely connect signals and manage cached action arrays.
The Expanded 6-State Lifecycle & Bitmasking
Standard InputAction resources only use three exclusive states: STARTED, PROCESSING, and ENDED.
Proxies expand this by injecting intermediate tracking and failure states, allowing gameplay code to react before an input finishes:
DETECTED: The very first frame any monitored action begins. Useful for triggering instant cues (like sound effects or wind-up animations).PENDING: The condition is in progress but not yet satisfied (e.g., waiting for the next tap in a double-tap window, or waiting for a chord to complete).CANCELLED: The player let go too early or timed out before meeting the activation condition.STARTED: The activation conditions were fully met. Fires once.PROCESSING: All required actions continue to be held down after triggering.ENDED: The input was cleanly released after reachingSTARTED/PROCESSING.
Why You Must Use has_state()
Because GAIS enforces a strict one dispatch per frame rule, proxies cannot batch separate signal dispatches. If a proxy needs to communicate multiple lifecycle flags on the same frame, it combines them via bitmasking.
Because of this, direct equality checks (context.execution_state == InputContext.STARTED) can fail when other bit flags are active. Always use context.has_state(...) when listening to an InputActionProxy:
func _on_proxy_action(context: InputContext) -> void:
if context.has_state(InputContext.STARTED):
player.perform_action()
elif context.has_state(InputContext.PENDING):
ui_bar.set_progress(context.elapsed_duration)
elif context.has_state(InputContext.CANCELLED):
player.reset_stance()
Creating Custom Proxies
You can easily build custom proxy behaviors by extending SingleActionProxy, SingleActionProcessingProxy, or MultiActionProxy. Pre-built templates showing all overrideable functions are located in:
addons/GodotAdvancedInputSystem/InputActionProxy/Templates/
Simply override _ready(), _input_action_fired(), _evaluate_proxy(), or _reset_context() to implement custom timing, tap thresholds, or complex multi-key conditions.
7. Registries (Runtime Rebinding & Dynamic Icons)
Building player-facing settings menus typically introduces two headaches: converting raw engine input events into serializable data, and updating UI prompts so an Xbox button doesn’t show up when using a PlayStation controller or keyboard. GAIS handles both through dedicated registry utilities.
InputProviderRegistry (The Rebinding Engine)
InputProviderRegistry is an engine utility class instantiated via InputProviderRegistry.new(). Its primary role is to serve as a bi-directional translation bridge between Godot’s transient InputEvent objects and GAIS’s serializable InputMetadata resources.
[ Physical Keypress / Controller Input ]
│
▼
Godot InputEvent
(InputEventKey, Joypad, etc.)
│
▼
InputProviderRegistry.new()
│
▼
GAIS InputMetadata
(Serializable, Profile-Ready Data)
Why Not Store Raw InputEvents?
Native Godot InputEvent objects carry instance-specific overhead and platform quirks, making them clunky to save to disk or swap dynamically inside an InputProfile.
InputMetadata strips out that noise, storing only the pure identity of the hardware input (key code, joypad button index, motion axis, etc.).
How It’s Used:
When building a custom “Press any key to rebind” menu:
- Wait for the player’s next raw event in
_unhandled_input(event). - Pass that
InputEventto the registry to generate cleanInputMetadata. - Swap or update the hardware row in the active
InputProfilewith that metadata—no manual engine-level input map rewriting required.
For any confusion, you can also checkout the demo and see how it was structured for that setup.
InputIconRegistry (Context-Aware UI Glyphs)
InputIconRegistry is a custom Godot Resource that pairs InputMetadata keys with visual UI assets (icons and button prompt glyphs).
Instead of hardcoding separate UI paths for every input device, you configure an InputIconRegistry asset in your project, map your controller prompt textures or sprite sheets, and query it dynamically.
Sprite-Sheet Sampling & Resolution
- Automatic Coordinate Slicing: The registry supports uniform sprite sheets. By configuring cell size and layout rules, it cuts out the exact button glyph without requiring dozens of individual image files.
- Fallback Display: If an icon texture isn’t assigned for an input (such as rare keyboard keys), it gracefully falls back to a formatted text label (e.g.,
"Space","Shift","F12").
Dynamic UI Updating:
@export var icon_registry: InputIconRegistry
func update_prompt(action_metadata: InputMetadata) -> void:
# Fetches either a Texture2D or a formatted fallback String
var display_item = icon_registry.get_display_item(action_metadata)
if display_item is Texture2D:
prompt_texture_rect.texture = display_item
prompt_label.visible = false
else:
prompt_label.text = str(display_item)
prompt_texture_rect.visible = false
Whenever the active controller changes (e.g., the player picks up a DualSense instead of an Xbox controller), passing the metadata back through get_display_item() instantly updates your UI with the correct platform icon.