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 callsInput.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_inputsand theAIU_KEY2METHODconstant must stay public (aboveprivate). The framework callsupdate_inputsfrom 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 temporaryupdate_inputs/action_bdirectly in001 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_KEY2METHODmaps a key to a method name. We already listA: :action_ahere, but we only defineaction_bfor now --automatic_input_updatechecksrespond_to?before calling, so pressing A simply does nothing until we addaction_ain the dialogs guide.@composition.done?blocks input while an animation plays. It always returnstruetoday (no animations yet), but wiring the guard now means input is automatically blocked once we add animations.automatic_input_update(AIU_KEY2METHOD)checksInput.trigger?for each mapped key and calls the matching method. It returnsfalsewhen it consumed a press, which we forward to stop further processing.update_navigationhandles the arrows separately withInput.repeat?. It returnsfalseto consume the input,trueto let processing continue.play_cursor_seandplay_cancel_seare the standard navigation and cancel sound effects, inherited fromGamePlay::Base.action_bsets@runningtofalseto 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_rowhighlights one row and stores its index in@selected_index. It is also reused by mouse handling in the next guide.move_selectioncomputes the new index, clamped so the cursor stops at the edges of the list. It returnstrueonly 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_countwraps: going past the last entry returns to the first, and vice versa.- Use
.clamp(0, max)for a bounded list,% sizefor 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_inputsandAIU_KEY2METHODmust be public -- the framework callsupdate_inputsfrom outside.- Button keys (A/B) use
Input.trigger?viaautomatic_input_update; directional keys (UP/DOWN) useInput.repeat?. automatic_input_updateskips 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
.clampfor a bounded list, modulo for a looping menu.