Skip to main content
View
Mystery Gift — Step 6 / 11Handle keyboard input
1. Prepare the assets and text before creating a UI
2. Create a UI scene
3. Create a Composition
4. Create a SpriteStack component
5. Add a data layer with PFM
6. Handle keyboard input
7. Handle mouse input
8. Add i18n text
9. Use confirmation dialogs
10. Create animations
11. Customize GenericBase

Handle keyboard input

This guide builds on the data layer guide: the scene now shows the player's claimed gifts. Here we add keyboard handling -- navigating the list with the arrows and quitting with B -- and we move the input code out of 001 Main.rb into its own file.

Principle

Keyboard input is split into two mechanisms:

  • Button keys (A/B/X/Y): handled by automatic_input_update, which calls Input.trigger? -- the button fires once per press.
  • Directional keys (UP/DOWN): handled with Input.repeat? -- the key fires continuously while held, after an initial delay.

Two rules matter:

  • update_inputs and the AIU_KEY2METHOD constant must stay public (above private). The framework calls update_inputs from outside the class; if it were private, the framework could not reach it.
  • Input handling lives in its own file that reopens the scene class (003 Input.rb). In the scene guide we put a temporary update_inputs/action_b directly in 001 Main.rb; now we move them here and expand them. The same action methods are shared by keyboard and mouse.

The Input file

Create scripts/20 MysteryGift/003 GamePlay/003 Input.rb:

module GamePlay
# Keyboard input handling for the Mystery Gift scene
class MysteryGift < BaseCleanUpdate::FrameBalanced
# Key-to-action mapping
AIU_KEY2METHOD = { A: :action_a, B: :action_b }

# Handle keyboard input each frame
# @return [Boolean]
def update_inputs
return false unless @composition.done?
return false unless automatic_input_update(AIU_KEY2METHOD)

return update_navigation
end

private

# Handle directional key navigation in the gift list
# @return [Boolean]
def update_navigation
if Input.repeat?(:UP)
play_cursor_se if @composition.move_selection(-1)
return false
elsif Input.repeat?(:DOWN)
play_cursor_se if @composition.move_selection(1)
return false
end

return true
end

# Action triggered by the B button -- quit
def action_b
play_cancel_se
@running = false
end
end
end
  • AIU_KEY2METHOD maps a key to a method name. We already list A: :action_a here, but we only define action_b for now -- automatic_input_update checks respond_to? before calling, so pressing A simply does nothing until we add action_a in the dialogs guide.
  • @composition.done? blocks input while an animation plays. It always returns true today (no animations yet), but wiring the guard now means input is automatically blocked once we add animations.
  • automatic_input_update(AIU_KEY2METHOD) checks Input.trigger? for each mapped key and calls the matching method. It returns false when it consumed a press, which we forward to stop further processing.
  • update_navigation handles the arrows separately with Input.repeat?. It returns false to consume the input, true to let processing continue.
  • play_cursor_se and play_cancel_se are the standard navigation and cancel sound effects, inherited from GamePlay::Base.
  • action_b sets @running to false to exit the scene.

Because this file reopens GamePlay::MysteryGift, remove the temporary update_inputs and action_b from 001 Main.rb -- they now live here. 001 Main.rb becomes:

module GamePlay
# Mystery Gift scene -- displays the player's claimed gifts
class MysteryGift < BaseCleanUpdate::FrameBalanced
include UI::MysteryGift

# Create the scene
def initialize
super
@running = true
$user_data[:mystery_gift] ||= PFM::MysteryGift.new
end

# Update graphics each frame
def update_graphics
@base_ui.update_background_animation
@composition.update
end

private

# Create all the graphics for the scene
def create_graphics
create_viewport
create_base_ui
create_composition
Graphics.sort_z
end

# Create the base UI with button texts
def create_base_ui
@base_ui = UI::MysteryGiftBase.new(@viewport, button_texts)
end

# Create the composition
def create_composition
@composition = Composition.new(@viewport, $user_data[:mystery_gift])
end

# Return the button texts for the ctrl buttons [A, X, Y, B]
# @return [Array<String, nil>]
def button_texts
return [nil, nil, nil, 'Quit']
end
end
end

GamePlay.mystery_gift_class = GamePlay::MysteryGift

Selection in the Composition

Navigation asks the Composition to move the selected row. Add these two public methods to scripts/20 MysteryGift/002 UI/999 Composition.rb (next to refresh):

# Select a row by index
# @param index [Integer]
def select_row(index)
@rows.each_with_index { |row, row_index| row.selected = (row_index == index) }
@selected_index = index
end

# Move selection by delta, clamped to the valid range
# @param delta [Integer] -1 for up, +1 for down
# @return [Boolean] true if the selection changed
def move_selection(delta)
gift_count = @mystery_data.claimed_count
return false if gift_count == 0

new_index = (@selected_index + delta).clamp(0, [gift_count - 1, VISIBLE_ROWS - 1].min)
return false if new_index == @selected_index

select_row(new_index)
return true
end
  • select_row highlights one row and stores its index in @selected_index. It is also reused by mouse handling in the next guide.
  • move_selection computes the new index, clamped so the cursor stops at the edges of the list. It returns true only if the selection actually changed -- the scene uses that to play the cursor sound only when something moved.
  • The clamp upper bound is [gift_count - 1, VISIBLE_ROWS - 1].min: you cannot select beyond the number of claimed gifts, nor beyond the number of visible rows.

Wrapping vs clamping

Mystery Gift uses .clamp: the cursor stops at the first and last entries. For a menu where the cursor should loop back to the start after the last item, use modulo instead:

# Wrap the index around (loops back to the start)
new_index = (@selected_index + delta) % gift_count
  • % gift_count wraps: going past the last entry returns to the first, and vice versa.
  • Use .clamp(0, max) for a bounded list, % size for a looping menu. The choice depends on the scene.

Try it

Claim a few codes from the console so there is something to navigate, then open the scene:

$user_data[:mystery_gift].claim('POTION50')
$user_data[:mystery_gift].claim('MASTERBALL')
GamePlay.open_mystery_gift

UP and DOWN move the highlight between the two rows (with the cursor sound), and B closes the scene.

Conclusion

  • update_inputs and AIU_KEY2METHOD must be public -- the framework calls update_inputs from outside.
  • Button keys (A/B) use Input.trigger? via automatic_input_update; directional keys (UP/DOWN) use Input.repeat?.
  • automatic_input_update skips keys whose action method is not defined yet, so you can map a key before implementing its action.
  • Input handling reopens the scene class in its own file (003 Input.rb); the action methods are shared with the mouse.
  • Always check @composition.done? before accepting input.
  • Use .clamp for a bounded list, modulo for a looping menu.