Class: Ruby2D::Gamepad
- Inherits:
-
Object
- Object
- Ruby2D::Gamepad
- 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_*andR2D_AXIS_*constants inruby2d.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.
axesalways returns the full hash so the shape is stable. AXIS_ENUM.keys.freeze
Instance Attribute Summary collapse
-
#dead_zone ⇒ Object
Returns the value of attribute dead_zone.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#type ⇒ Object
readonly
Returns the value of attribute type.
Instance Method Summary collapse
-
#_apply_axis(axis, raw_value) ⇒ Object
Returns the dead-zoned value (which is what handlers receive), or
nilif the value didn't move outside the dead-zoned region — letting the caller suppress event emission for motion entirely inside the dead zone. -
#_apply_button_down(button) ⇒ Object
----- Internal: dispatch hooks called from gamepad_callback -----.
- #_apply_button_held(button) ⇒ Object
- #_apply_button_up(button) ⇒ Object
- #_clear_frame_state ⇒ Object
- #_disconnect ⇒ Object
- #axes ⇒ Object
- #axes_moved ⇒ Object
- #axis(name, raw: false) ⇒ Object
- #axis_moved?(axis) ⇒ Boolean
-
#battery ⇒ Object
Battery — live query, since charge level changes during play.
- #buttons_held ⇒ Object
- #connected? ⇒ Boolean
-
#debug_info ⇒ Object
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.
-
#has?(capability, name = nil) ⇒ Boolean
Capability check.
-
#held?(button) ⇒ Boolean
Polling — currently held / current axis position.
-
#initialize(window, id, name) ⇒ Gamepad
constructor
A new instance of Gamepad.
-
#joystick_state ⇒ Object
Snapshot of the underlying joystick's raw input state — buttons, axes (raw -32768..32767), and hats (SDL hat bitmask).
-
#led=(rgb) ⇒ Object
Convenience setter form of
set_led. -
#pressed?(button) ⇒ Boolean
Polling — frame-scoped transitions.
- #released?(button) ⇒ Boolean
-
#rumble(strength: nil, low: nil, high: nil, duration: 0.2) ⇒ Object
Feedback.
- #rumble_triggers(left: 0.0, right: 0.0, duration: 0.2) ⇒ Object
-
#set_led(rgb) ⇒ Object
Set the LED color from an
[r, g, b]triple.
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, 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_zone ⇒ Object
Returns the value of attribute dead_zone.
64 65 66 |
# File 'lib/ruby2d/gamepad.rb', line 64 def dead_zone @dead_zone end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
63 64 65 |
# File 'lib/ruby2d/gamepad.rb', line 63 def id @id end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
63 64 65 |
# File 'lib/ruby2d/gamepad.rb', line 63 def name @name end |
#type ⇒ Object (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 () return unless @connected @buttons_down << unless @buttons_down.include?() @buttons_held << unless @buttons_held.include?() end |
#_apply_button_held(button) ⇒ Object
265 266 267 268 |
# File 'lib/ruby2d/gamepad.rb', line 265 def () return unless @connected @buttons_held << unless @buttons_held.include?() end |
#_apply_button_up(button) ⇒ Object
270 271 272 273 274 |
# File 'lib/ruby2d/gamepad.rb', line 270 def () return unless @connected @buttons_up << unless @buttons_up.include?() @buttons_held.delete() end |
#_clear_frame_state ⇒ Object
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 |
#_disconnect ⇒ Object
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 |
#axes ⇒ Object
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_moved ⇒ Object
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
168 |
# File 'lib/ruby2d/gamepad.rb', line 168 def axis_moved?(axis) = @axes_moved.include?(axis) |
#battery ⇒ Object
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_held ⇒ Object
158 |
# File 'lib/ruby2d/gamepad.rb', line 158 def = @buttons_held.dup |
#connected? ⇒ Boolean
107 |
# File 'lib/ruby2d/gamepad.rb', line 107 def connected? = @connected |
#debug_info ⇒ Object
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)
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.
149 |
# File 'lib/ruby2d/gamepad.rb', line 149 def held?() = @buttons_held.include?() |
#joystick_state ⇒ Object
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.
166 |
# File 'lib/ruby2d/gamepad.rb', line 166 def pressed?() = @buttons_down.include?() |
#released?(button) ⇒ Boolean
167 |
# File 'lib/ruby2d/gamepad.rb', line 167 def released?() = @buttons_up.include?() |
#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 |