Use when handling input in Godot 4.x projects and need to modernize patterns. Generates Input Map configurations from hardcoded key checks, sets up joypad rumble (Godot 4.x feature), implements gyroscope/accelerometer input on mobile, creates context-sensitive input handling for different game states, and adds input buffering for responsive gameplay. Converts old InputEvent patterns to modern Godot 4.x best practices.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: godot-modernize-input
version: 2.0.0
displayName: Modernize Input Handling
description: >
Use when handling input in Godot 4.x projects and need to modernize patterns.
Generates Input Map configurations from hardcoded key checks, sets up joypad
rumble (Godot 4.x feature), implements gyroscope/accelerometer input on mobile,
creates context-sensitive input handling for different game states, and adds
input buffering for responsive gameplay. Converts old InputEvent patterns to
modern Godot 4.x best practices.
author: Asreonn
license: MIT
category: game-development
type: tool
difficulty: intermediate
audience: [developers]
keywords:
- godot
- input-handling
- input-map
- joypad
- rumble
- haptic
- gyroscope
- accelerometer
- mobile-input
- input-buffering
- gdscript
- gamepad
- controller
platforms: [macos, linux, windows, android, ios]
repository: https://github.com/asreonn/godot-superpowers
homepage: https://github.com/asreonn/godot-superpowers#readme
permissions:
filesystem:
read: [".gd", ".tscn", ".tres", "project.godot"]
write: [".gd", ".tscn", "project.godot"]
git: true
behavior:
auto_rollback: true
validation: true
git_commits: true
outputs: "Modernized input scripts, Input Map configurations, device support scripts, input buffering systems"
requirements: "Git repository, Godot 4.x, input handling code to modernize"
execution: "Semi-automatic with detection prompts and git commits"
integration: "Works with godot-modernize-gdscript for complete codebase updates"
---
# Modernize Input Handling
## Core Principle
**Input handling should be device-agnostic, responsive, and context-aware.** Hardcoded key checks, missing device support, and unbuffered input create poor player experiences across platforms.
## What This Skill Does
### 1. Input Map Generation from Code
Converts hardcoded key detection to Input Map actions:
**Before:**
```gdscript
# Hardcoded key checks - NOT recommended
func _input(event):
if event is InputEventKey:
if event.keycode == KEY_SPACE:
jump()
if event.keycode == KEY_ESCAPE:
pause_game()
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
shoot()
```
**After:**
```gdscript
# Modern Input Map usage
func _input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
jump()
if event.is_action_pressed("pause"):
pause_game()
if event.is_action_pressed("shoot"):
shoot()
```
**Generated Input Map (project.godot):**
```ini
[input]
jump={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
]
}
shoot={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null)
]
}
```
### 2. Joypad Rumble Setup (Godot 4.x)
Implements haptic feedback for controllers:
```gdscript
# InputRumbler.gd - Autoload singleton
extends Node
const WEAK_RUMBLE: float = 0.3
const MEDIUM_RUMBLE: float = 0.6
const STRONG_RUMBLE: float = 1.0
const SHORT_DURATION: float = 0.1
const MEDIUM_DURATION: float = 0.3
const LONG_DURATION: float = 0.8
var current_device: int = 0
func _ready() -> void:
Input.joy_connection_changed.connect(_on_joy_connection_changed)
func _on_joy_connection_changed(device: int, connected: bool) -> void:
if connected:
current_device = device
print("Controller connected: ", Input.get_joy_name(device))
func rumble(weak_magnitude: float, strong_magnitude: float, duration: float) -> void:
if Input.get_joy_name(current_device).is_empty():
return
Input.start_joy_vibration(current_device, weak_magnitude, strong_magnitude, duration)
func rumble_impact() -> void:
rumble(WEAK_RUMBLE, STRONG_RUMBLE, SHORT_DURATION)
func rumble_damage() -> void:
rumble(MEDIUM_RUMBLE, MEDIUM_RUMBLE, MEDIUM_DURATION)
func rumble_explosion() -> void:
rumble(STRONG_RUMBLE, STRONG_RUMBLE, LONG_DURATION)
func rumble_heartbeat() -> void:
rumble(WEAK_RUMBLE, 0.0, 0.5)
await get_tree().create_timer(0.6).timeout
rumble(WEAK_RUMBLE, 0.0, 0.5)
```
### 3. Gyroscope/Accelerometer Input (Mobile)
Implements motion controls for mobile devices:
```gdscript
# MotionController.gd
extends Node
@export var tilt_sensitivity: float = 2.0
@export var shake_threshold: float = 15.0
signal tilt_detected(direction: Vector2)
signal shake_detected
var accelerometer_enabled: bool = false
var gyroscope_enabled: bool = false
func _ready() -> void:
# Check if running on mobile
if OS.has_feature("mobile") or OS.has_feature("android") or OS.has_feature("ios"):
enable_motion_sensors()
func enable_motion_sensors() -> void:
if DisplayServer.has_feature(DisplayServer.FEATURE_TOUCHSCREEN):
# Enable accelerometer (gravity + user acceleration)
if DisplayServer.has_feature(DisplayServer.FEATURE_SENSOR_ACCELEROMETER):
DisplayServer.accelerometer_set_mode(DisplayServer.ACCELEROMETER_MODE_COMBINED)
accelerometer_enabled = true
# Enable gyroscope for rotation rate
if DisplayServer.has_feature(DisplayServer.FEATURE_SENSOR_GYROSCOPE):
gyroscope_enabled = true
func _process(delta: float) -> void:
if not (accelerometer_enabled or gyroscope_enabled):
return
# Handle tilt-based movement
if accelerometer_enabled:
var accel: Vector3 = Input.get_accelerometer()
var tilt: Vector2 = Vector2(accel.x, -accel.y) * tilt_sensitivity
if tilt.length() > 0.1:
tilt_detected.emit(tilt)
# Handle shake detection
if gyroscope_enabled:
var gyro: Vector3 = Input.get_gyroscope()
if gyro.length() > shake_threshold:
shake_detected.emit()
func get_tilt_direction() -> Vector2:
if not accelerometer_enabled:
return Vector2.ZERO
var accel: Vector3 = Input.get_accelerometer()
# Normalize for device orientation
return Vector2(accel.x, -accel.y).normalized()
func calibrate_center() -> void:
# Store current orientation as neutral position
var current_accel: Vector3 = Input.get_accelerometer()
# Implementation: Store offset and apply to future readings
```
### 4. Context-Sensitive Input Handling
Manages different input contexts (gameplay, menu, dialogue, etc.):
```gdscript
# InputContextManager.gd - Autoload singleton
extends Node
enum Context {
GAMEPLAY,
MENU,
DIALOGUE,
PAUSED,
INVENTORY
}
var current_context: Context = Context.GAMEPLAY
var context_stack: Array[Context] = []
# Context-specific action mappings
var context_actions: Dictionary = {
Context.GAMEPLAY: ["move_left", "move_right", "jump", "shoot", "interact"],
Context.MENU: ["ui_up", "ui_down", "ui_accept", "ui_cancel", "ui_left", "ui_right"],
Context.DIALOGUE: ["ui_accept", "ui_cancel", "skip_dialogue"],
Context.PAUSED: ["ui_accept", "ui_cancel", "resume"],
Context.INVENTORY: ["inventory_navigate", "inventory_use", "inventory_close"]
}
func set_context(new_context: Context) -> void:
context_stack.push_back(current_context)
current_context = new_context
_update_input_processing()
print("Input context changed to: ", Context.keys()[new_context])
func restore_previous_context() -> void:
if context_stack.size() > 0:
current_context = context_stack.pop_back()
_update_input_processing()
func reset_to_gameplay() -> void:
context_stack.clear()
current_context = Context.GAMEPLAY
_update_input_processing()
func _update_input_processing() -> void:
# Enable/disable action processing based on context
var enabled_actions: Array = context_actions.get(current_context, [])
# This works with built-in UI actions too
match current_context:
Context.GAMEPLAY:
get_tree().paused = false
Context.PAUSED:
get_tree().paused = true
Context.MENU, Context.INVENTORY:
get_viewport().set_input_as_handled()
func is_action_allowed(action: StringName) -> bool:
var enabled_actions: Array = context_actions.get(current_context, [])
return action in enabled_actions or action.begins_with("ui_")
# Example usage in player controller
func _input(event: InputEvent) -> void:
if not InputContextManager.is_action_allowed("jump"):
return
if event.is_action_pressed("jump"):
jump()
```
### 5. Input Buffering
Adds input buffering for responsive gameplay:
```gdscript
# InputBuffer.gd - Component for responsive input
extends Node
@export var buffer_duration: float = 0.15 # 150ms buffer window
var buffered_actions: Dictionary = {}
func _ready() -> void:
# Initialize buffer for common actions
buffered_actions = {
"jump": 0.0,
"shoot": 0.0,
"dash": 0.0,
"interact": 0.0
}
func _process(delta: float) -> void:
# Decay buffer timers
for action in buffered_actions.keys():
if buffered_actions[action] > 0:
buffered_actions[action] -= delta
func _input(event: InputEvent) -> void:
for action in buffered_actions.keys():
if event.is_action_pressed(action):
buffer_action(action)
func buffer_action(action: StringName) -> void:
if action in buffered_actions:
buffered_actions[action] = buffer_duration
func is_buffered(action: StringName) -> bool:
return action in buffered_actions and buffered_actions[action] > 0
func consume_buffer(action: StringName) -> bool:
if is_buffered(action):
buffered_actions[action] = 0.0
return true
return false
# Example usage in player controller
func _physics_process(delta: float) -> void:
# Check buffered jump (allows jump before hitting ground)
if input_buffer.is_buffered("jump") and is_on_floor():
input_buffer.consume_buffer("jump")
velocity.y = jump_velocity
```
## Detection Patterns
### Hardcoded Input Detection
Scans for:
- `InputEventKey` with specific `keycode` values
- `InputEventMouseButton` with specific `button_index` values
- `Input.is_key_pressed()` calls
- Direct joypad button checks without action mapping
### Legacy Patterns to Modernize
**Old Godot 3.x Pattern:**
```gdscript
if event is InputEventKey and event.scancode == KEY_SPACE:
jump()
```
**Modern Godot 4.x:**
```gdscript
if event.is_action_pressed("jump"):
jump()
```
## Device Support Matrix
| Input Type | Desktop | Mobile | Console | Implementation |
|------------|---------|--------|---------|----------------|
| Keyboard | ✅ | ❌ | ❌ | Input Map |
| Mouse | ✅ | ⚠️ Touch | ❌ | Input Map + Touch emulation |
| Touch | ❌ | ✅ | ❌ | Touch events + Virtual joystick |
| Gamepad | ✅ | ✅ (MFi) | ✅ | Input Map with deadzone |
| Gyroscope | ❌ | ✅ | ❌ (PS only) | MotionController singleton |
| Rumble | ✅ | ❌ | ✅ | InputRumbler singleton |
## When to Use
### You're Adding Controller Support
Your game only supports keyboard/mouse and needs gamepad compatibility.
### You're Porting to Mobile
Need to add touch controls, motion sensors, and adapt input for mobile devices.
### Input Feels Unresponsive
Players complain about missed inputs; need buffering for precise timing.
### Context Conflicts Exist
Menu navigation interferes with gameplay; need context-sensitive handling.
## Modern Input Best Practices
### 1. Always Use Input Map Actions
Never hardcode key checks in gameplay code. Use actions for:
- Remapping support
- Localization (different keyboard layouts)
- Accessibility (adaptive controllers)
- Multiple device types
### 2. Implement Deadzones
```gdscript
# In Input Map configuration (project.godot)
move_left={
"deadzone": 0.2,
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null)
]
}
```
### 3. Support Multiple Input Types Simultaneously
```gdscript
func _input(event: InputEvent) -> void:
# Handle keyboard/controller
if event.is_action_pressed("jump"):
jump()
# Handle touch (mobile)
if event is InputEventScreenTouch and event.pressed:
if _is_touch_in_jump_zone(event.position):
jump()
```
### 4. Use InputEvent for One-Shots, _process for Continuous
```gdscript
# One-shot actions (button presses)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("shoot"):
shoot()
# Continuous actions (held buttons)
func _process(delta: float) -> void:
var move_direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity.x = move_direction.x * speed
```
## Integration
Works with:
- **godot-modernize-gdscript** - Converts legacy GDScript patterns
- **godot-setup-multiplayer** - Synchronizes input across network
- **godot-extract-to-scenes** - Extracts input components as reusable scenes
## Safety
- Git commit per input modernization step
- Detection review before applying changes
- Rollback capability for each transformation
- Preserves manual Input Map entries
## When NOT to Use
Don't modernize if:
- Input handling is already using Input Map actions properly
- You're implementing platform-specific input (raw access needed)
- Legacy input is intentional for compatibility reasons
## Examples
### Complete Mobile Setup
```gdscript
# MobileInputHandler.gd
extends Node
@onready var virtual_joystick: VirtualJoystick = $VirtualJoystick
@onready var touch_buttons: Control = $TouchButtons
func _ready() -> void:
# Only show touch UI on mobile
var is_mobile: bool = OS.has_feature("mobile") or OS.has_feature("android") or OS.has_feature("ios")
virtual_joystick.visible = is_mobile
touch_buttons.visible = is_mobile
func get_movement_input() -> Vector2:
# Combine gamepad and touch input
var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
if virtual_joystick.visible:
input += virtual_joystick.output
return input.normalized()
```
### Input Remapping UI
```gdscript
# RemapButton.gd
extends Button
@export var action: StringName
func _ready() -> void:
pressed.connect(_on_pressed)
update_display()
func _on_pressed() -> void:
text = "Press any key..."
set_process_input(true)
func _input(event: InputEvent) -> void:
if event.is_pressed() and not event.is_echo():
# Clear existing events for this action
InputMap.action_erase_events(action)
InputMap.action_add_event(action, event)
set_process_input(false)
update_display()
func update_display() -> void:
var events: Array[InputEvent] = InputMap.action_get_events(action)
if events.size() > 0:
text = events[0].as_text()
else:
text = "Not assigned"
```
No comments yet. Be the first to comment!