Claude-skill-registry godot-modernize-input

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/godot-modernize-input" ~/.claude/skills/majiayu000-claude-skill-registry-godot-modernize-input && rm -rf "$T"
manifest: skills/data/godot-modernize-input/SKILL.md
source content

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:

# 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:

# 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):

[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:

# 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:

# 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.):

# 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:

# 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:

if event is InputEventKey and event.scancode == KEY_SPACE:
    jump()

Modern Godot 4.x:

if event.is_action_pressed("jump"):
    jump()

Device Support Matrix

Input TypeDesktopMobileConsoleImplementation
KeyboardInput Map
Mouse⚠️ TouchInput Map + Touch emulation
TouchTouch events + Virtual joystick
Gamepad✅ (MFi)Input Map with deadzone
Gyroscope❌ (PS only)MotionController singleton
RumbleInputRumbler 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

# 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

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

# 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

# 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

# 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"