
Godot Input Handling
- 275 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-input-handling for development tasks
About
godot-input-handling: A skill for development. This provides functionality for development workflows.
- godot-input-handling
Godot Input Handling by the numbers
- 275 all-time installs (skills.sh)
- +25 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,426 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-input-handlingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 275 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-input-handling for development tasks
Files
Input Handling
Handle keyboard, mouse, gamepad, and touch input with proper buffering and accessibility support.
Available Scripts
advanced_input_buffer.gd
Frame-perfect input buffering system for responsive jumps, dashes, and combo chains.
safe_runtime_rebind.gd
Dynamic input rebinding with conflict detection, persistence, and multi-device support.
analog_deadzone_manager.gd
Radial deadzone management for analog sticks to eliminate drift while maintaining natural follow-through.
multi_touch_gestures.gd
Handling touch, drags, and pinch-to-zoom gestures for mobile and touchscreen compatibility.
input_echo_filter.gd
Filtering echo events to distinguish between hold-to-navigate (UI) and one-time gameplay actions.
mouse_capture_manager.gd
Robust mouse capture and sensitivity scaling logic for FPS and mouse-intensive systems.
hold_toggle_accessibility.gd
Software-side support for user-defined 'Hold' vs 'Toggle' accessibility preferences.
glyph_prompt_manager.gd
Real-time switching between Keyboard and Gamepad UI prompts based on the last active device.
action_state_machine.gd
Tracking the lifecycle of an action ('Just Pressed', 'Held', 'Released') for complex state logic.
unhandled_input_priority.gd
Demonstrating the correct use of _unhandled_input to prevent gameplay logic from leaking into UI.
MANDATORY - For Responsive Controls: Read input_buffer.gd before implementing jump/dash mechanics.
NEVER Do in Input Handling
- NEVER poll input in `_process()` for gameplay actions — Use
_physics_process()or_unhandled_input()._process()is frame-rate dependent, causing dropped inputs at low FPS [22]. - NEVER use hardcoded key checks (e.g., `KEY_W`) — Always use
InputMapactions. Hardcoded keys prevent rebinding and break compatibility with non-QWERTY layouts [23]. - NEVER ignore analog stick deadzones — Drifting sticks at 0.05 magnitude will cause unintended movement. Implement a radial deadzone (not axial) in code or settings [24].
- NEVER assume a single input device — Players may switch between Keyboard and Controller mid-session. Use
Input.joy_connection_changedto update UI prompts dynamically [25]. - NEVER use `_input()` for gameplay actions —
_input()fires for ALL events (including UI). Use_unhandled_input()so gameplay logic doesn't trigger while clicking menus [26]. - NEVER omit input buffering in fast-paced games — If a player presses jump 50ms before landing, the input is lost without a buffer. Implement a 100-150ms buffer for a "tight" feel [27].
- NEVER use `Input.is_action_pressed()` for one-time triggers — It returns true every frame the key is held. Use
_just_pressedfor jumps, attacks, and toggles to avoid logic spam. - NEVER implement manual 'Hold vs Toggle' logic in multiple places — Centralize it in a setting or input wrapper to ensure accessibility consistency across the whole game.
- NEVER forget to handle `InputEvent.is_echo()` in UI navigation — Echo events (keyboard repeat) should move menus but rarely should they trigger "Confirm" or "Back" actions.
- NEVER capture the mouse without a 'Release' shortcut — If your game crashes or blocks
ui_cancel, the user is trapped. Always provide a fallback escape for mouse capture.
---
Input Propagation & Isolation
Godot propagates input events in a specific order. Understanding this is key to isolating UI from gameplay.
1. `_input(event)`: High-priority global intercept. Use for dev consoles or debug overlays. 2. `_gui_input(event)`: Handled by Control nodes (UI). If a UI element consumes the event (e.g., clicking a button), it calls accept_event(), stopping further propagation. 3. `_unhandled_input(event)`: Reached ONLY if no UI element consumed the event. Expert Pattern: Put all gameplay logic (jump, shoot) here to prevent accidental triggers while interacting with menus.
InputMap Best Practices
Avoid physical key checks. Define semantic actions (e.g., move_left, interact) in Project Settings > Input Map.
1. Analog Deadzones
Analog sticks suffer from drift. Use Input.get_vector() for mathematically correct circular deadzones.
- Bad: Subtracting axis strengths manually creates "square" deadzones.
- Good:
var input := Input.get_vector("left", "right", "up", "down")applies a perfectly circular deadzone and clamps magnitude to 1.0.
2. Basic Polling
# Check if action pressed this frame
if Input.is_action_just_pressed("jump"):
jump()
# Check if action held
if Input.is_action_pressed("fire"):
shoot()
# Check if action released
if Input.is_action_just_released("jump"):
release_jump()
# Get axis (-1 to 1)
var direction := Input.get_axis("move_left", "move_right")
# Get vector
var input_vector := Input.get_vector("left", "right", "up", "down")InputEvent Processing
func _input(event: InputEvent) -> void:
if event is InputEventKey:
if event.keycode == KEY_ESCAPE and event.pressed:
pause_game()
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
click_position = event.positionMulti-Modal Input & UI Glyphs
Modern games must handle simultaneous Controller and Keyboard/Mouse input smoothly.
1. Handling Input Modes
- Mouse Aiming: Process
InputEventMouseMotionin_unhandled_input()for relative movement. - Stick Movement: Poll
Input.get_vector()in_physics_process()for clamped state.
2. Dynamic Glyph Swapping
To update UI prompts (e.g., "Press E" vs "Press X") in real-time:
- Autoload Strategy: Create a singleton that monitors
_input(event). - Detection: Check
event is InputEventJoypadButtonorInputEventJoypadMotionto detect gamepad use. - Broadcasting: Emit a signal (e.g.,
signal device_changed(is_gamepad: bool)) when the hardware type shifts. All UI elements should listen to this signal to swap their prompt textures.
Expert Input Extensions
1. Input-Buffering (Action Queuing)
Decouple input presses from physics execution to make controls feel "tight." Store the input in a timed buffer and consume it when a valid state (e.g., landing) is reached [1, 2].
var jump_buffer_timer: float = 0.0
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
jump_buffer_timer = 0.15 # 150ms window
func _physics_process(delta: float) -> void:
if jump_buffer_timer > 0.0:
jump_buffer_timer -= delta
if is_on_floor():
jump()
jump_buffer_timer = 0.02. Coyote-Time (Jump Leniency)
Allow a jump for a few frames after the character leaves a ledge by tracking the time since last grounded [4].
var coyote_timer: float = 0.0
func _physics_process(delta: float) -> void:
if is_on_floor():
coyote_timer = 0.1 # 100ms grace period
else:
coyote_timer -= delta
if Input.is_action_just_pressed("jump") and coyote_timer > 0.0:
jump()
coyote_timer = 0.03. Multiplayer-Input-Synchronization
Route local device input to the authoritative server using RPCs and multiplayer.get_remote_sender_id() for validation [7, 8].
@rpc("any_peer", "call_local", "unreliable")
func sync_input(dir: Vector2) -> void:
var sender = multiplayer.get_remote_sender_id()
# Apply dir to the player body associated with sender...Reference
Expert Input Architectures
1. Input-Event-Parsing (Virtual Injection)
To simulate player input for automated testing or AI assistance, use Input.parse_input_event(). This injects raw InputEvent objects directly into the engine's processing pipeline, bypassing physical hardware. This is critical for building robust CI/CD test suites or deterministic tutorials.
class_name VirtualInputInjector extends Node
## Injects virtual hardware events into the engine pipeline.
func simulate_jump() -> void:
var event := InputEventAction.new()
event.action = &"jump"
event.pressed = true
# 1. Dispatch the "Pressed" event.
Input.parse_input_event(event)
# 2. Schedule the "Released" event.
await get_tree().create_timer(0.1).timeout
event.pressed = false
Input.parse_input_event(event)2. Input-Combo-Validation (Sequence Buffering)
Professional fighting games and action-RPGs utilize a rolling buffer to validate complex input sequences (e.g., "Down, Right, Punch"). Store semantic actions with their timestamps and check if the buffer ends with a specific pattern within a strict time window (e.g., 500ms).
class_name ComboValidator extends Node
## Validates timed input sequences for special moves.
var _input_buffer: Array[Dictionary] = []
@export var combo_timeout: float = 0.5
func add_input(action: StringName) -> void:
_input_buffer.append({"action": action, "time": Time.get_ticks_msec()})
_cleanup_buffer()
# Example: Fireball (Down, Right, Attack)
if _check_sequence(["move_down", "move_right", "attack"]):
_execute_special_move("fireball")
func _cleanup_buffer() -> void:
var now := Time.get_ticks_msec()
_input_buffer = _input_buffer.filter(func(i): return now - i.time < (combo_timeout * 1000))
func _check_sequence(sequence: Array[StringName]) -> bool:
if _input_buffer.size() < sequence.size(): return false
for i in range(sequence.size()):
var buffer_idx := _input_buffer.size() - sequence.size() + i
if _input_buffer[buffer_idx].action != sequence[i]:
return false
return true3. Input-Replay-Buffer (Deterministic Playback)
Deterministic replay is essential for debugging high-speed physics or creating "Ghost" racing data. Capture every InputEvent in _unhandled_input(), serializing the data with the current multiplayer.get_unique_id() or frame count.
class_name InputReplayBuffer extends Node
## Captures and replays deterministic input streams.
var _recorded_events: Array[Dictionary] = []
var _is_replaying: bool = false
func _unhandled_input(event: InputEvent) -> void:
if _is_replaying: return
# Capture the duplicate event and current engine frame.
_recorded_events.append({
"frame": Engine.get_frames_drawn(),
"event": event.duplicate()
})
func start_replay() -> void:
_is_replaying = true
var start_frame := Engine.get_frames_drawn()
for entry in _recorded_events:
var target_frame: int = entry.frame
while Engine.get_frames_drawn() < start_frame + target_frame:
await get_tree().process_frame
Input.parse_input_event(entry.event)Reference
Related
- Master Skill: godot-master
# action_state_machine.gd
# Tracking 'Just Pressed' vs 'Released' for complex behavior
extends Node
# PROBLEM: Input.is_action_just_pressed() only works once per frame.
# State machines need to know if an action is currently in its 'Release' phase.
var is_jumping: bool = false
var is_falling: bool = false
func _physics_process(_delta: float) -> void:
if Input.is_action_just_pressed("jump"):
is_jumping = true
_start_jump()
if Input.is_action_just_released("jump") and is_jumping:
is_jumping = false
is_falling = true
_end_jump_early() # Variable jump height logic
# advanced_input_buffer.gd
# Frame-perfect input buffering for combos and responsive feel [12, 13]
extends Node
# EXPERT NOTE: Simple buffering just checks "was jump pressed".
# Advanced buffering tracks the 'time_since_pressed' for multiple
# actions to allow priority-based execution (e.g. Dash over Jump).
var _buffer: Dictionary = {} # action_name -> timestamp
@export var buffer_window_ms: int = 150
func _input(event: InputEvent) -> void:
for action in ["jump", "dash", "attack"]:
if event.is_action_pressed(action):
_buffer[action] = Time.get_ticks_msec()
func is_action_buffered(action: String) -> bool:
if _buffer.has(action):
var delta = Time.get_ticks_msec() - _buffer[action]
if delta <= buffer_window_ms:
return true
return false
func consume_buffer(action: String) -> void:
_buffer.erase(action)
# analog_deadzone_manager.gd
# Expert radial deadzone management for analog sticks [24]
extends Node
# PROBLEM: Axial deadzones (X/Y separately) cause "cross-shaped" deadzones.
# SOLUTION: Radial deadzone (vector length) provides a circular, natural feel.
@export var deadzone: float = 0.2
func get_movement_vector() -> Vector2:
var raw = Input.get_vector("move_left", "move_right", "move_up", "move_down")
if raw.length() < deadzone:
return Vector2.ZERO
# Optional: Scaled Radial Deadzone (remaps 0.2..1.0 to 0.0..1.0)
return raw.normalized() * ((raw.length() - deadzone) / (1.0 - deadzone))
# glyph_prompt_manager.gd
# Dynamic UI prompt switching (Keyboard vs Gamepad) [25]
extends Node
signal device_changed(type: String)
enum Device { KEYBOARD_MOUSE, GAMEPAD }
var last_device: Device = Device.KEYBOARD_MOUSE
func _input(event: InputEvent) -> void:
var current: Device = last_device
if event is InputEventKey or event is InputEventMouseButton:
current = Device.KEYBOARD_MOUSE
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
current = Device.GAMEPAD
if current != last_device:
last_device = current
device_changed.emit("GamePad" if current == Device.GAMEPAD else "KBM")
# hold_toggle_accessibility.gd
# Software-side Support for 'Hold' vs 'Toggle' actions
extends Node
@export var use_toggle_sprint: bool = false
var _sprint_active: bool = false
func _physics_process(_delta: float) -> void:
if use_toggle_sprint:
if Input.is_action_just_pressed("sprint"):
_sprint_active = !_sprint_active
else:
_sprint_active = Input.is_action_pressed("sprint")
if _sprint_active:
_apply_sprint()
func _apply_sprint():
pass
# skills/input-handling/code/input_buffer_manager.gd
extends Node
## Input Buffer Manager Expert Pattern
## Implements "Input Buffering" for responsive action-game controls.
@export var buffer_window_frames: int = 10
var _buffer: Dictionary = {} # ActionName -> FrameCount
func _process(_delta: float) -> void:
# 1. Decay the Buffer
for action in _buffer.keys():
_buffer[action] -= 1
if _buffer[action] <= 0:
_buffer.erase(action)
func _unhandled_input(event: InputEvent) -> void:
# 2. Action-based Input Capture
# Professional games NEVER check for individual keys in logic.
if event is InputEventAction and event.is_pressed():
_buffer[event.action] = buffer_window_frames
func is_action_buffered(action: String) -> bool:
return _buffer.has(action)
func consume_action(action: String) -> void:
_buffer.erase(action)
## EXPERT NOTE:
## Use the 'Jump Buffering' pattern: Check 'is_action_buffered(\"jump\")'
## when the character touches the 'is_on_floor()' ground to allow
## jumping even if the button was pressed frames early.
## Combine with 'Coyote Time' for the industry-standard "Tight Controls" feel.
## For 'input-handling', implement 'Action Remapping' by saving modified
## 'InputMap' settings to a 'ConfigFile' for persistence.
# skills/input-handling/scripts/input_buffer.gd
extends Node
## Input Buffer Expert Pattern
## Buffers inputs for responsive controls - press jump 100ms before landing? Still registers.
class_name InputBuffer
var _buffer: Dictionary = {} # action_name → buffer time remaining
@export var buffer_duration: float = 0.15 # 150ms
func _process(delta: float) -> void:
# Decay all buffered inputs
for action in _buffer.keys():
_buffer[action] -= delta
if _buffer[action] <= 0:
_buffer.erase(action)
func buffer_action(action_name: String) -> void:
_buffer[action_name] = buffer_duration
func is_action_buffered(action_name: String) -> bool:
return action_name in _buffer
func consume_action(action_name: String) -> bool:
if action_name in _buffer:
_buffer.erase(action_name)
return true
return false
## EXPERT USAGE:
## In _unhandled_input():
## if Input.is_action_just_pressed("jump"):
## input_buffer.buffer_action("jump")
##
## In _physics_process():
## if is_on_floor() and input_buffer.consume_action("jump"):
## velocity.y = JUMP_VELOCITY
# input_echo_filter.gd
# Filtering echo events for UI navigation vs Gameplay
extends Control
# EXPERT NOTE: InputEvent.is_echo() is true for auto-repeated keys.
# Never trigger gameplay actions on echo, but always allow UI movement.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
if event.is_echo():
# Ignore hold-to-confirm if that's not desired
return
_do_confirm()
func _do_confirm():
print("Confirmed!")
# skills/input-handling/scripts/input_remapper.gd
extends Node
## Input Remapper Expert Pattern
## Runtime input rebinding with conflict detection and persistence.
class_name InputRemapper
const CONFIG_PATH := "user://input_bindings.cfg"
func rebind_action(action_name: String, new_event: InputEvent) -> bool:
# Check for conflicts
for existing_action in InputMap.get_actions():
if existing_action == action_name:
continue
for event in InputMap.action_get_events(existing_action):
if _events_match(event, new_event):
push_warning("Input conflict: %s already bound to %s" % [new_event, existing_action])
return false
# Clear existing binding
InputMap.action_erase_events(action_name)
# Add new binding
InputMap.action_add_event(action_name, new_event)
return true
func save_bindings() -> void:
var config := ConfigFile.new()
for action in InputMap.get_actions():
var events := InputMap.action_get_events(action)
if events.size() > 0:
config.set_value("bindings", action, _serialize_events(events))
config.save(CONFIG_PATH)
func load_bindings() -> void:
var config := ConfigFile.new()
if config.load(CONFIG_PATH) != OK:
return
for action in config.get_section_keys("bindings"):
var event_data = config.get_value("bindings", action)
var events := _deserialize_events(event_data)
InputMap.action_erase_events(action)
for event in events:
InputMap.action_add_event(action, event)
func _events_match(event_a: InputEvent, event_b: InputEvent) -> bool:
if event_a.get_class() != event_b.get_class():
return false
if event_a is InputEventKey:
return event_a.keycode == (event_b as InputEventKey).keycode
elif event_a is InputEventMouseButton:
return event_a.button_index == (event_b as InputEventMouseButton).button_index
elif event_a is InputEventJoypadButton:
return event_a.button_index == (event_b as InputEventJoypadButton).button_index
return false
func _serialize_events(events: Array) -> Array:
var result := []
for event in events:
result.append(var_to_str(event))
return result
func _deserialize_events(data: Array) -> Array:
var result := []
for event_str in data:
result.append(str_to_var(event_str))
return result
## EXPERT USAGE:
## InputRemapper.load_bindings() # In autoload _ready()
## InputRemapper.rebind_action("jump", event)
## InputRemapper.save_bindings()
# mouse_capture_manager.gd
# Handling mouse capture and sensitivity scaling for FPS games
extends Node3D
@export var sensitivity: float = 0.002
var _captured: bool = false
func _ready() -> void:
_toggle_capture(true)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
_toggle_capture(!_captured)
if _captured and event is InputEventMouseMotion:
# Expert: Apply sensitivity scaling here
var rot = -event.relative * sensitivity
_apply_rotation(rot)
func _toggle_capture(enable: bool) -> void:
_captured = enable
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if enable else Input.MOUSE_MODE_VISIBLE
func _apply_rotation(_rot: Vector2):
pass
# multi_touch_gestures.gd
# Handling touch, drags, and pinch-to-zoom gestures
extends Node2D
var _touches: Dictionary = {}
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed:
_touches[event.index] = event.position
else:
_touches.erase(event.index)
if event is InputEventScreenDrag:
_touches[event.index] = event.position
if _touches.size() == 2:
_handle_pinch()
func _handle_pinch() -> void:
# Logic for calculating distance between touch 0 and 1
pass
# safe_runtime_rebind.gd
# Safe runtime input rebinding with multi-device support [15, 16]
extends Node
# EXPERT NOTE: Always check for conflicts before applying a rebind.
# Also, handle the case where a player binds a Joypad button to a keyboard action.
func rebind_action(action_name: String, new_event: InputEvent) -> bool:
# 1. Check for conflicts
for action in InputMap.get_actions():
if action == action_name: continue
if InputMap.action_has_event(action, new_event):
printerr("Conflict: ", new_event.as_text(), " already bound to ", action)
return false
# 2. Apply rebind
InputMap.action_erase_events(action_name)
InputMap.action_add_event(action_name, new_event)
# 3. Persistence (Save to ConfigFile)
_save_rebinds()
return true
func _save_rebinds():
# Standard pattern: save to user://input.cfg
pass
# unhandled_input_priority.gd
# Expert use of _unhandled_input across the tree [26]
extends Node2D
# 1. _input() -> Global (always)
# 2. Control._gui_input() -> UI only
# 3. _unhandled_input() -> Gameplay (only if UI didn't use it)
# 4. _unhandled_key_input() -> Shortcuts
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
# This interaction only triggers if the player didn't
# just click a 'Use' button on a menu!
_check_interaction()
func _check_interaction():
pass