Input

January 13, 2026 ยท View on GitHub

GMR provides both raw input polling and an action mapping system. Action mapping is recommended for most games.

Action Mapping

Map logical actions to physical inputs. This decouples game logic from specific keys and makes rebinding easier.

Block DSL

input do |i|
  i.move_left [:a, :left]
  i.move_right [:d, :right]
  i.jump [:space, :up, :w]
  i.attack :z, mouse: :left    # Keyboard + mouse binding
  i.pause :escape
end

Method Chaining

Input.map(:move_left, [:left, :a])
     .map(:move_right, [:right, :d])
     .map(:jump, [:space, :up, :w])

Both syntaxes are equivalent. Use whichever you prefer.

Querying Actions

def update(dt)
  # Check if action is currently held
  @player.x -= 200 * dt if Input.action_down?(:move_left)
  @player.x += 200 * dt if Input.action_down?(:move_right)

  # Check if action was just pressed this frame
  @player.jump if Input.action_pressed?(:jump)

  # Check if action was just released this frame
  @player.stop_charging if Input.action_released?(:attack)
end
MethodReturns true when
action_down?(:name)Action is currently held
action_pressed?(:name)Action was pressed this frame
action_released?(:name)Action was released this frame

Action Callbacks

Register callbacks for discrete events:

Input.on(:pause) { toggle_pause }
Input.on(:jump) { @player.jump }
Input.on(:attack, when: :pressed) { @player.start_attack }
Input.on(:attack, when: :released) { @player.release_attack }

The when parameter defaults to :pressed. Options are :pressed and :released.

Input Contexts

Switch between different control schemes for gameplay vs menus:

# Define a context for menu navigation
input_context :menu do |i|
  i.confirm :enter
  i.cancel :escape
  i.nav_up :up
  i.nav_down :down
end

# Define default gameplay context
input do |i|
  i.move_left [:a, :left]
  i.move_right [:d, :right]
  i.jump [:space, :w]
  i.pause :escape
end

Switching Contexts

def open_pause_menu
  Input.push_context(:menu)
  SceneManager.push(PauseMenu.new)
end

def close_pause_menu
  Input.pop_context
  SceneManager.pop
end

Context management is stack-based. Push when entering a new mode, pop when leaving.

Raw Input

When you need direct access to specific keys or mouse buttons.

Keyboard

# Single key
Input.key_down?(:space)           # Currently held
Input.key_pressed?(:enter)        # Just pressed this frame
Input.key_released?(:shift)       # Just released this frame

# Any of multiple keys
Input.key_down?([:a, :left])      # True if any key is down

Available Key Symbols

Letters: :a through :z

Numbers: :zero through :nine (or :num_0 through :num_9)

Arrows: :up, :down, :left, :right

Modifiers: :shift, :ctrl, :alt

Special: :space, :enter, :escape, :tab, :backspace

Function keys: :f1 through :f12

Mouse

# Position (virtual resolution aware)
x = Input.mouse_x
y = Input.mouse_y

# Buttons: :left, :right, :middle
Input.mouse_down?(:left)
Input.mouse_pressed?(:right)
Input.mouse_released?(:middle)

# Scroll wheel (float, positive = up)
wheel = Input.mouse_wheel

Mouse position is automatically adjusted for virtual resolution. If your game runs at 960x540 but the window is larger, mouse_x and mouse_y return coordinates in game space.

Text Input

For text fields and name entry:

def update(dt)
  # Get the Unicode code point of the last character pressed
  char_code = Input.char_pressed
  if char_code > 0
    @text += char_code.chr
  end

  # Handle backspace separately
  if Input.key_pressed?(:backspace) && @text.length > 0
    @text = @text[0..-2]
  end
end

Combining Inputs

Multiple Keys for One Action

Pass an array to bind multiple keys:

input do |i|
  i.jump [:space, :w, :up]  # Any of these triggers jump
end

Keyboard + Mouse

Use the mouse: parameter:

input do |i|
  i.attack :z, mouse: :left       # Z key or left mouse button
  i.secondary :x, mouse: :right   # X key or right mouse button
end

Keyboard + Gamepad

Use the gamepad: parameter to bind controller buttons alongside keyboard:

input do |i|
  i.jump [:space, :w], gamepad: :a           # Space/W or A button
  i.attack :z, gamepad: :x                   # Z key or X button
  i.pause :escape, gamepad: :start           # Escape or Start button
  i.dash :shift, gamepad: [:lb, :rb]         # Shift or either bumper
end

By default, gamepad bindings respond to any connected gamepad. To bind to a specific gamepad:

input do |i|
  # Player 1 controls (gamepad 0 only)
  i.p1_jump :space, gamepad: :a, gamepad_index: 0

  # Player 2 controls (gamepad 1 only)
  i.p2_jump :enter, gamepad: :a, gamepad_index: 1
end

Available Gamepad Button Symbols

Face buttons: :a, :b, :x, :y

D-pad: :dpad_up, :dpad_down, :dpad_left, :dpad_right

Shoulders: :lb, :rb (bumpers), :lt, :rt (triggers as buttons)

Thumbsticks: :l3, :r3 (stick click)

Center: :start, :select, :guide

Gamepad (Raw Access)

For direct gamepad access without action mapping, use the Gamepad module.

Connection Status

# Check how many gamepads are connected
num_gamepads = Gamepad.count

# Check specific gamepad
if Gamepad.connected?(0)
  puts "Gamepad 0: #{Gamepad.name(0)}"
end

Button State

# Check button state on gamepad 0
Gamepad.down?(0, :a)        # Currently held
Gamepad.pressed?(0, :b)     # Just pressed this frame
Gamepad.released?(0, :x)    # Just released this frame

# Check any connected gamepad
Gamepad.any_pressed?(:start)  # Start pressed on any gamepad
Gamepad.any_down?(:a)         # A button held on any gamepad

Analog Sticks

# Read analog stick with dead zone applied (-1.0 to 1.0)
move_x = Gamepad.axis(0, :left_x)
move_y = Gamepad.axis(0, :left_y)
aim_x = Gamepad.axis(0, :right_x)
aim_y = Gamepad.axis(0, :right_y)

# Read raw axis value (no dead zone)
raw_x = Gamepad.axis_raw(0, :left_x)

Available axes: :left_x, :left_y, :right_x, :right_y, :left_trigger, :right_trigger

Dead Zone Configuration

# Set inner dead zone (default: 0.15)
# Values below this threshold are treated as zero
Gamepad.dead_zone = 0.2

# Set outer dead zone (default: 0.95)
# Values above this threshold are treated as 1.0/-1.0
Gamepad.outer_dead_zone = 0.98

Vibration / Rumble

# Trigger rumble (0.0 to 1.0 intensity)
Gamepad.vibrate(0, left: 0.5, right: 0.3, duration: 0.2)

# Or with positional arguments
Gamepad.vibrate(0, 0.5, 0.5, 0.1)  # gamepad, left, right, duration

Complete Gamepad Example

def update(dt)
  return unless Gamepad.connected?(0)

  # Analog movement
  move_x = Gamepad.axis(0, :left_x)
  move_y = Gamepad.axis(0, :left_y)
  @player.move(move_x * @speed * dt, move_y * @speed * dt)

  # Analog aiming
  aim_x = Gamepad.axis(0, :right_x)
  aim_y = Gamepad.axis(0, :right_y)
  if aim_x.abs > 0.1 || aim_y.abs > 0.1
    @player.aim_direction = Math.atan2(aim_y, aim_x)
  end

  # Triggers as analog input
  shoot_power = Gamepad.axis(0, :right_trigger)
  @player.charge_shot(shoot_power) if shoot_power > 0.1

  # Button actions
  @player.jump if Gamepad.pressed?(0, :a)
  @player.dash if Gamepad.pressed?(0, :lb)

  # Rumble on hit
  if @player.took_damage?
    Gamepad.vibrate(0, left: 0.8, right: 0.4, duration: 0.15)
  end
end

Example: Complete Input Setup

include GMR

def init
  # Define all input mappings upfront
  input do |i|
    i.move_left [:a, :left]
    i.move_right [:d, :right]
    i.move_up [:w, :up]
    i.move_down [:s, :down]
    i.jump [:space]
    i.attack :z, mouse: :left
    i.interact :e
    i.pause :escape
  end

  # Define menu context
  input_context :menu do |i|
    i.confirm [:enter, :space]
    i.cancel :escape
    i.nav_up [:up, :w]
    i.nav_down [:down, :s]
  end

  # Register callbacks for discrete actions
  Input.on(:pause) { toggle_pause }

  @player = Player.new(400, 300)
  @paused = false
end

def update(dt)
  return if @paused

  # Continuous movement
  vx, vy = 0, 0
  vx -= 1 if Input.action_down?(:move_left)
  vx += 1 if Input.action_down?(:move_right)
  vy -= 1 if Input.action_down?(:move_up)
  vy += 1 if Input.action_down?(:move_down)

  @player.move(vx, vy, dt)

  # Discrete actions
  @player.jump if Input.action_pressed?(:jump)
  @player.attack if Input.action_pressed?(:attack)
  @player.interact if Input.action_pressed?(:interact)
end

def toggle_pause
  @paused = !@paused
  if @paused
    Input.push_context(:menu)
  else
    Input.pop_context
  end
end

Input During Console

When the developer console is open, you typically want to ignore game input:

def update(dt)
  return if Console.open?

  # Normal input handling
  @player.update(dt)
end

The console captures keyboard input for its own use. Checking Console.open? prevents your game from responding to keys meant for the console.

See Also

  • Game Loop - Where to put input handling
  • Scenes - Input contexts with scene transitions
  • Console - Developer console interaction