Class: RGame::Engine::InputMap

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/engine/input/input_map.rb

Overview

What physical inputs mean, for one player.

map = InputMap.new(
thrust: { axis: [Controls::KEY_DOWN, Controls::KEY_UP], stick: Controls::AXIS_TRIGGER_RIGHT },
fire:   { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
)

One entry per action, naming physical ids from RGame::Util::Controls directly. That is the whole point of this class: it is the single table a rebinding screen edits, and it holds nothing but integers, so the engine layer may own one outright.

Three kinds of source, and an action may combine them:

Key Reads as Meaning
buttons: held? down if any listed id is down
axis: axis [negative_id, positive_id], or a list of such pairs — a digital axis from buttons
stick: axis an analog axis id, for a real stick or trigger

One table serves every device

Listing a key and a pad button in the same entry is safe, and needs no per-device branching, because a device only answers for its own kind of input — asking a gamepad about a keyboard scancode is false, never the keyboard's answer (see docs/api/input.md). So fire can be "Space or A" and each player's device picks out the half that applies to it.

An axis can have several pairs, like a button can have several ids

axis: [KEY_LEFT, KEY_RIGHT] is the common case and stays a bare pair. A list of pairs binds more than one control to the same axis:

move_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
               [Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
        stick: Controls::AXIS_LEFT_X }

Without it a d-pad cannot drive movement at all, because the same action already needed the arrow keys. The largest deflection wins, so the pairs cost nothing on a device that has only one of them.

This replaces a two-stage scheme in which a game's action map named RGame::Core::Input's action names, which named physical ids — two tables in series, neither of them the one a config screen wanted, and the lower one unreachable from the engine layer, which may not name Core at all.

A stick's sign is the device's, not the game's

AXIS_LEFT_Y is positive downwards, like screen coordinates. An action that wants the opposite ("thrust", "climb") negates at the call site or binds a trigger instead — the map stays declarative rather than growing an inversion flag that every reader would then have to check for.

Defined Under Namespace

Classes: Binding

Constant Summary collapse

Controls =
RGame::Util::Controls
SOURCES =
%i[buttons axis stick].freeze
UI =

The universal set, merged into every map unless the game overrides it.

The UI package navigates and activates through these, so a control can rely on them existing for every player without a game having declared them. They are prefixed rather than plain (ui_up, not up) so a game is free to use :up for something of its own.

ui_cancel is Escape, which is why RGame::Game's quit key is F2: the button a player expects to back out of a menu belongs to the menu.

{
  ui_up: { buttons: [Controls::KEY_UP, Controls::PAD_DPAD_UP] },
  ui_down: { buttons: [Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN] },
  ui_left: { buttons: [Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT] },
  ui_right: { buttons: [Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT] },
  ui_confirm: { buttons: [Controls::KEY_RETURN, Controls::KEY_SPACE, Controls::PAD_A] },
  ui_cancel: { buttons: [Controls::KEY_ESCAPE, Controls::PAD_B] }
}.freeze
DEFAULT_ACTIONS =

A playable starting point: eight-way movement on the arrows or the left stick, and a fire button. A game that wants exactly this declares nothing at all.

{
  move_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
                   [Controls::KEY_A, Controls::KEY_D],
                   [Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
            stick: Controls::AXIS_LEFT_X },
  move_y: { axis: [[Controls::KEY_UP, Controls::KEY_DOWN],
                   [Controls::KEY_W, Controls::KEY_S],
                   [Controls::PAD_DPAD_UP, Controls::PAD_DPAD_DOWN]],
            stick: Controls::AXIS_LEFT_Y },
  fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries = {}) ⇒ InputMap

entries are merged over the universal UI set, so declaring a game's actions never costs it the ones the UI needs.



108
109
110
# File 'lib/rgame/engine/input/input_map.rb', line 108

def initialize(entries = {})
  @bindings = UI.merge(entries).to_h { |name, entry| [name, build(name, entry)] }.freeze
end

Instance Attribute Details

#bindingsObject (readonly)

Every action this map can answer for, as name => Binding.



104
105
106
# File 'lib/rgame/engine/input/input_map.rb', line 104

def bindings
  @bindings
end

Class Method Details

.defaultObject

The default map: the UI set plus DEFAULT_ACTIONS.



113
# File 'lib/rgame/engine/input/input_map.rb', line 113

def self.default = new(DEFAULT_ACTIONS)

Instance Method Details

#[](action) ⇒ Object



119
# File 'lib/rgame/engine/input/input_map.rb', line 119

def [](action) = @bindings[action]

#actionsObject



121
# File 'lib/rgame/engine/input/input_map.rb', line 121

def actions = @bindings.keys

#merge(entries) ⇒ Object

A copy with entries overriding, which is how a game rebinds one action without restating the rest.



117
# File 'lib/rgame/engine/input/input_map.rb', line 117

def merge(entries) = self.class.new(to_h.merge(entries))

#to_hObject

The entries in the shape they were declared in, so a map can be edited and rebuilt (a config screen) or merged.



125
126
127
128
129
130
131
132
133
# File 'lib/rgame/engine/input/input_map.rb', line 125

def to_h
  @bindings.to_h do |name, binding|
    entry = {}
    entry[:buttons] = binding.buttons if binding.buttons
    entry[:axis] = binding.pairs.size == 1 ? binding.pairs.first : binding.pairs if binding.pairs
    entry[:stick] = binding.stick if binding.stick
    [name, entry]
  end
end