Class: Ruby2D::Gamepad

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby2d/gamepad.rb

Overview

A connected gamepad. Lives from connect to disconnect; reconnects produce a new object. Identity-based equality (default Ruby behavior) makes Gamepad instances stable hash keys for per-pad state.

Disconnected pads remain safe to poll: held? etc. return false, axis returns 0.0, and feedback methods return false silently.

Constant Summary collapse

BUTTON_ENUM =

SDL3 enum values for symbol → integer translation. These mirror the R2D_BUTTON_* and R2D_AXIS_* constants in ruby2d.h. The C side already speaks symbols for events; these are only needed for capability queries that go from Ruby through to SDL.

{
  south:           0,
  east:            1,
  west:            2,
  north:           3,
  back:            4,
  guide:           5,
  start:           6,
  left_stick:      7,
  right_stick:     8,
  left_shoulder:   9,
  right_shoulder:  10,
  dpad_up:         11,
  dpad_down:       12,
  dpad_left:       13,
  dpad_right:      14,
  misc1:           15,
  paddle1:         16,
  paddle2:         17,
  paddle3:         18,
  paddle4:         19,
  touchpad:        20,
  misc2:           21,
  misc3:           22,
  misc4:           23,
  misc5:           24,
  misc6:           25
}.freeze
AXIS_ENUM =
{
  left_x:        0,
  left_y:        1,
  right_x:       2,
  right_y:       3,
  left_trigger:  4,
  right_trigger: 5
}.freeze
DEFAULT_DEAD_ZONE =

Sticks only — triggers (0.0..1.0) are exempt because they rest at zero and rarely drift. 0.05 should cover typical worst-case stick drift.

0.05
TRIGGER_AXES =
%i[left_trigger right_trigger].freeze
AXIS_NAMES =

Ordered list of axis names. axes always returns the full hash so the shape is stable.

AXIS_ENUM.keys.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(window, id, name) ⇒ Gamepad

Returns a new instance of Gamepad.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/ruby2d/gamepad.rb', line 66

def initialize(window, id, name)
  @window = window
  @id = id
  @name = name || ''
  @connected = true
  @dead_zone = DEFAULT_DEAD_ZONE

  # Cached per-connection metadata. `type` and capabilities don't change
  # while the pad is connected, so we read them once and avoid the SDL
  # round-trip on every call.
  @type = Ext.window_gamepad_type(window, id)
  caps = Ext.window_gamepad_caps(window, id)
  @cap_rumble          = caps & 0x1 != 0
  @cap_rumble_triggers = caps & 0x2 != 0
  @cap_led             = caps & 0x4 != 0

  # Per-button and per-axis presence is also cached at connect — one SDL
  # call per known enum, then `has?(:button, name)` / `has?(:axis, name)`
  # are pure hash reads. Cheap (~27 calls total) and matches the
  # "capability checks are cached at connect time" rule in USAGE.md.
  @btn_present  = BUTTON_ENUM.each_with_object({}) do |(sym, enum), h|
    h[sym] = Ext.window_gamepad_has_button(window, id, enum)
  end
  @axis_present = AXIS_ENUM.each_with_object({}) do |(sym, enum), h|
    h[sym] = Ext.window_gamepad_has_axis(window, id, enum)
  end

  # Frame-scoped event sets. Pressed/released/axes_moved are cleared by
  # `_clear_frame_state`; held is also cleared and refilled each frame
  # from the C-side held loop.
  @buttons_down  = []
  @buttons_up    = []
  @buttons_held  = []
  @axes_moved    = []

  # Sticky axis values populated by motion events.
  @axis_values = {}
  AXIS_NAMES.each { |a| @axis_values[a] = 0.0 }
  @raw_axis_values = @axis_values.dup
end

Instance Attribute Details

#dead_zoneObject

Returns the value of attribute dead_zone.



64
65
66
# File 'lib/ruby2d/gamepad.rb', line 64

def dead_zone
  @dead_zone
end

#idObject (readonly)

Returns the value of attribute id.



63
64
65
# File 'lib/ruby2d/gamepad.rb', line 63

def id
  @id
end

#nameObject (readonly)

Returns the value of attribute name.



63
64
65
# File 'lib/ruby2d/gamepad.rb', line 63

def name
  @name
end

#typeObject (readonly)

Returns the value of attribute type.



63
64
65
# File 'lib/ruby2d/gamepad.rb', line 63

def type
  @type
end

Instance Method Details

#_apply_axis(axis, raw_value) ⇒ Object

Returns the dead-zoned value (which is what handlers receive), or nil if the value didn't move outside the dead-zoned region — letting the caller suppress event emission for motion entirely inside the dead zone.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/ruby2d/gamepad.rb', line 279

def _apply_axis(axis, raw_value)
  return nil unless @connected
  return nil unless AXIS_ENUM.key?(axis)

  previous = @axis_values[axis] || 0.0
  dz = apply_dead_zone(axis, raw_value)

  @raw_axis_values[axis] = raw_value
  @axis_values[axis] = dz

  if dz != previous
    @axes_moved << axis unless @axes_moved.include?(axis)
    dz
  else
    nil
  end
end

#_apply_button_down(button) ⇒ Object

----- Internal: dispatch hooks called from gamepad_callback -----



259
260
261
262
263
# File 'lib/ruby2d/gamepad.rb', line 259

def _apply_button_down(button)
  return unless @connected
  @buttons_down << button unless @buttons_down.include?(button)
  @buttons_held << button unless @buttons_held.include?(button)
end

#_apply_button_held(button) ⇒ Object



265
266
267
268
# File 'lib/ruby2d/gamepad.rb', line 265

def _apply_button_held(button)
  return unless @connected
  @buttons_held << button unless @buttons_held.include?(button)
end

#_apply_button_up(button) ⇒ Object



270
271
272
273
274
# File 'lib/ruby2d/gamepad.rb', line 270

def _apply_button_up(button)
  return unless @connected
  @buttons_up << button unless @buttons_up.include?(button)
  @buttons_held.delete(button)
end

#_clear_frame_stateObject



297
298
299
300
301
302
# File 'lib/ruby2d/gamepad.rb', line 297

def _clear_frame_state
  @buttons_down.clear
  @buttons_up.clear
  @buttons_held.clear
  @axes_moved.clear
end

#_disconnectObject



304
305
306
307
308
309
310
311
312
313
314
# File 'lib/ruby2d/gamepad.rb', line 304

def _disconnect
  @connected = false
  @buttons_down.clear
  @buttons_up.clear
  @buttons_held.clear
  @axes_moved.clear
  AXIS_NAMES.each do |a|
    @axis_values[a] = 0.0
    @raw_axis_values[a] = 0.0
  end
end

#axesObject



160
161
162
# File 'lib/ruby2d/gamepad.rb', line 160

def axes
  AXIS_NAMES.each_with_object({}) { |a, h| h[a] = @axis_values[a] || 0.0 }
end

#axes_movedObject



169
# File 'lib/ruby2d/gamepad.rb', line 169

def axes_moved        = @axes_moved.dup

#axis(name, raw: false) ⇒ Object



151
152
153
154
155
156
# File 'lib/ruby2d/gamepad.rb', line 151

def axis(name, raw: false)
  return 0.0 unless AXIS_ENUM.key?(name)

  v = (raw ? @raw_axis_values : @axis_values)[name]
  v || 0.0
end

#axis_moved?(axis) ⇒ Boolean

Returns:

  • (Boolean)


168
# File 'lib/ruby2d/gamepad.rb', line 168

def axis_moved?(axis) = @axes_moved.include?(axis)

#batteryObject

Battery — live query, since charge level changes during play. Returns one of :wired, :full, :medium, :low, :empty, or nil when SDL can't determine the state.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ruby2d/gamepad.rb', line 131

def battery
  return nil unless @connected

  state, percent = Ext.window_gamepad_battery(@window, @id)
  case state
  when :wired then :wired
  when :on_battery
    case percent
    when -1 then :medium  # plugged into a battery slot but no level reported
    when 0..9 then :empty
    when 10..29 then :low
    when 30..74 then :medium
    else :full
    end
  end
end

#buttons_heldObject



158
# File 'lib/ruby2d/gamepad.rb', line 158

def buttons_held = @buttons_held.dup

#connected?Boolean

Returns:

  • (Boolean)


107
# File 'lib/ruby2d/gamepad.rb', line 107

def connected? = @connected

#debug_infoObject

Read-only snapshot of low-level metadata SDL exposes about the pad — useful when adding a custom mapping for an unrecognized device, or diagnosing why a connected device behaves oddly. Returns nil when the pad is disconnected.

Keys:

:guid           — 32-char SDL GUID hex string (the same string used
                in `gamepads.txt` mapping entries)
:vendor_id      — USB-style vendor ID (Integer; format as `%04x`)
:product_id     — USB-style product ID (Integer; format as `%04x`)
:version        — product version (Integer; format as `%04x`)
:serial         — String, or nil if SDL has no serial for this pad
:connection     — :wired, :wireless, or :unknown
:real_type      — the underlying gamepad type before any user mapping
                remaps it (:xbox / :playstation / :nintendo /
                :generic / :unknown)
:num_touchpads  — count of touchpad surfaces (PS4/PS5 = 1, most = 0)
:mapping        — the resolved SDL gamepad mapping string, or nil


240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/ruby2d/gamepad.rb', line 240

def debug_info
  return nil unless @connected

  arr = Ext.window_gamepad_debug_info(@window, @id) or return nil
  {
    guid:          arr[0],
    vendor_id:     arr[1],
    product_id:    arr[2],
    version:       arr[3],
    serial:        arr[4],
    connection:    arr[5],
    real_type:     arr[6],
    num_touchpads: arr[7],
    mapping:       arr[8]
  }
end

#has?(capability, name = nil) ⇒ Boolean

Capability check. Forms:

pad.has?(:rumble)
pad.has?(:rumble_triggers)
pad.has?(:led)
pad.has?(:button, :paddle1)
pad.has?(:axis, :left_trigger)

Returns:

  • (Boolean)


115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ruby2d/gamepad.rb', line 115

def has?(capability, name = nil)
  return false unless @connected

  case capability
  when :rumble           then @cap_rumble
  when :rumble_triggers  then @cap_rumble_triggers
  when :led              then @cap_led
  when :button           then @btn_present[name]  || false
  when :axis             then @axis_present[name] || false
  else                        false
  end
end

#held?(button) ⇒ Boolean

Polling — currently held / current axis position.

Returns:

  • (Boolean)


149
# File 'lib/ruby2d/gamepad.rb', line 149

def held?(button) = @buttons_held.include?(button)

#joystick_stateObject

Snapshot of the underlying joystick's raw input state — buttons, axes (raw -32768..32767), and hats (SDL hat bitmask). Bypasses any gamepad mapping. Intended for mapping-generation tools that need to observe the physical hardware before SDL remaps it. Returns nil when the pad is disconnected.



215
216
217
218
219
220
# File 'lib/ruby2d/gamepad.rb', line 215

def joystick_state
  return nil unless @connected

  arr = Ext.window_gamepad_joystick_state(@window, @id) or return nil
  { buttons: arr[0], axes: arr[1], hats: arr[2] }
end

#led=(rgb) ⇒ Object

Convenience setter form of set_led. As a Ruby assignment it returns the assigned value, not the success Boolean — call set_led if you need that.



206
207
208
# File 'lib/ruby2d/gamepad.rb', line 206

def led=(rgb)
  set_led(rgb)
end

#pressed?(button) ⇒ Boolean

Polling — frame-scoped transitions. Cleared each frame by _clear_frame_state.

Returns:

  • (Boolean)


166
# File 'lib/ruby2d/gamepad.rb', line 166

def pressed?(button)  = @buttons_down.include?(button)

#released?(button) ⇒ Boolean

Returns:

  • (Boolean)


167
# File 'lib/ruby2d/gamepad.rb', line 167

def released?(button) = @buttons_up.include?(button)

#rumble(strength: nil, low: nil, high: nil, duration: 0.2) ⇒ Object

Feedback. rumble, rumble_triggers, and set_led return false when the pad is disconnected or the SDL call fails — never raise. led= is a convenience alias of set_led; being a Ruby assignment it evaluates to the assigned value, not the success Boolean, so use set_led when you need it.

rumble(strength:, duration:) is the simple form; the low/high form exposes the dual-motor pattern most pads expose.



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ruby2d/gamepad.rb', line 178

def rumble(strength: nil, low: nil, high: nil, duration: 0.2)
  return false unless @connected

  if strength
    low ||= strength
    high ||= strength
  end
  low  ||= 0.0
  high ||= 0.0
  Ext.window_gamepad_rumble(@window, @id, low.to_f, high.to_f, duration.to_f)
end

#rumble_triggers(left: 0.0, right: 0.0, duration: 0.2) ⇒ Object



190
191
192
193
194
195
# File 'lib/ruby2d/gamepad.rb', line 190

def rumble_triggers(left: 0.0, right: 0.0, duration: 0.2)
  return false unless @connected

  Ext.window_gamepad_rumble_triggers(@window, @id,
               left.to_f, right.to_f, duration.to_f)
end

#set_led(rgb) ⇒ Object

Set the LED color from an [r, g, b] triple. Returns false on failure.



198
199
200
201
202
# File 'lib/ruby2d/gamepad.rb', line 198

def set_led(rgb)
  return false unless @connected
  r, g, b = rgb
  Ext.window_gamepad_set_led(@window, @id, r.to_i, g.to_i, b.to_i)
end